You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

AmazonS3.java 25KB

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