1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 package org.eclipse.jgit.transport;
45
46 import java.io.ByteArrayOutputStream;
47 import java.io.File;
48 import java.io.FileInputStream;
49 import java.io.FileNotFoundException;
50 import java.io.IOException;
51 import java.io.InputStream;
52 import java.io.OutputStream;
53 import java.net.HttpURLConnection;
54 import java.net.Proxy;
55 import java.net.ProxySelector;
56 import java.net.URL;
57 import java.net.URLConnection;
58 import java.security.DigestOutputStream;
59 import java.security.GeneralSecurityException;
60 import java.security.InvalidKeyException;
61 import java.security.MessageDigest;
62 import java.security.NoSuchAlgorithmException;
63 import java.text.MessageFormat;
64 import java.text.SimpleDateFormat;
65 import java.util.ArrayList;
66 import java.util.Collections;
67 import java.util.Date;
68 import java.util.HashSet;
69 import java.util.Iterator;
70 import java.util.List;
71 import java.util.Locale;
72 import java.util.Map;
73 import java.util.Properties;
74 import java.util.Set;
75 import java.util.SortedMap;
76 import java.util.TimeZone;
77 import java.util.TreeMap;
78
79 import javax.crypto.Mac;
80 import javax.crypto.spec.SecretKeySpec;
81
82 import org.eclipse.jgit.internal.JGitText;
83 import org.eclipse.jgit.lib.Constants;
84 import org.eclipse.jgit.lib.NullProgressMonitor;
85 import org.eclipse.jgit.lib.ProgressMonitor;
86 import org.eclipse.jgit.util.Base64;
87 import org.eclipse.jgit.util.HttpSupport;
88 import org.eclipse.jgit.util.StringUtils;
89 import org.eclipse.jgit.util.TemporaryBuffer;
90 import org.xml.sax.Attributes;
91 import org.xml.sax.InputSource;
92 import org.xml.sax.SAXException;
93 import org.xml.sax.XMLReader;
94 import org.xml.sax.helpers.DefaultHandler;
95 import org.xml.sax.helpers.XMLReaderFactory;
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113 public class AmazonS3 {
114 private static final Set<String> SIGNED_HEADERS;
115
116 private static final String HMAC = "HmacSHA1";
117
118 private static final String X_AMZ_ACL = "x-amz-acl";
119
120 private static final String X_AMZ_META = "x-amz-meta-";
121
122 static {
123 SIGNED_HEADERS = new HashSet<String>();
124 SIGNED_HEADERS.add("content-type");
125 SIGNED_HEADERS.add("content-md5");
126 SIGNED_HEADERS.add("date");
127 }
128
129 private static boolean isSignedHeader(final String name) {
130 final String nameLC = StringUtils.toLowerCase(name);
131 return SIGNED_HEADERS.contains(nameLC) || nameLC.startsWith("x-amz-");
132 }
133
134 private static String toCleanString(final List<String> list) {
135 final StringBuilder s = new StringBuilder();
136 for (final String v : list) {
137 if (s.length() > 0)
138 s.append(',');
139 s.append(v.replaceAll("\n", "").trim());
140 }
141 return s.toString();
142 }
143
144 private static String remove(final Map<String, String> m, final String k) {
145 final String r = m.remove(k);
146 return r != null ? r : "";
147 }
148
149 private static String httpNow() {
150 final String tz = "GMT";
151 final SimpleDateFormat fmt;
152 fmt = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.US);
153 fmt.setTimeZone(TimeZone.getTimeZone(tz));
154 return fmt.format(new Date()) + " " + tz;
155 }
156
157 private static MessageDigest newMD5() {
158 try {
159 return MessageDigest.getInstance("MD5");
160 } catch (NoSuchAlgorithmException e) {
161 throw new RuntimeException(JGitText.get().JRELacksMD5Implementation, e);
162 }
163 }
164
165
166 private final String publicKey;
167
168
169 private final SecretKeySpec privateKey;
170
171
172 private final ProxySelector proxySelector;
173
174
175 private final String acl;
176
177
178 final int maxAttempts;
179
180
181 private final WalkEncryption encryption;
182
183
184 private final File tmpDir;
185
186
187 private final String domain;
188
189
190 interface Keys {
191 String ACCESS_KEY = "accesskey";
192 String SECRET_KEY = "secretkey";
193 String PASSWORD = "password";
194 String CRYPTO_ALG = "crypto.algorithm";
195 String CRYPTO_VER = "crypto.version";
196 String ACL = "acl";
197 String DOMAIN = "domain";
198 String HTTP_RETRY = "httpclient.retry-max";
199 String TMP_DIR = "tmpdir";
200 }
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234 public AmazonS3(final Properties props) {
235 domain = props.getProperty(Keys.DOMAIN, "s3.amazonaws.com");
236
237 publicKey = props.getProperty(Keys.ACCESS_KEY);
238 if (publicKey == null)
239 throw new IllegalArgumentException(JGitText.get().missingAccesskey);
240
241 final String secret = props.getProperty(Keys.SECRET_KEY);
242 if (secret == null)
243 throw new IllegalArgumentException(JGitText.get().missingSecretkey);
244 privateKey = new SecretKeySpec(Constants.encodeASCII(secret), HMAC);
245
246 final String pacl = props.getProperty(Keys.ACL, "PRIVATE");
247 if (StringUtils.equalsIgnoreCase("PRIVATE", pacl))
248 acl = "private";
249 else if (StringUtils.equalsIgnoreCase("PUBLIC", pacl))
250 acl = "public-read";
251 else if (StringUtils.equalsIgnoreCase("PUBLIC-READ", pacl))
252 acl = "public-read";
253 else if (StringUtils.equalsIgnoreCase("PUBLIC_READ", pacl))
254 acl = "public-read";
255 else
256 throw new IllegalArgumentException("Invalid acl: " + pacl);
257
258 try {
259 encryption = WalkEncryption.instance(props);
260 } catch (GeneralSecurityException e) {
261 throw new IllegalArgumentException(JGitText.get().invalidEncryption, e);
262 }
263
264 maxAttempts = Integer
265 .parseInt(props.getProperty(Keys.HTTP_RETRY, "3"));
266 proxySelector = ProxySelector.getDefault();
267
268 String tmp = props.getProperty(Keys.TMP_DIR);
269 tmpDir = tmp != null && tmp.length() > 0 ? new File(tmp) : null;
270 }
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285 public URLConnection get(final String bucket, final String key)
286 throws IOException {
287 for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
288 final HttpURLConnection c = open("GET", bucket, key);
289 authorize(c);
290 switch (HttpSupport.response(c)) {
291 case HttpURLConnection.HTTP_OK:
292 encryption.validate(c, X_AMZ_META);
293 return c;
294 case HttpURLConnection.HTTP_NOT_FOUND:
295 throw new FileNotFoundException(key);
296 case HttpURLConnection.HTTP_INTERNAL_ERROR:
297 continue;
298 default:
299 throw error(JGitText.get().s3ActionReading, key, c);
300 }
301 }
302 throw maxAttempts(JGitText.get().s3ActionReading, key);
303 }
304
305
306
307
308
309
310
311
312
313
314 public InputStream decrypt(final URLConnection u) throws IOException {
315 return encryption.decrypt(u.getInputStream());
316 }
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338 public List<String> list(final String bucket, String prefix)
339 throws IOException {
340 if (prefix.length() > 0 && !prefix.endsWith("/"))
341 prefix += "/";
342 final ListParser lp = new ListParser(bucket, prefix);
343 do {
344 lp.list();
345 } while (lp.truncated);
346 return lp.entries;
347 }
348
349
350
351
352
353
354
355
356
357
358
359
360
361 public void delete(final String bucket, final String key)
362 throws IOException {
363 for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
364 final HttpURLConnection c = open("DELETE", bucket, key);
365 authorize(c);
366 switch (HttpSupport.response(c)) {
367 case HttpURLConnection.HTTP_NO_CONTENT:
368 return;
369 case HttpURLConnection.HTTP_INTERNAL_ERROR:
370 continue;
371 default:
372 throw error(JGitText.get().s3ActionDeletion, key, c);
373 }
374 }
375 throw maxAttempts(JGitText.get().s3ActionDeletion, key);
376 }
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398 public void put(final String bucket, final String key, final byte[] data)
399 throws IOException {
400 if (encryption != WalkEncryption.NONE) {
401
402
403
404 final OutputStream os = beginPut(bucket, key, null, null);
405 os.write(data);
406 os.close();
407 return;
408 }
409
410 final String md5str = Base64.encodeBytes(newMD5().digest(data));
411 final String lenstr = String.valueOf(data.length);
412 for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
413 final HttpURLConnection c = open("PUT", bucket, key);
414 c.setRequestProperty("Content-Length", lenstr);
415 c.setRequestProperty("Content-MD5", md5str);
416 c.setRequestProperty(X_AMZ_ACL, acl);
417 authorize(c);
418 c.setDoOutput(true);
419 c.setFixedLengthStreamingMode(data.length);
420 final OutputStream os = c.getOutputStream();
421 try {
422 os.write(data);
423 } finally {
424 os.close();
425 }
426
427 switch (HttpSupport.response(c)) {
428 case HttpURLConnection.HTTP_OK:
429 return;
430 case HttpURLConnection.HTTP_INTERNAL_ERROR:
431 continue;
432 default:
433 throw error(JGitText.get().s3ActionWriting, key, c);
434 }
435 }
436 throw maxAttempts(JGitText.get().s3ActionWriting, key);
437 }
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467 public OutputStream beginPut(final String bucket, final String key,
468 final ProgressMonitor monitor, final String monitorTask)
469 throws IOException {
470 final MessageDigest md5 = newMD5();
471 final TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(tmpDir) {
472 @Override
473 public void close() throws IOException {
474 super.close();
475 try {
476 putImpl(bucket, key, md5.digest(), this, monitor,
477 monitorTask);
478 } finally {
479 destroy();
480 }
481 }
482 };
483 return encryption.encrypt(new DigestOutputStream(buffer, md5));
484 }
485
486 void putImpl(final String bucket, final String key,
487 final byte[] csum, final TemporaryBuffer buf,
488 ProgressMonitor monitor, String monitorTask) throws IOException {
489 if (monitor == null)
490 monitor = NullProgressMonitor.INSTANCE;
491 if (monitorTask == null)
492 monitorTask = MessageFormat.format(JGitText.get().progressMonUploading, key);
493
494 final String md5str = Base64.encodeBytes(csum);
495 final long len = buf.length();
496 for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
497 final HttpURLConnection c = open("PUT", bucket, key);
498 c.setFixedLengthStreamingMode(len);
499 c.setRequestProperty("Content-MD5", md5str);
500 c.setRequestProperty(X_AMZ_ACL, acl);
501 encryption.request(c, X_AMZ_META);
502 authorize(c);
503 c.setDoOutput(true);
504 monitor.beginTask(monitorTask, (int) (len / 1024));
505 final OutputStream os = c.getOutputStream();
506 try {
507 buf.writeTo(os, monitor);
508 } finally {
509 monitor.endTask();
510 os.close();
511 }
512
513 switch (HttpSupport.response(c)) {
514 case HttpURLConnection.HTTP_OK:
515 return;
516 case HttpURLConnection.HTTP_INTERNAL_ERROR:
517 continue;
518 default:
519 throw error(JGitText.get().s3ActionWriting, key, c);
520 }
521 }
522 throw maxAttempts(JGitText.get().s3ActionWriting, key);
523 }
524
525 IOException error(final String action, final String key,
526 final HttpURLConnection c) throws IOException {
527 final IOException err = new IOException(MessageFormat.format(
528 JGitText.get().amazonS3ActionFailed, action, key,
529 Integer.valueOf(HttpSupport.response(c)),
530 c.getResponseMessage()));
531 final InputStream errorStream = c.getErrorStream();
532 if (errorStream == null) {
533 return err;
534 }
535
536 try {
537 final ByteArrayOutputStream b = new ByteArrayOutputStream();
538 byte[] buf = new byte[2048];
539 for (;;) {
540 final int n = errorStream.read(buf);
541 if (n < 0) {
542 break;
543 }
544 if (n > 0) {
545 b.write(buf, 0, n);
546 }
547 }
548 buf = b.toByteArray();
549 if (buf.length > 0) {
550 err.initCause(new IOException("\n" + new String(buf)));
551 }
552 } finally {
553 errorStream.close();
554 }
555 return err;
556 }
557
558 IOException maxAttempts(final String action, final String key) {
559 return new IOException(MessageFormat.format(
560 JGitText.get().amazonS3ActionFailedGivingUp, action, key,
561 Integer.valueOf(maxAttempts)));
562 }
563
564 private HttpURLConnection open(final String method, final String bucket,
565 final String key) throws IOException {
566 final Map<String, String> noArgs = Collections.emptyMap();
567 return open(method, bucket, key, noArgs);
568 }
569
570 HttpURLConnection open(final String method, final String bucket,
571 final String key, final Map<String, String> args)
572 throws IOException {
573 final StringBuilder urlstr = new StringBuilder();
574 urlstr.append("http://"); //$NON-NLS-1$
575 urlstr.append(bucket);
576 urlstr.append('.');
577 urlstr.append(domain);
578 urlstr.append('/');
579 if (key.length() > 0)
580 HttpSupport.encode(urlstr, key);
581 if (!args.isEmpty()) {
582 final Iterator<Map.Entry<String, String>> i;
583
584 urlstr.append('?');
585 i = args.entrySet().iterator();
586 while (i.hasNext()) {
587 final Map.Entry<String, String> e = i.next();
588 urlstr.append(e.getKey());
589 urlstr.append('=');
590 HttpSupport.encode(urlstr, e.getValue());
591 if (i.hasNext())
592 urlstr.append('&');
593 }
594 }
595
596 final URL url = new URL(urlstr.toString());
597 final Proxy proxy = HttpSupport.proxyFor(proxySelector, url);
598 final HttpURLConnection c;
599
600 c = (HttpURLConnection) url.openConnection(proxy);
601 c.setRequestMethod(method);
602 c.setRequestProperty("User-Agent", "jgit/1.0");
603 c.setRequestProperty("Date", httpNow());
604 return c;
605 }
606
607 void authorize(final HttpURLConnection c) throws IOException {
608 final Map<String, List<String>> reqHdr = c.getRequestProperties();
609 final SortedMap<String, String> sigHdr = new TreeMap<String, String>();
610 for (final Map.Entry<String, List<String>> entry : reqHdr.entrySet()) {
611 final String hdr = entry.getKey();
612 if (isSignedHeader(hdr))
613 sigHdr.put(StringUtils.toLowerCase(hdr), toCleanString(entry.getValue()));
614 }
615
616 final StringBuilder s = new StringBuilder();
617 s.append(c.getRequestMethod());
618 s.append('\n');
619
620 s.append(remove(sigHdr, "content-md5"));
621 s.append('\n');
622
623 s.append(remove(sigHdr, "content-type"));
624 s.append('\n');
625
626 s.append(remove(sigHdr, "date"));
627 s.append('\n');
628
629 for (final Map.Entry<String, String> e : sigHdr.entrySet()) {
630 s.append(e.getKey());
631 s.append(':');
632 s.append(e.getValue());
633 s.append('\n');
634 }
635
636 final String host = c.getURL().getHost();
637 s.append('/');
638 s.append(host.substring(0, host.length() - domain.length() - 1));
639 s.append(c.getURL().getPath());
640
641 final String sec;
642 try {
643 final Mac m = Mac.getInstance(HMAC);
644 m.init(privateKey);
645 sec = Base64.encodeBytes(m.doFinal(s.toString().getBytes("UTF-8")));
646 } catch (NoSuchAlgorithmException e) {
647 throw new IOException(MessageFormat.format(JGitText.get().noHMACsupport, HMAC, e.getMessage()));
648 } catch (InvalidKeyException e) {
649 throw new IOException(MessageFormat.format(JGitText.get().invalidKey, e.getMessage()));
650 }
651 c.setRequestProperty("Authorization", "AWS " + publicKey + ":" + sec);
652 }
653
654 static Properties properties(final File authFile)
655 throws FileNotFoundException, IOException {
656 final Properties p = new Properties();
657 final FileInputStream in = new FileInputStream(authFile);
658 try {
659 p.load(in);
660 } finally {
661 in.close();
662 }
663 return p;
664 }
665
666 private final class ListParser extends DefaultHandler {
667 final List<String> entries = new ArrayList<String>();
668
669 private final String bucket;
670
671 private final String prefix;
672
673 boolean truncated;
674
675 private StringBuilder data;
676
677 ListParser(final String bn, final String p) {
678 bucket = bn;
679 prefix = p;
680 }
681
682 void list() throws IOException {
683 final Map<String, String> args = new TreeMap<String, String>();
684 if (prefix.length() > 0)
685 args.put("prefix", prefix);
686 if (!entries.isEmpty())
687 args.put("marker", prefix + entries.get(entries.size() - 1));
688
689 for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
690 final HttpURLConnection c = open("GET", bucket, "", args);
691 authorize(c);
692 switch (HttpSupport.response(c)) {
693 case HttpURLConnection.HTTP_OK:
694 truncated = false;
695 data = null;
696
697 final XMLReader xr;
698 try {
699 xr = XMLReaderFactory.createXMLReader();
700 } catch (SAXException e) {
701 throw new IOException(JGitText.get().noXMLParserAvailable);
702 }
703 xr.setContentHandler(this);
704 final InputStream in = c.getInputStream();
705 try {
706 xr.parse(new InputSource(in));
707 } catch (SAXException parsingError) {
708 final IOException p;
709 p = new IOException(MessageFormat.format(JGitText.get().errorListing, prefix));
710 p.initCause(parsingError);
711 throw p;
712 } finally {
713 in.close();
714 }
715 return;
716
717 case HttpURLConnection.HTTP_INTERNAL_ERROR:
718 continue;
719
720 default:
721 throw AmazonS3.this.error("Listing", prefix, c);
722 }
723 }
724 throw maxAttempts("Listing", prefix);
725 }
726
727 @Override
728 public void startElement(final String uri, final String name,
729 final String qName, final Attributes attributes)
730 throws SAXException {
731 if ("Key".equals(name) || "IsTruncated".equals(name))
732 data = new StringBuilder();
733 }
734
735 @Override
736 public void ignorableWhitespace(final char[] ch, final int s,
737 final int n) throws SAXException {
738 if (data != null)
739 data.append(ch, s, n);
740 }
741
742 @Override
743 public void characters(final char[] ch, final int s, final int n)
744 throws SAXException {
745 if (data != null)
746 data.append(ch, s, n);
747 }
748
749 @Override
750 public void endElement(final String uri, final String name,
751 final String qName) throws SAXException {
752 if ("Key".equals(name))
753 entries.add(data.toString().substring(prefix.length()));
754 else if ("IsTruncated".equals(name))
755 truncated = StringUtils.equalsIgnoreCase("true", data.toString());
756 data = null;
757 }
758 }
759 }