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

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