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.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   * 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 	private 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 	/**
190 	 * Create a new S3 client for the supplied user information.
191 	 * <p>
192 	 * The connection properties are a subset of those supported by the popular
193 	 * <a href="http://jets3t.s3.amazonaws.com/index.html">jets3t</a> library.
194 	 * For example:
195 	 *
196 	 * <pre>
197 	 * # AWS Access and Secret Keys (required)
198 	 * accesskey: &lt;YourAWSAccessKey&gt;
199 	 * secretkey: &lt;YourAWSSecretKey&gt;
200 	 *
201 	 * # Access Control List setting to apply to uploads, must be one of:
202 	 * # PRIVATE, PUBLIC_READ (defaults to PRIVATE).
203 	 * acl: PRIVATE
204 	 *
205 	 * # S3 Domain
206 	 * # AWS S3 Region Domain (defaults to s3.amazonaws.com)
207 	 * domain: s3.amazonaws.com
208 	 *
209 	 * # Number of times to retry after internal error from S3.
210 	 * httpclient.retry-max: 3
211 	 *
212 	 * # End-to-end encryption (hides content from S3 owners)
213 	 * password: &lt;encryption pass-phrase&gt;
214 	 * crypto.algorithm: PBEWithMD5AndDES
215 	 * </pre>
216 	 *
217 	 * @param props
218 	 *            connection properties.
219 	 *
220 	 */
221 	public AmazonS3(final Properties props) {
222 		domain = props.getProperty("domain", "s3.amazonaws.com"); //$NON-NLS-1$ //$NON-NLS-2$
223 		publicKey = props.getProperty("accesskey"); //$NON-NLS-1$
224 		if (publicKey == null)
225 			throw new IllegalArgumentException(JGitText.get().missingAccesskey);
226 
227 		final String secret = props.getProperty("secretkey"); //$NON-NLS-1$
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"); //$NON-NLS-1$ //$NON-NLS-2$
233 		if (StringUtils.equalsIgnoreCase("PRIVATE", pacl)) //$NON-NLS-1$
234 			acl = "private"; //$NON-NLS-1$
235 		else if (StringUtils.equalsIgnoreCase("PUBLIC", pacl)) //$NON-NLS-1$
236 			acl = "public-read"; //$NON-NLS-1$
237 		else if (StringUtils.equalsIgnoreCase("PUBLIC-READ", pacl)) //$NON-NLS-1$
238 			acl = "public-read"; //$NON-NLS-1$
239 		else if (StringUtils.equalsIgnoreCase("PUBLIC_READ", pacl)) //$NON-NLS-1$
240 			acl = "public-read"; //$NON-NLS-1$
241 		else
242 			throw new IllegalArgumentException("Invalid acl: " + pacl); //$NON-NLS-1$
243 
244 		try {
245 			final String cPas = props.getProperty("password"); //$NON-NLS-1$
246 			if (cPas != null) {
247 				String cAlg = props.getProperty("crypto.algorithm"); //$NON-NLS-1$
248 				if (cAlg == null)
249 					cAlg = "PBEWithMD5AndDES"; //$NON-NLS-1$
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")); //$NON-NLS-1$ //$NON-NLS-2$
262 		proxySelector = ProxySelector.getDefault();
263 
264 		String tmp = props.getProperty("tmpdir"); //$NON-NLS-1$
265 		tmpDir = tmp != null && tmp.length() > 0 ? new File(tmp) : null;
266 	}
267 
268 	/**
269 	 * Get the content of a bucket object.
270 	 *
271 	 * @param bucket
272 	 *            name of the bucket storing the object.
273 	 * @param key
274 	 *            key of the object within its bucket.
275 	 * @return connection to stream the content of the object. The request
276 	 *         properties of the connection may not be modified by the caller as
277 	 *         the request parameters have already been signed.
278 	 * @throws IOException
279 	 *             sending the request was not possible.
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); //$NON-NLS-1$
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 	 * Decrypt an input stream from {@link #get(String, String)}.
303 	 *
304 	 * @param u
305 	 *            connection previously created by {@link #get(String, String)}}.
306 	 * @return stream to read plain text from.
307 	 * @throws IOException
308 	 *             decryption could not be configured.
309 	 */
310 	public InputStream decrypt(final URLConnection u) throws IOException {
311 		return encryption.decrypt(u.getInputStream());
312 	}
313 
314 	/**
315 	 * List the names of keys available within a bucket.
316 	 * <p>
317 	 * This method is primarily meant for obtaining a "recursive directory
318 	 * listing" rooted under the specified bucket and prefix location.
319 	 *
320 	 * @param bucket
321 	 *            name of the bucket whose objects should be listed.
322 	 * @param prefix
323 	 *            common prefix to filter the results by. Must not be null.
324 	 *            Supplying the empty string will list all keys in the bucket.
325 	 *            Supplying a non-empty string will act as though a trailing '/'
326 	 *            appears in prefix, even if it does not.
327 	 * @return list of keys starting with <code>prefix</code>, after removing
328 	 *         <code>prefix</code> (or <code>prefix + "/"</code>)from all
329 	 *         of them.
330 	 * @throws IOException
331 	 *             sending the request was not possible, or the response XML
332 	 *             document could not be parsed properly.
333 	 */
334 	public List<String> list(final String bucket, String prefix)
335 			throws IOException {
336 		if (prefix.length() > 0 && !prefix.endsWith("/")) //$NON-NLS-1$
337 			prefix += "/"; //$NON-NLS-1$
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 	 * Delete a single object.
347 	 * <p>
348 	 * Deletion always succeeds, even if the object does not exist.
349 	 *
350 	 * @param bucket
351 	 *            name of the bucket storing the object.
352 	 * @param key
353 	 *            key of the object within its bucket.
354 	 * @throws IOException
355 	 *             deletion failed due to communications error.
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); //$NON-NLS-1$
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 	 * Atomically create or replace a single small object.
376 	 * <p>
377 	 * This form is only suitable for smaller contents, where the caller can
378 	 * reasonable fit the entire thing into memory.
379 	 * <p>
380 	 * End-to-end data integrity is assured by internally computing the MD5
381 	 * checksum of the supplied data and transmitting the checksum along with
382 	 * the data itself.
383 	 *
384 	 * @param bucket
385 	 *            name of the bucket storing the object.
386 	 * @param key
387 	 *            key of the object within its bucket.
388 	 * @param data
389 	 *            new data content for the object. Must not be null. Zero length
390 	 *            array will create a zero length object.
391 	 * @throws IOException
392 	 *             creation/updating failed due to communications error.
393 	 */
394 	public void put(final String bucket, final String key, final byte[] data)
395 			throws IOException {
396 		if (encryption != WalkEncryption.NONE) {
397 			// We have to copy to produce the cipher text anyway so use
398 			// the large object code path as it supports that behavior.
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); //$NON-NLS-1$
410 			c.setRequestProperty("Content-Length", lenstr); //$NON-NLS-1$
411 			c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$
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 	 * Atomically create or replace a single large object.
437 	 * <p>
438 	 * Initially the returned output stream buffers data into memory, but if the
439 	 * total number of written bytes starts to exceed an internal limit the data
440 	 * is spooled to a temporary file on the local drive.
441 	 * <p>
442 	 * Network transmission is attempted only when <code>close()</code> gets
443 	 * called at the end of output. Closing the returned stream can therefore
444 	 * take significant time, especially if the written content is very large.
445 	 * <p>
446 	 * End-to-end data integrity is assured by internally computing the MD5
447 	 * checksum of the supplied data and transmitting the checksum along with
448 	 * the data itself.
449 	 *
450 	 * @param bucket
451 	 *            name of the bucket storing the object.
452 	 * @param key
453 	 *            key of the object within its bucket.
454 	 * @param monitor
455 	 *            (optional) progress monitor to post upload completion to
456 	 *            during the stream's close method.
457 	 * @param monitorTask
458 	 *            (optional) task name to display during the close method.
459 	 * @return a stream which accepts the new data, and transmits once closed.
460 	 * @throws IOException
461 	 *             if encryption was enabled it could not be configured.
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); //$NON-NLS-1$
494 			c.setFixedLengthStreamingMode(len);
495 			c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$
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))); //$NON-NLS-1$
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"); //$NON-NLS-1$ //$NON-NLS-2$
591 		c.setRequestProperty("Date", httpNow()); //$NON-NLS-1$
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")); //$NON-NLS-1$
609 		s.append('\n');
610 
611 		s.append(remove(sigHdr, "content-type")); //$NON-NLS-1$
612 		s.append('\n');
613 
614 		s.append(remove(sigHdr, "date")); //$NON-NLS-1$
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"))); //$NON-NLS-1$
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); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
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); //$NON-NLS-1$
674 			if (!entries.isEmpty())
675 				args.put("marker", prefix + entries.get(entries.size() - 1)); //$NON-NLS-1$
676 
677 			for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
678 				final HttpURLConnection c = open("GET", bucket, "", args); //$NON-NLS-1$ //$NON-NLS-2$
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); //$NON-NLS-1$
710 				}
711 			}
712 			throw maxAttempts("Listing", prefix); //$NON-NLS-1$
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)) //$NON-NLS-1$ //$NON-NLS-2$
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)) //$NON-NLS-1$
741 				entries.add(data.toString().substring(prefix.length()));
742 			else if ("IsTruncated".equals(name)) //$NON-NLS-1$
743 				truncated = StringUtils.equalsIgnoreCase("true", data.toString()); //$NON-NLS-1$
744 			data = null;
745 		}
746 	}
747 }