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.InvalidKeyException;
60 import java.security.MessageDigest;
61 import java.security.NoSuchAlgorithmException;
62 import java.security.spec.InvalidKeySpecException;
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 private 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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221 public AmazonS3(final Properties props) {
222 domain = props.getProperty("domain", "s3.amazonaws.com");
223 publicKey = props.getProperty("accesskey");
224 if (publicKey == null)
225 throw new IllegalArgumentException(JGitText.get().missingAccesskey);
226
227 final String secret = props.getProperty("secretkey");
228 if (secret == null)
229 throw new IllegalArgumentException(JGitText.get().missingSecretkey);
230 privateKey = new SecretKeySpec(Constants.encodeASCII(secret), HMAC);
231
232 final String pacl = props.getProperty("acl", "PRIVATE");
233 if (StringUtils.equalsIgnoreCase("PRIVATE", pacl))
234 acl = "private";
235 else if (StringUtils.equalsIgnoreCase("PUBLIC", pacl))
236 acl = "public-read";
237 else if (StringUtils.equalsIgnoreCase("PUBLIC-READ", pacl))
238 acl = "public-read";
239 else if (StringUtils.equalsIgnoreCase("PUBLIC_READ", pacl))
240 acl = "public-read";
241 else
242 throw new IllegalArgumentException("Invalid acl: " + pacl);
243
244 try {
245 final String cPas = props.getProperty("password");
246 if (cPas != null) {
247 String cAlg = props.getProperty("crypto.algorithm");
248 if (cAlg == null)
249 cAlg = "PBEWithMD5AndDES";
250 encryption = new WalkEncryption.ObjectEncryptionV2(cAlg, cPas);
251 } else {
252 encryption = WalkEncryption.NONE;
253 }
254 } catch (InvalidKeySpecException e) {
255 throw new IllegalArgumentException(JGitText.get().invalidEncryption, e);
256 } catch (NoSuchAlgorithmException e) {
257 throw new IllegalArgumentException(JGitText.get().invalidEncryption, e);
258 }
259
260 maxAttempts = Integer.parseInt(props.getProperty(
261 "httpclient.retry-max", "3"));
262 proxySelector = ProxySelector.getDefault();
263
264 String tmp = props.getProperty("tmpdir");
265 tmpDir = tmp != null && tmp.length() > 0 ? new File(tmp) : null;
266 }
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281 public URLConnection get(final String bucket, final String key)
282 throws IOException {
283 for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
284 final HttpURLConnection c = open("GET", bucket, key);
285 authorize(c);
286 switch (HttpSupport.response(c)) {
287 case HttpURLConnection.HTTP_OK:
288 encryption.validate(c, X_AMZ_META);
289 return c;
290 case HttpURLConnection.HTTP_NOT_FOUND:
291 throw new FileNotFoundException(key);
292 case HttpURLConnection.HTTP_INTERNAL_ERROR:
293 continue;
294 default:
295 throw error(JGitText.get().s3ActionReading, key, c);
296 }
297 }
298 throw maxAttempts(JGitText.get().s3ActionReading, key);
299 }
300
301
302
303
304
305
306
307
308
309
310 public InputStream decrypt(final URLConnection u) throws IOException {
311 return encryption.decrypt(u.getInputStream());
312 }
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334 public List<String> list(final String bucket, String prefix)
335 throws IOException {
336 if (prefix.length() > 0 && !prefix.endsWith("/"))
337 prefix += "/";
338 final ListParser lp = new ListParser(bucket, prefix);
339 do {
340 lp.list();
341 } while (lp.truncated);
342 return lp.entries;
343 }
344
345
346
347
348
349
350
351
352
353
354
355
356
357 public void delete(final String bucket, final String key)
358 throws IOException {
359 for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
360 final HttpURLConnection c = open("DELETE", bucket, key);
361 authorize(c);
362 switch (HttpSupport.response(c)) {
363 case HttpURLConnection.HTTP_NO_CONTENT:
364 return;
365 case HttpURLConnection.HTTP_INTERNAL_ERROR:
366 continue;
367 default:
368 throw error(JGitText.get().s3ActionDeletion, key, c);
369 }
370 }
371 throw maxAttempts(JGitText.get().s3ActionDeletion, key);
372 }
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394 public void put(final String bucket, final String key, final byte[] data)
395 throws IOException {
396 if (encryption != WalkEncryption.NONE) {
397
398
399
400 final OutputStream os = beginPut(bucket, key, null, null);
401 os.write(data);
402 os.close();
403 return;
404 }
405
406 final String md5str = Base64.encodeBytes(newMD5().digest(data));
407 final String lenstr = String.valueOf(data.length);
408 for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
409 final HttpURLConnection c = open("PUT", bucket, key);
410 c.setRequestProperty("Content-Length", lenstr);
411 c.setRequestProperty("Content-MD5", md5str);
412 c.setRequestProperty(X_AMZ_ACL, acl);
413 authorize(c);
414 c.setDoOutput(true);
415 c.setFixedLengthStreamingMode(data.length);
416 final OutputStream os = c.getOutputStream();
417 try {
418 os.write(data);
419 } finally {
420 os.close();
421 }
422
423 switch (HttpSupport.response(c)) {
424 case HttpURLConnection.HTTP_OK:
425 return;
426 case HttpURLConnection.HTTP_INTERNAL_ERROR:
427 continue;
428 default:
429 throw error(JGitText.get().s3ActionWriting, key, c);
430 }
431 }
432 throw maxAttempts(JGitText.get().s3ActionWriting, key);
433 }
434
435
436
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 public OutputStream beginPut(final String bucket, final String key,
464 final ProgressMonitor monitor, final String monitorTask)
465 throws IOException {
466 final MessageDigest md5 = newMD5();
467 final TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(tmpDir) {
468 @Override
469 public void close() throws IOException {
470 super.close();
471 try {
472 putImpl(bucket, key, md5.digest(), this, monitor,
473 monitorTask);
474 } finally {
475 destroy();
476 }
477 }
478 };
479 return encryption.encrypt(new DigestOutputStream(buffer, md5));
480 }
481
482 private void putImpl(final String bucket, final String key,
483 final byte[] csum, final TemporaryBuffer buf,
484 ProgressMonitor monitor, String monitorTask) throws IOException {
485 if (monitor == null)
486 monitor = NullProgressMonitor.INSTANCE;
487 if (monitorTask == null)
488 monitorTask = MessageFormat.format(JGitText.get().progressMonUploading, key);
489
490 final String md5str = Base64.encodeBytes(csum);
491 final long len = buf.length();
492 for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
493 final HttpURLConnection c = open("PUT", bucket, key);
494 c.setFixedLengthStreamingMode(len);
495 c.setRequestProperty("Content-MD5", md5str);
496 c.setRequestProperty(X_AMZ_ACL, acl);
497 encryption.request(c, X_AMZ_META);
498 authorize(c);
499 c.setDoOutput(true);
500 monitor.beginTask(monitorTask, (int) (len / 1024));
501 final OutputStream os = c.getOutputStream();
502 try {
503 buf.writeTo(os, monitor);
504 } finally {
505 monitor.endTask();
506 os.close();
507 }
508
509 switch (HttpSupport.response(c)) {
510 case HttpURLConnection.HTTP_OK:
511 return;
512 case HttpURLConnection.HTTP_INTERNAL_ERROR:
513 continue;
514 default:
515 throw error(JGitText.get().s3ActionWriting, key, c);
516 }
517 }
518 throw maxAttempts(JGitText.get().s3ActionWriting, key);
519 }
520
521 private IOException error(final String action, final String key,
522 final HttpURLConnection c) throws IOException {
523 final IOException err = new IOException(MessageFormat.format(
524 JGitText.get().amazonS3ActionFailed, action, key,
525 Integer.valueOf(HttpSupport.response(c)),
526 c.getResponseMessage()));
527 final InputStream errorStream = c.getErrorStream();
528 if (errorStream == null)
529 return err;
530
531 final ByteArrayOutputStream b = new ByteArrayOutputStream();
532 byte[] buf = new byte[2048];
533 for (;;) {
534 final int n = errorStream.read(buf);
535 if (n < 0)
536 break;
537 if (n > 0)
538 b.write(buf, 0, n);
539 }
540 buf = b.toByteArray();
541 if (buf.length > 0)
542 err.initCause(new IOException("\n" + new String(buf)));
543 return err;
544 }
545
546 private IOException maxAttempts(final String action, final String key) {
547 return new IOException(MessageFormat.format(
548 JGitText.get().amazonS3ActionFailedGivingUp, action, key,
549 Integer.valueOf(maxAttempts)));
550 }
551
552 private HttpURLConnection open(final String method, final String bucket,
553 final String key) throws IOException {
554 final Map<String, String> noArgs = Collections.emptyMap();
555 return open(method, bucket, key, noArgs);
556 }
557
558 private HttpURLConnection open(final String method, final String bucket,
559 final String key, final Map<String, String> args)
560 throws IOException {
561 final StringBuilder urlstr = new StringBuilder();
562 urlstr.append("http://"); //$NON-NLS-1$
563 urlstr.append(bucket);
564 urlstr.append('.');
565 urlstr.append(domain);
566 urlstr.append('/');
567 if (key.length() > 0)
568 HttpSupport.encode(urlstr, key);
569 if (!args.isEmpty()) {
570 final Iterator<Map.Entry<String, String>> i;
571
572 urlstr.append('?');
573 i = args.entrySet().iterator();
574 while (i.hasNext()) {
575 final Map.Entry<String, String> e = i.next();
576 urlstr.append(e.getKey());
577 urlstr.append('=');
578 HttpSupport.encode(urlstr, e.getValue());
579 if (i.hasNext())
580 urlstr.append('&');
581 }
582 }
583
584 final URL url = new URL(urlstr.toString());
585 final Proxy proxy = HttpSupport.proxyFor(proxySelector, url);
586 final HttpURLConnection c;
587
588 c = (HttpURLConnection) url.openConnection(proxy);
589 c.setRequestMethod(method);
590 c.setRequestProperty("User-Agent", "jgit/1.0");
591 c.setRequestProperty("Date", httpNow());
592 return c;
593 }
594
595 private void authorize(final HttpURLConnection c) throws IOException {
596 final Map<String, List<String>> reqHdr = c.getRequestProperties();
597 final SortedMap<String, String> sigHdr = new TreeMap<String, String>();
598 for (final Map.Entry<String, List<String>> entry : reqHdr.entrySet()) {
599 final String hdr = entry.getKey();
600 if (isSignedHeader(hdr))
601 sigHdr.put(StringUtils.toLowerCase(hdr), toCleanString(entry.getValue()));
602 }
603
604 final StringBuilder s = new StringBuilder();
605 s.append(c.getRequestMethod());
606 s.append('\n');
607
608 s.append(remove(sigHdr, "content-md5"));
609 s.append('\n');
610
611 s.append(remove(sigHdr, "content-type"));
612 s.append('\n');
613
614 s.append(remove(sigHdr, "date"));
615 s.append('\n');
616
617 for (final Map.Entry<String, String> e : sigHdr.entrySet()) {
618 s.append(e.getKey());
619 s.append(':');
620 s.append(e.getValue());
621 s.append('\n');
622 }
623
624 final String host = c.getURL().getHost();
625 s.append('/');
626 s.append(host.substring(0, host.length() - domain.length() - 1));
627 s.append(c.getURL().getPath());
628
629 final String sec;
630 try {
631 final Mac m = Mac.getInstance(HMAC);
632 m.init(privateKey);
633 sec = Base64.encodeBytes(m.doFinal(s.toString().getBytes("UTF-8")));
634 } catch (NoSuchAlgorithmException e) {
635 throw new IOException(MessageFormat.format(JGitText.get().noHMACsupport, HMAC, e.getMessage()));
636 } catch (InvalidKeyException e) {
637 throw new IOException(MessageFormat.format(JGitText.get().invalidKey, e.getMessage()));
638 }
639 c.setRequestProperty("Authorization", "AWS " + publicKey + ":" + sec);
640 }
641
642 static Properties properties(final File authFile)
643 throws FileNotFoundException, IOException {
644 final Properties p = new Properties();
645 final FileInputStream in = new FileInputStream(authFile);
646 try {
647 p.load(in);
648 } finally {
649 in.close();
650 }
651 return p;
652 }
653
654 private final class ListParser extends DefaultHandler {
655 final List<String> entries = new ArrayList<String>();
656
657 private final String bucket;
658
659 private final String prefix;
660
661 boolean truncated;
662
663 private StringBuilder data;
664
665 ListParser(final String bn, final String p) {
666 bucket = bn;
667 prefix = p;
668 }
669
670 void list() throws IOException {
671 final Map<String, String> args = new TreeMap<String, String>();
672 if (prefix.length() > 0)
673 args.put("prefix", prefix);
674 if (!entries.isEmpty())
675 args.put("marker", prefix + entries.get(entries.size() - 1));
676
677 for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
678 final HttpURLConnection c = open("GET", bucket, "", args);
679 authorize(c);
680 switch (HttpSupport.response(c)) {
681 case HttpURLConnection.HTTP_OK:
682 truncated = false;
683 data = null;
684
685 final XMLReader xr;
686 try {
687 xr = XMLReaderFactory.createXMLReader();
688 } catch (SAXException e) {
689 throw new IOException(JGitText.get().noXMLParserAvailable);
690 }
691 xr.setContentHandler(this);
692 final InputStream in = c.getInputStream();
693 try {
694 xr.parse(new InputSource(in));
695 } catch (SAXException parsingError) {
696 final IOException p;
697 p = new IOException(MessageFormat.format(JGitText.get().errorListing, prefix));
698 p.initCause(parsingError);
699 throw p;
700 } finally {
701 in.close();
702 }
703 return;
704
705 case HttpURLConnection.HTTP_INTERNAL_ERROR:
706 continue;
707
708 default:
709 throw AmazonS3.this.error("Listing", prefix, c);
710 }
711 }
712 throw maxAttempts("Listing", prefix);
713 }
714
715 @Override
716 public void startElement(final String uri, final String name,
717 final String qName, final Attributes attributes)
718 throws SAXException {
719 if ("Key".equals(name) || "IsTruncated".equals(name))
720 data = new StringBuilder();
721 }
722
723 @Override
724 public void ignorableWhitespace(final char[] ch, final int s,
725 final int n) throws SAXException {
726 if (data != null)
727 data.append(ch, s, n);
728 }
729
730 @Override
731 public void characters(final char[] ch, final int s, final int n)
732 throws SAXException {
733 if (data != null)
734 data.append(ch, s, n);
735 }
736
737 @Override
738 public void endElement(final String uri, final String name,
739 final String qName) throws SAXException {
740 if ("Key".equals(name))
741 entries.add(data.toString().substring(prefix.length()));
742 else if ("IsTruncated".equals(name))
743 truncated = StringUtils.equalsIgnoreCase("true", data.toString());
744 data = null;
745 }
746 }
747 }