View Javadoc
1   /*
2    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.transport;
45  
46  import static org.eclipse.jgit.lib.Constants.CHARSET;
47  
48  import java.io.ByteArrayOutputStream;
49  import java.io.File;
50  import java.io.FileInputStream;
51  import java.io.FileNotFoundException;
52  import java.io.IOException;
53  import java.io.InputStream;
54  import java.io.OutputStream;
55  import java.net.HttpURLConnection;
56  import java.net.Proxy;
57  import java.net.ProxySelector;
58  import java.net.URL;
59  import java.net.URLConnection;
60  import java.security.DigestOutputStream;
61  import java.security.GeneralSecurityException;
62  import java.security.InvalidKeyException;
63  import java.security.MessageDigest;
64  import java.security.NoSuchAlgorithmException;
65  import java.text.MessageFormat;
66  import java.text.SimpleDateFormat;
67  import java.util.ArrayList;
68  import java.util.Collections;
69  import java.util.Date;
70  import java.util.HashSet;
71  import java.util.Iterator;
72  import java.util.List;
73  import java.util.Locale;
74  import java.util.Map;
75  import java.util.Properties;
76  import java.util.Set;
77  import java.util.SortedMap;
78  import java.util.TimeZone;
79  import java.util.TreeMap;
80  
81  import javax.crypto.Mac;
82  import javax.crypto.spec.SecretKeySpec;
83  
84  import org.eclipse.jgit.internal.JGitText;
85  import org.eclipse.jgit.lib.Constants;
86  import org.eclipse.jgit.lib.NullProgressMonitor;
87  import org.eclipse.jgit.lib.ProgressMonitor;
88  import org.eclipse.jgit.util.Base64;
89  import org.eclipse.jgit.util.HttpSupport;
90  import org.eclipse.jgit.util.StringUtils;
91  import org.eclipse.jgit.util.TemporaryBuffer;
92  import org.xml.sax.Attributes;
93  import org.xml.sax.InputSource;
94  import org.xml.sax.SAXException;
95  import org.xml.sax.XMLReader;
96  import org.xml.sax.helpers.DefaultHandler;
97  import org.xml.sax.helpers.XMLReaderFactory;
98  
99  /**
100  * A simple HTTP REST client for the Amazon S3 service.
101  * <p>
102  * This client uses the REST API to communicate with the Amazon S3 servers and
103  * read or write content through a bucket that the user has access to. It is a
104  * very lightweight implementation of the S3 API and therefore does not have all
105  * of the bells and whistles of popular client implementations.
106  * <p>
107  * Authentication is always performed using the user's AWSAccessKeyId and their
108  * private AWSSecretAccessKey.
109  * <p>
110  * Optional client-side encryption may be enabled if requested. The format is
111  * compatible with <a href="http://jets3t.s3.amazonaws.com/index.html">jets3t</a>,
112  * a popular Java based Amazon S3 client library. Enabling encryption can hide
113  * sensitive data from the operators of the S3 service.
114  */
115 public class AmazonS3 {
116 	private static final Set<String> SIGNED_HEADERS;
117 
118 	private static final String HMAC = "HmacSHA1"; //$NON-NLS-1$
119 
120 	private static final String X_AMZ_ACL = "x-amz-acl"; //$NON-NLS-1$
121 
122 	private static final String X_AMZ_META = "x-amz-meta-"; //$NON-NLS-1$
123 
124 	static {
125 		SIGNED_HEADERS = new HashSet<>();
126 		SIGNED_HEADERS.add("content-type"); //$NON-NLS-1$
127 		SIGNED_HEADERS.add("content-md5"); //$NON-NLS-1$
128 		SIGNED_HEADERS.add("date"); //$NON-NLS-1$
129 	}
130 
131 	private static boolean isSignedHeader(String name) {
132 		final String nameLC = StringUtils.toLowerCase(name);
133 		return SIGNED_HEADERS.contains(nameLC) || nameLC.startsWith("x-amz-"); //$NON-NLS-1$
134 	}
135 
136 	private static String toCleanString(List<String> list) {
137 		final StringBuilder s = new StringBuilder();
138 		for (String v : list) {
139 			if (s.length() > 0)
140 				s.append(',');
141 			s.append(v.replaceAll("\n", "").trim()); //$NON-NLS-1$ //$NON-NLS-2$
142 		}
143 		return s.toString();
144 	}
145 
146 	private static String remove(Map<String, String> m, String k) {
147 		final String r = m.remove(k);
148 		return r != null ? r : ""; //$NON-NLS-1$
149 	}
150 
151 	private static String httpNow() {
152 		final String tz = "GMT"; //$NON-NLS-1$
153 		final SimpleDateFormat fmt;
154 		fmt = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.US); //$NON-NLS-1$
155 		fmt.setTimeZone(TimeZone.getTimeZone(tz));
156 		return fmt.format(new Date()) + " " + tz; //$NON-NLS-1$
157 	}
158 
159 	private static MessageDigest newMD5() {
160 		try {
161 			return MessageDigest.getInstance("MD5"); //$NON-NLS-1$
162 		} catch (NoSuchAlgorithmException e) {
163 			throw new RuntimeException(JGitText.get().JRELacksMD5Implementation, e);
164 		}
165 	}
166 
167 	/** AWSAccessKeyId, public string that identifies the user's account. */
168 	private final String publicKey;
169 
170 	/** Decoded form of the private AWSSecretAccessKey, to sign requests. */
171 	private final SecretKeySpec privateKey;
172 
173 	/** Our HTTP proxy support, in case we are behind a firewall. */
174 	private final ProxySelector proxySelector;
175 
176 	/** ACL to apply to created objects. */
177 	private final String acl;
178 
179 	/** Maximum number of times to try an operation. */
180 	final int maxAttempts;
181 
182 	/** Encryption algorithm, may be a null instance that provides pass-through. */
183 	private final WalkEncryption encryption;
184 
185 	/** Directory for locally buffered content. */
186 	private final File tmpDir;
187 
188 	/** S3 Bucket Domain. */
189 	private final String domain;
190 
191 	/** Property names used in amazon connection configuration file. */
192 	interface Keys {
193 		String ACCESS_KEY = "accesskey"; //$NON-NLS-1$
194 		String SECRET_KEY = "secretkey"; //$NON-NLS-1$
195 		String PASSWORD = "password"; //$NON-NLS-1$
196 		String CRYPTO_ALG = "crypto.algorithm"; //$NON-NLS-1$
197 		String CRYPTO_VER = "crypto.version"; //$NON-NLS-1$
198 		String ACL = "acl"; //$NON-NLS-1$
199 		String DOMAIN = "domain"; //$NON-NLS-1$
200 		String HTTP_RETRY = "httpclient.retry-max"; //$NON-NLS-1$
201 		String TMP_DIR = "tmpdir"; //$NON-NLS-1$
202 	}
203 
204 	/**
205 	 * Create a new S3 client for the supplied user information.
206 	 * <p>
207 	 * The connection properties are a subset of those supported by the popular
208 	 * <a href="http://jets3t.s3.amazonaws.com/index.html">jets3t</a> library.
209 	 * For example:
210 	 *
211 	 * <pre>
212 	 * # AWS Access and Secret Keys (required)
213 	 * accesskey: &lt;YourAWSAccessKey&gt;
214 	 * secretkey: &lt;YourAWSSecretKey&gt;
215 	 *
216 	 * # Access Control List setting to apply to uploads, must be one of:
217 	 * # PRIVATE, PUBLIC_READ (defaults to PRIVATE).
218 	 * acl: PRIVATE
219 	 *
220 	 * # S3 Domain
221 	 * # AWS S3 Region Domain (defaults to s3.amazonaws.com)
222 	 * domain: s3.amazonaws.com
223 	 *
224 	 * # Number of times to retry after internal error from S3.
225 	 * httpclient.retry-max: 3
226 	 *
227 	 * # End-to-end encryption (hides content from S3 owners)
228 	 * password: &lt;encryption pass-phrase&gt;
229 	 * crypto.algorithm: PBEWithMD5AndDES
230 	 * </pre>
231 	 *
232 	 * @param props
233 	 *            connection properties.
234 	 */
235 	public AmazonS3(final Properties props) {
236 		domain = props.getProperty(Keys.DOMAIN, "s3.amazonaws.com"); //$NON-NLS-1$
237 
238 		publicKey = props.getProperty(Keys.ACCESS_KEY);
239 		if (publicKey == null)
240 			throw new IllegalArgumentException(JGitText.get().missingAccesskey);
241 
242 		final String secret = props.getProperty(Keys.SECRET_KEY);
243 		if (secret == null)
244 			throw new IllegalArgumentException(JGitText.get().missingSecretkey);
245 		privateKey = new SecretKeySpec(Constants.encodeASCII(secret), HMAC);
246 
247 		final String pacl = props.getProperty(Keys.ACL, "PRIVATE"); //$NON-NLS-1$
248 		if (StringUtils.equalsIgnoreCase("PRIVATE", pacl)) //$NON-NLS-1$
249 			acl = "private"; //$NON-NLS-1$
250 		else if (StringUtils.equalsIgnoreCase("PUBLIC", pacl)) //$NON-NLS-1$
251 			acl = "public-read"; //$NON-NLS-1$
252 		else if (StringUtils.equalsIgnoreCase("PUBLIC-READ", pacl)) //$NON-NLS-1$
253 			acl = "public-read"; //$NON-NLS-1$
254 		else if (StringUtils.equalsIgnoreCase("PUBLIC_READ", pacl)) //$NON-NLS-1$
255 			acl = "public-read"; //$NON-NLS-1$
256 		else
257 			throw new IllegalArgumentException("Invalid acl: " + pacl); //$NON-NLS-1$
258 
259 		try {
260 			encryption = WalkEncryption.instance(props);
261 		} catch (GeneralSecurityException e) {
262 			throw new IllegalArgumentException(JGitText.get().invalidEncryption, e);
263 		}
264 
265 		maxAttempts = Integer
266 				.parseInt(props.getProperty(Keys.HTTP_RETRY, "3")); //$NON-NLS-1$
267 		proxySelector = ProxySelector.getDefault();
268 
269 		String tmp = props.getProperty(Keys.TMP_DIR);
270 		tmpDir = tmp != null && tmp.length() > 0 ? new File(tmp) : null;
271 	}
272 
273 	/**
274 	 * Get the content of a bucket object.
275 	 *
276 	 * @param bucket
277 	 *            name of the bucket storing the object.
278 	 * @param key
279 	 *            key of the object within its bucket.
280 	 * @return connection to stream the content of the object. The request
281 	 *         properties of the connection may not be modified by the caller as
282 	 *         the request parameters have already been signed.
283 	 * @throws java.io.IOException
284 	 *             sending the request was not possible.
285 	 */
286 	public URLConnection get(String bucket, String key)
287 			throws IOException {
288 		for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
289 			final HttpURLConnection c = open("GET", bucket, key); //$NON-NLS-1$
290 			authorize(c);
291 			switch (HttpSupport.response(c)) {
292 			case HttpURLConnection.HTTP_OK:
293 				encryption.validate(c, X_AMZ_META);
294 				return c;
295 			case HttpURLConnection.HTTP_NOT_FOUND:
296 				throw new FileNotFoundException(key);
297 			case HttpURLConnection.HTTP_INTERNAL_ERROR:
298 				continue;
299 			default:
300 				throw error(JGitText.get().s3ActionReading, key, c);
301 			}
302 		}
303 		throw maxAttempts(JGitText.get().s3ActionReading, key);
304 	}
305 
306 	/**
307 	 * Decrypt an input stream from {@link #get(String, String)}.
308 	 *
309 	 * @param u
310 	 *            connection previously created by {@link #get(String, String)}}.
311 	 * @return stream to read plain text from.
312 	 * @throws java.io.IOException
313 	 *             decryption could not be configured.
314 	 */
315 	public InputStream decrypt(URLConnection u) throws IOException {
316 		return encryption.decrypt(u.getInputStream());
317 	}
318 
319 	/**
320 	 * List the names of keys available within a bucket.
321 	 * <p>
322 	 * This method is primarily meant for obtaining a "recursive directory
323 	 * listing" rooted under the specified bucket and prefix location.
324 	 *
325 	 * @param bucket
326 	 *            name of the bucket whose objects should be listed.
327 	 * @param prefix
328 	 *            common prefix to filter the results by. Must not be null.
329 	 *            Supplying the empty string will list all keys in the bucket.
330 	 *            Supplying a non-empty string will act as though a trailing '/'
331 	 *            appears in prefix, even if it does not.
332 	 * @return list of keys starting with <code>prefix</code>, after removing
333 	 *         <code>prefix</code> (or <code>prefix + "/"</code>)from all
334 	 *         of them.
335 	 * @throws java.io.IOException
336 	 *             sending the request was not possible, or the response XML
337 	 *             document could not be parsed properly.
338 	 */
339 	public List<String> list(String bucket, String prefix)
340 			throws IOException {
341 		if (prefix.length() > 0 && !prefix.endsWith("/")) //$NON-NLS-1$
342 			prefix += "/"; //$NON-NLS-1$
343 		final ListParser lp = new ListParser(bucket, prefix);
344 		do {
345 			lp.list();
346 		} while (lp.truncated);
347 		return lp.entries;
348 	}
349 
350 	/**
351 	 * Delete a single object.
352 	 * <p>
353 	 * Deletion always succeeds, even if the object does not exist.
354 	 *
355 	 * @param bucket
356 	 *            name of the bucket storing the object.
357 	 * @param key
358 	 *            key of the object within its bucket.
359 	 * @throws java.io.IOException
360 	 *             deletion failed due to communications error.
361 	 */
362 	public void delete(String bucket, String key)
363 			throws IOException {
364 		for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
365 			final HttpURLConnection c = open("DELETE", bucket, key); //$NON-NLS-1$
366 			authorize(c);
367 			switch (HttpSupport.response(c)) {
368 			case HttpURLConnection.HTTP_NO_CONTENT:
369 				return;
370 			case HttpURLConnection.HTTP_INTERNAL_ERROR:
371 				continue;
372 			default:
373 				throw error(JGitText.get().s3ActionDeletion, key, c);
374 			}
375 		}
376 		throw maxAttempts(JGitText.get().s3ActionDeletion, key);
377 	}
378 
379 	/**
380 	 * Atomically create or replace a single small object.
381 	 * <p>
382 	 * This form is only suitable for smaller contents, where the caller can
383 	 * reasonable fit the entire thing into memory.
384 	 * <p>
385 	 * End-to-end data integrity is assured by internally computing the MD5
386 	 * checksum of the supplied data and transmitting the checksum along with
387 	 * the data itself.
388 	 *
389 	 * @param bucket
390 	 *            name of the bucket storing the object.
391 	 * @param key
392 	 *            key of the object within its bucket.
393 	 * @param data
394 	 *            new data content for the object. Must not be null. Zero length
395 	 *            array will create a zero length object.
396 	 * @throws java.io.IOException
397 	 *             creation/updating failed due to communications error.
398 	 */
399 	public void put(String bucket, String key, byte[] data)
400 			throws IOException {
401 		if (encryption != WalkEncryption.NONE) {
402 			// We have to copy to produce the cipher text anyway so use
403 			// the large object code path as it supports that behavior.
404 			//
405 			try (OutputStream os = beginPut(bucket, key, null, null)) {
406 				os.write(data);
407 			}
408 			return;
409 		}
410 
411 		final String md5str = Base64.encodeBytes(newMD5().digest(data));
412 		final String lenstr = String.valueOf(data.length);
413 		for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
414 			final HttpURLConnection c = open("PUT", bucket, key); //$NON-NLS-1$
415 			c.setRequestProperty("Content-Length", lenstr); //$NON-NLS-1$
416 			c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$
417 			c.setRequestProperty(X_AMZ_ACL, acl);
418 			authorize(c);
419 			c.setDoOutput(true);
420 			c.setFixedLengthStreamingMode(data.length);
421 			try (OutputStream os = c.getOutputStream()) {
422 				os.write(data);
423 			}
424 
425 			switch (HttpSupport.response(c)) {
426 			case HttpURLConnection.HTTP_OK:
427 				return;
428 			case HttpURLConnection.HTTP_INTERNAL_ERROR:
429 				continue;
430 			default:
431 				throw error(JGitText.get().s3ActionWriting, key, c);
432 			}
433 		}
434 		throw maxAttempts(JGitText.get().s3ActionWriting, key);
435 	}
436 
437 	/**
438 	 * Atomically create or replace a single large object.
439 	 * <p>
440 	 * Initially the returned output stream buffers data into memory, but if the
441 	 * total number of written bytes starts to exceed an internal limit the data
442 	 * is spooled to a temporary file on the local drive.
443 	 * <p>
444 	 * Network transmission is attempted only when <code>close()</code> gets
445 	 * called at the end of output. Closing the returned stream can therefore
446 	 * take significant time, especially if the written content is very large.
447 	 * <p>
448 	 * End-to-end data integrity is assured by internally computing the MD5
449 	 * checksum of the supplied data and transmitting the checksum along with
450 	 * the data itself.
451 	 *
452 	 * @param bucket
453 	 *            name of the bucket storing the object.
454 	 * @param key
455 	 *            key of the object within its bucket.
456 	 * @param monitor
457 	 *            (optional) progress monitor to post upload completion to
458 	 *            during the stream's close method.
459 	 * @param monitorTask
460 	 *            (optional) task name to display during the close method.
461 	 * @return a stream which accepts the new data, and transmits once closed.
462 	 * @throws java.io.IOException
463 	 *             if encryption was enabled it could not be configured.
464 	 */
465 	public OutputStream beginPut(final String bucket, final String key,
466 			final ProgressMonitor monitor, final String monitorTask)
467 			throws IOException {
468 		final MessageDigest md5 = newMD5();
469 		final TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(tmpDir) {
470 			@Override
471 			public void close() throws IOException {
472 				super.close();
473 				try {
474 					putImpl(bucket, key, md5.digest(), this, monitor,
475 							monitorTask);
476 				} finally {
477 					destroy();
478 				}
479 			}
480 		};
481 		return encryption.encrypt(new DigestOutputStream(buffer, md5));
482 	}
483 
484 	void putImpl(final String bucket, final String key,
485 			final byte[] csum, final TemporaryBuffer buf,
486 			ProgressMonitor monitor, String monitorTask) throws IOException {
487 		if (monitor == null)
488 			monitor = NullProgressMonitor.INSTANCE;
489 		if (monitorTask == null)
490 			monitorTask = MessageFormat.format(JGitText.get().progressMonUploading, key);
491 
492 		final String md5str = Base64.encodeBytes(csum);
493 		final long len = buf.length();
494 		for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
495 			final HttpURLConnection c = open("PUT", bucket, key); //$NON-NLS-1$
496 			c.setFixedLengthStreamingMode(len);
497 			c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$
498 			c.setRequestProperty(X_AMZ_ACL, acl);
499 			encryption.request(c, X_AMZ_META);
500 			authorize(c);
501 			c.setDoOutput(true);
502 			monitor.beginTask(monitorTask, (int) (len / 1024));
503 			try (OutputStream os = c.getOutputStream()) {
504 				buf.writeTo(os, monitor);
505 			} finally {
506 				monitor.endTask();
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 	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 		if (c.getErrorStream() == null) {
528 			return err;
529 		}
530 
531 		try (InputStream errorStream = c.getErrorStream()) {
532 			final ByteArrayOutputStream b = new ByteArrayOutputStream();
533 			byte[] buf = new byte[2048];
534 			for (;;) {
535 				final int n = errorStream.read(buf);
536 				if (n < 0) {
537 					break;
538 				}
539 				if (n > 0) {
540 					b.write(buf, 0, n);
541 				}
542 			}
543 			buf = b.toByteArray();
544 			if (buf.length > 0) {
545 				err.initCause(new IOException("\n" + new String(buf))); //$NON-NLS-1$
546 			}
547 		}
548 		return err;
549 	}
550 
551 	IOException maxAttempts(String action, String key) {
552 		return new IOException(MessageFormat.format(
553 				JGitText.get().amazonS3ActionFailedGivingUp, action, key,
554 				Integer.valueOf(maxAttempts)));
555 	}
556 
557 	private HttpURLConnection open(final String method, final String bucket,
558 			final String key) throws IOException {
559 		final Map<String, String> noArgs = Collections.emptyMap();
560 		return open(method, bucket, key, noArgs);
561 	}
562 
563 	HttpURLConnection open(final String method, final String bucket,
564 			final String key, final Map<String, String> args)
565 			throws IOException {
566 		final StringBuilder urlstr = new StringBuilder();
567 		urlstr.append("http://"); //$NON-NLS-1$
568 		urlstr.append(bucket);
569 		urlstr.append('.');
570 		urlstr.append(domain);
571 		urlstr.append('/');
572 		if (key.length() > 0)
573 			HttpSupport.encode(urlstr, key);
574 		if (!args.isEmpty()) {
575 			final Iterator<Map.Entry<String, String>> i;
576 
577 			urlstr.append('?');
578 			i = args.entrySet().iterator();
579 			while (i.hasNext()) {
580 				final Map.Entry<String, String> e = i.next();
581 				urlstr.append(e.getKey());
582 				urlstr.append('=');
583 				HttpSupport.encode(urlstr, e.getValue());
584 				if (i.hasNext())
585 					urlstr.append('&');
586 			}
587 		}
588 
589 		final URL url = new URL(urlstr.toString());
590 		final Proxy proxy = HttpSupport.proxyFor(proxySelector, url);
591 		final HttpURLConnection c;
592 
593 		c = (HttpURLConnection) url.openConnection(proxy);
594 		c.setRequestMethod(method);
595 		c.setRequestProperty("User-Agent", "jgit/1.0"); //$NON-NLS-1$ //$NON-NLS-2$
596 		c.setRequestProperty("Date", httpNow()); //$NON-NLS-1$
597 		return c;
598 	}
599 
600 	void authorize(HttpURLConnection c) throws IOException {
601 		final Map<String, List<String>> reqHdr = c.getRequestProperties();
602 		final SortedMap<String, String> sigHdr = new TreeMap<>();
603 		for (Map.Entry<String, List<String>> entry : reqHdr.entrySet()) {
604 			final String hdr = entry.getKey();
605 			if (isSignedHeader(hdr))
606 				sigHdr.put(StringUtils.toLowerCase(hdr), toCleanString(entry.getValue()));
607 		}
608 
609 		final StringBuilder s = new StringBuilder();
610 		s.append(c.getRequestMethod());
611 		s.append('\n');
612 
613 		s.append(remove(sigHdr, "content-md5")); //$NON-NLS-1$
614 		s.append('\n');
615 
616 		s.append(remove(sigHdr, "content-type")); //$NON-NLS-1$
617 		s.append('\n');
618 
619 		s.append(remove(sigHdr, "date")); //$NON-NLS-1$
620 		s.append('\n');
621 
622 		for (Map.Entry<String, String> e : sigHdr.entrySet()) {
623 			s.append(e.getKey());
624 			s.append(':');
625 			s.append(e.getValue());
626 			s.append('\n');
627 		}
628 
629 		final String host = c.getURL().getHost();
630 		s.append('/');
631 		s.append(host.substring(0, host.length() - domain.length() - 1));
632 		s.append(c.getURL().getPath());
633 
634 		final String sec;
635 		try {
636 			final Mac m = Mac.getInstance(HMAC);
637 			m.init(privateKey);
638 			sec = Base64.encodeBytes(m.doFinal(s.toString().getBytes(CHARSET)));
639 		} catch (NoSuchAlgorithmException e) {
640 			throw new IOException(MessageFormat.format(JGitText.get().noHMACsupport, HMAC, e.getMessage()));
641 		} catch (InvalidKeyException e) {
642 			throw new IOException(MessageFormat.format(JGitText.get().invalidKey, e.getMessage()));
643 		}
644 		c.setRequestProperty("Authorization", "AWS " + publicKey + ":" + sec); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
645 	}
646 
647 	static Properties properties(File authFile)
648 			throws FileNotFoundException, IOException {
649 		final Properties p = new Properties();
650 		try (FileInputStream in = new FileInputStream(authFile)) {
651 			p.load(in);
652 		}
653 		return p;
654 	}
655 
656 	private final class ListParser extends DefaultHandler {
657 		final List<String> entries = new ArrayList<>();
658 
659 		private final String bucket;
660 
661 		private final String prefix;
662 
663 		boolean truncated;
664 
665 		private StringBuilder data;
666 
667 		ListParser(String bn, String p) {
668 			bucket = bn;
669 			prefix = p;
670 		}
671 
672 		void list() throws IOException {
673 			final Map<String, String> args = new TreeMap<>();
674 			if (prefix.length() > 0)
675 				args.put("prefix", prefix); //$NON-NLS-1$
676 			if (!entries.isEmpty())
677 				args.put("marker", prefix + entries.get(entries.size() - 1)); //$NON-NLS-1$
678 
679 			for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
680 				final HttpURLConnection c = open("GET", bucket, "", args); //$NON-NLS-1$ //$NON-NLS-2$
681 				authorize(c);
682 				switch (HttpSupport.response(c)) {
683 				case HttpURLConnection.HTTP_OK:
684 					truncated = false;
685 					data = null;
686 
687 					final XMLReader xr;
688 					try {
689 						xr = XMLReaderFactory.createXMLReader();
690 					} catch (SAXException e) {
691 						throw new IOException(JGitText.get().noXMLParserAvailable);
692 					}
693 					xr.setContentHandler(this);
694 					try (InputStream in = c.getInputStream()) {
695 						xr.parse(new InputSource(in));
696 					} catch (SAXException parsingError) {
697 						throw new IOException(
698 								MessageFormat.format(
699 										JGitText.get().errorListing, prefix),
700 								parsingError);
701 					}
702 					return;
703 
704 				case HttpURLConnection.HTTP_INTERNAL_ERROR:
705 					continue;
706 
707 				default:
708 					throw AmazonS3.this.error("Listing", prefix, c); //$NON-NLS-1$
709 				}
710 			}
711 			throw maxAttempts("Listing", prefix); //$NON-NLS-1$
712 		}
713 
714 		@Override
715 		public void startElement(final String uri, final String name,
716 				final String qName, final Attributes attributes)
717 				throws SAXException {
718 			if ("Key".equals(name) || "IsTruncated".equals(name)) //$NON-NLS-1$ //$NON-NLS-2$
719 				data = new StringBuilder();
720 		}
721 
722 		@Override
723 		public void ignorableWhitespace(final char[] ch, final int s,
724 				final int n) throws SAXException {
725 			if (data != null)
726 				data.append(ch, s, n);
727 		}
728 
729 		@Override
730 		public void characters(char[] ch, int s, int n)
731 				throws SAXException {
732 			if (data != null)
733 				data.append(ch, s, n);
734 		}
735 
736 		@Override
737 		public void endElement(final String uri, final String name,
738 				final String qName) throws SAXException {
739 			if ("Key".equals(name)) //$NON-NLS-1$
740 				entries.add(data.toString().substring(prefix.length()));
741 			else if ("IsTruncated".equals(name)) //$NON-NLS-1$
742 				truncated = StringUtils.equalsIgnoreCase("true", data.toString()); //$NON-NLS-1$
743 			data = null;
744 		}
745 	}
746 }