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 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   * A simple HTTP REST client for the Amazon S3 service.
99   * <p>
100  * This client uses the REST API to communicate with the Amazon S3 servers and
101  * read or write content through a bucket that the user has access to. It is a
102  * very lightweight implementation of the S3 API and therefore does not have all
103  * of the bells and whistles of popular client implementations.
104  * <p>
105  * Authentication is always performed using the user's AWSAccessKeyId and their
106  * private AWSSecretAccessKey.
107  * <p>
108  * Optional client-side encryption may be enabled if requested. The format is
109  * compatible with <a href="http://jets3t.s3.amazonaws.com/index.html">jets3t</a>,
110  * a popular Java based Amazon S3 client library. Enabling encryption can hide
111  * sensitive data from the operators of the S3 service.
112  */
113 public class AmazonS3 {
114 	private static final Set<String> SIGNED_HEADERS;
115 
116 	private static final String HMAC = "HmacSHA1"; //$NON-NLS-1$
117 
118 	private static final String X_AMZ_ACL = "x-amz-acl"; //$NON-NLS-1$
119 
120 	private static final String X_AMZ_META = "x-amz-meta-"; //$NON-NLS-1$
121 
122 	static {
123 		SIGNED_HEADERS = new HashSet<String>();
124 		SIGNED_HEADERS.add("content-type"); //$NON-NLS-1$
125 		SIGNED_HEADERS.add("content-md5"); //$NON-NLS-1$
126 		SIGNED_HEADERS.add("date"); //$NON-NLS-1$
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-"); //$NON-NLS-1$
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()); //$NON-NLS-1$ //$NON-NLS-2$
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 : ""; //$NON-NLS-1$
147 	}
148 
149 	private static String httpNow() {
150 		final String tz = "GMT"; //$NON-NLS-1$
151 		final SimpleDateFormat fmt;
152 		fmt = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.US); //$NON-NLS-1$
153 		fmt.setTimeZone(TimeZone.getTimeZone(tz));
154 		return fmt.format(new Date()) + " " + tz; //$NON-NLS-1$
155 	}
156 
157 	private static MessageDigest newMD5() {
158 		try {
159 			return MessageDigest.getInstance("MD5"); //$NON-NLS-1$
160 		} catch (NoSuchAlgorithmException e) {
161 			throw new RuntimeException(JGitText.get().JRELacksMD5Implementation, e);
162 		}
163 	}
164 
165 	/** AWSAccessKeyId, public string that identifies the user's account. */
166 	private final String publicKey;
167 
168 	/** Decoded form of the private AWSSecretAccessKey, to sign requests. */
169 	private final SecretKeySpec privateKey;
170 
171 	/** Our HTTP proxy support, in case we are behind a firewall. */
172 	private final ProxySelector proxySelector;
173 
174 	/** ACL to apply to created objects. */
175 	private final String acl;
176 
177 	/** Maximum number of times to try an operation. */
178 	final int maxAttempts;
179 
180 	/** Encryption algorithm, may be a null instance that provides pass-through. */
181 	private final WalkEncryption encryption;
182 
183 	/** Directory for locally buffered content. */
184 	private final File tmpDir;
185 
186 	/** S3 Bucket Domain. */
187 	private final String domain;
188 
189 	/** Property names used in amazon connection configuration file. */
190 	interface Keys {
191 		String ACCESS_KEY = "accesskey"; //$NON-NLS-1$
192 		String SECRET_KEY = "secretkey"; //$NON-NLS-1$
193 		String PASSWORD = "password"; //$NON-NLS-1$
194 		String CRYPTO_ALG = "crypto.algorithm"; //$NON-NLS-1$
195 		String CRYPTO_VER = "crypto.version"; //$NON-NLS-1$
196 		String ACL = "acl"; //$NON-NLS-1$
197 		String DOMAIN = "domain"; //$NON-NLS-1$
198 		String HTTP_RETRY = "httpclient.retry-max"; //$NON-NLS-1$
199 		String TMP_DIR = "tmpdir"; //$NON-NLS-1$
200 	}
201 
202 	/**
203 	 * Create a new S3 client for the supplied user information.
204 	 * <p>
205 	 * The connection properties are a subset of those supported by the popular
206 	 * <a href="http://jets3t.s3.amazonaws.com/index.html">jets3t</a> library.
207 	 * For example:
208 	 *
209 	 * <pre>
210 	 * # AWS Access and Secret Keys (required)
211 	 * accesskey: &lt;YourAWSAccessKey&gt;
212 	 * secretkey: &lt;YourAWSSecretKey&gt;
213 	 *
214 	 * # Access Control List setting to apply to uploads, must be one of:
215 	 * # PRIVATE, PUBLIC_READ (defaults to PRIVATE).
216 	 * acl: PRIVATE
217 	 *
218 	 * # S3 Domain
219 	 * # AWS S3 Region Domain (defaults to s3.amazonaws.com)
220 	 * domain: s3.amazonaws.com
221 	 *
222 	 * # Number of times to retry after internal error from S3.
223 	 * httpclient.retry-max: 3
224 	 *
225 	 * # End-to-end encryption (hides content from S3 owners)
226 	 * password: &lt;encryption pass-phrase&gt;
227 	 * crypto.algorithm: PBEWithMD5AndDES
228 	 * </pre>
229 	 *
230 	 * @param props
231 	 *            connection properties.
232 	 *
233 	 */
234 	public AmazonS3(final Properties props) {
235 		domain = props.getProperty(Keys.DOMAIN, "s3.amazonaws.com"); //$NON-NLS-1$
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"); //$NON-NLS-1$
247 		if (StringUtils.equalsIgnoreCase("PRIVATE", pacl)) //$NON-NLS-1$
248 			acl = "private"; //$NON-NLS-1$
249 		else if (StringUtils.equalsIgnoreCase("PUBLIC", pacl)) //$NON-NLS-1$
250 			acl = "public-read"; //$NON-NLS-1$
251 		else if (StringUtils.equalsIgnoreCase("PUBLIC-READ", pacl)) //$NON-NLS-1$
252 			acl = "public-read"; //$NON-NLS-1$
253 		else if (StringUtils.equalsIgnoreCase("PUBLIC_READ", pacl)) //$NON-NLS-1$
254 			acl = "public-read"; //$NON-NLS-1$
255 		else
256 			throw new IllegalArgumentException("Invalid acl: " + pacl); //$NON-NLS-1$
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")); //$NON-NLS-1$
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 	 * Get the content of a bucket object.
274 	 *
275 	 * @param bucket
276 	 *            name of the bucket storing the object.
277 	 * @param key
278 	 *            key of the object within its bucket.
279 	 * @return connection to stream the content of the object. The request
280 	 *         properties of the connection may not be modified by the caller as
281 	 *         the request parameters have already been signed.
282 	 * @throws IOException
283 	 *             sending the request was not possible.
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); //$NON-NLS-1$
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 	 * Decrypt an input stream from {@link #get(String, String)}.
307 	 *
308 	 * @param u
309 	 *            connection previously created by {@link #get(String, String)}}.
310 	 * @return stream to read plain text from.
311 	 * @throws IOException
312 	 *             decryption could not be configured.
313 	 */
314 	public InputStream decrypt(final URLConnection u) throws IOException {
315 		return encryption.decrypt(u.getInputStream());
316 	}
317 
318 	/**
319 	 * List the names of keys available within a bucket.
320 	 * <p>
321 	 * This method is primarily meant for obtaining a "recursive directory
322 	 * listing" rooted under the specified bucket and prefix location.
323 	 *
324 	 * @param bucket
325 	 *            name of the bucket whose objects should be listed.
326 	 * @param prefix
327 	 *            common prefix to filter the results by. Must not be null.
328 	 *            Supplying the empty string will list all keys in the bucket.
329 	 *            Supplying a non-empty string will act as though a trailing '/'
330 	 *            appears in prefix, even if it does not.
331 	 * @return list of keys starting with <code>prefix</code>, after removing
332 	 *         <code>prefix</code> (or <code>prefix + "/"</code>)from all
333 	 *         of them.
334 	 * @throws IOException
335 	 *             sending the request was not possible, or the response XML
336 	 *             document could not be parsed properly.
337 	 */
338 	public List<String> list(final String bucket, String prefix)
339 			throws IOException {
340 		if (prefix.length() > 0 && !prefix.endsWith("/")) //$NON-NLS-1$
341 			prefix += "/"; //$NON-NLS-1$
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 	 * Delete a single object.
351 	 * <p>
352 	 * Deletion always succeeds, even if the object does not exist.
353 	 *
354 	 * @param bucket
355 	 *            name of the bucket storing the object.
356 	 * @param key
357 	 *            key of the object within its bucket.
358 	 * @throws IOException
359 	 *             deletion failed due to communications error.
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); //$NON-NLS-1$
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 	 * Atomically create or replace a single small object.
380 	 * <p>
381 	 * This form is only suitable for smaller contents, where the caller can
382 	 * reasonable fit the entire thing into memory.
383 	 * <p>
384 	 * End-to-end data integrity is assured by internally computing the MD5
385 	 * checksum of the supplied data and transmitting the checksum along with
386 	 * the data itself.
387 	 *
388 	 * @param bucket
389 	 *            name of the bucket storing the object.
390 	 * @param key
391 	 *            key of the object within its bucket.
392 	 * @param data
393 	 *            new data content for the object. Must not be null. Zero length
394 	 *            array will create a zero length object.
395 	 * @throws IOException
396 	 *             creation/updating failed due to communications error.
397 	 */
398 	public void put(final String bucket, final String key, final byte[] data)
399 			throws IOException {
400 		if (encryption != WalkEncryption.NONE) {
401 			// We have to copy to produce the cipher text anyway so use
402 			// the large object code path as it supports that behavior.
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); //$NON-NLS-1$
414 			c.setRequestProperty("Content-Length", lenstr); //$NON-NLS-1$
415 			c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$
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 	 * Atomically create or replace a single large object.
441 	 * <p>
442 	 * Initially the returned output stream buffers data into memory, but if the
443 	 * total number of written bytes starts to exceed an internal limit the data
444 	 * is spooled to a temporary file on the local drive.
445 	 * <p>
446 	 * Network transmission is attempted only when <code>close()</code> gets
447 	 * called at the end of output. Closing the returned stream can therefore
448 	 * take significant time, especially if the written content is very large.
449 	 * <p>
450 	 * End-to-end data integrity is assured by internally computing the MD5
451 	 * checksum of the supplied data and transmitting the checksum along with
452 	 * the data itself.
453 	 *
454 	 * @param bucket
455 	 *            name of the bucket storing the object.
456 	 * @param key
457 	 *            key of the object within its bucket.
458 	 * @param monitor
459 	 *            (optional) progress monitor to post upload completion to
460 	 *            during the stream's close method.
461 	 * @param monitorTask
462 	 *            (optional) task name to display during the close method.
463 	 * @return a stream which accepts the new data, and transmits once closed.
464 	 * @throws IOException
465 	 *             if encryption was enabled it could not be configured.
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); //$NON-NLS-1$
498 			c.setFixedLengthStreamingMode(len);
499 			c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$
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))); //$NON-NLS-1$
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"); //$NON-NLS-1$ //$NON-NLS-2$
603 		c.setRequestProperty("Date", httpNow()); //$NON-NLS-1$
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")); //$NON-NLS-1$
621 		s.append('\n');
622 
623 		s.append(remove(sigHdr, "content-type")); //$NON-NLS-1$
624 		s.append('\n');
625 
626 		s.append(remove(sigHdr, "date")); //$NON-NLS-1$
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"))); //$NON-NLS-1$
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); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
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); //$NON-NLS-1$
686 			if (!entries.isEmpty())
687 				args.put("marker", prefix + entries.get(entries.size() - 1)); //$NON-NLS-1$
688 
689 			for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
690 				final HttpURLConnection c = open("GET", bucket, "", args); //$NON-NLS-1$ //$NON-NLS-2$
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); //$NON-NLS-1$
722 				}
723 			}
724 			throw maxAttempts("Listing", prefix); //$NON-NLS-1$
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)) //$NON-NLS-1$ //$NON-NLS-2$
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)) //$NON-NLS-1$
753 				entries.add(data.toString().substring(prefix.length()));
754 			else if ("IsTruncated".equals(name)) //$NON-NLS-1$
755 				truncated = StringUtils.equalsIgnoreCase("true", data.toString()); //$NON-NLS-1$
756 			data = null;
757 		}
758 	}
759 }