AmazonS3.java

  1. /*
  2.  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */

  10. package org.eclipse.jgit.transport;

  11. import static java.nio.charset.StandardCharsets.UTF_8;

  12. import java.io.ByteArrayOutputStream;
  13. import java.io.File;
  14. import java.io.FileInputStream;
  15. import java.io.FileNotFoundException;
  16. import java.io.IOException;
  17. import java.io.InputStream;
  18. import java.io.OutputStream;
  19. import java.net.HttpURLConnection;
  20. import java.net.Proxy;
  21. import java.net.ProxySelector;
  22. import java.net.URL;
  23. import java.net.URLConnection;
  24. import java.security.DigestOutputStream;
  25. import java.security.GeneralSecurityException;
  26. import java.security.InvalidKeyException;
  27. import java.security.MessageDigest;
  28. import java.security.NoSuchAlgorithmException;
  29. import java.text.MessageFormat;
  30. import java.text.SimpleDateFormat;
  31. import java.time.Instant;
  32. import java.util.ArrayList;
  33. import java.util.Collections;
  34. import java.util.Comparator;
  35. import java.util.Date;
  36. import java.util.HashSet;
  37. import java.util.Iterator;
  38. import java.util.List;
  39. import java.util.Locale;
  40. import java.util.Map;
  41. import java.util.Properties;
  42. import java.util.Set;
  43. import java.util.SortedMap;
  44. import java.util.TimeZone;
  45. import java.util.TreeMap;
  46. import java.util.stream.Collectors;

  47. import javax.crypto.Mac;
  48. import javax.crypto.spec.SecretKeySpec;
  49. import javax.xml.parsers.ParserConfigurationException;
  50. import javax.xml.parsers.SAXParserFactory;

  51. import org.eclipse.jgit.internal.JGitText;
  52. import org.eclipse.jgit.lib.Constants;
  53. import org.eclipse.jgit.lib.NullProgressMonitor;
  54. import org.eclipse.jgit.lib.ProgressMonitor;
  55. import org.eclipse.jgit.util.Base64;
  56. import org.eclipse.jgit.util.HttpSupport;
  57. import org.eclipse.jgit.util.StringUtils;
  58. import org.eclipse.jgit.util.TemporaryBuffer;
  59. import org.xml.sax.Attributes;
  60. import org.xml.sax.InputSource;
  61. import org.xml.sax.SAXException;
  62. import org.xml.sax.XMLReader;
  63. import org.xml.sax.helpers.DefaultHandler;

  64. /**
  65.  * A simple HTTP REST client for the Amazon S3 service.
  66.  * <p>
  67.  * This client uses the REST API to communicate with the Amazon S3 servers and
  68.  * read or write content through a bucket that the user has access to. It is a
  69.  * very lightweight implementation of the S3 API and therefore does not have all
  70.  * of the bells and whistles of popular client implementations.
  71.  * <p>
  72.  * Authentication is always performed using the user's AWSAccessKeyId and their
  73.  * private AWSSecretAccessKey.
  74.  * <p>
  75.  * Optional client-side encryption may be enabled if requested. The format is
  76.  * compatible with <a href="http://jets3t.s3.amazonaws.com/index.html">jets3t</a>,
  77.  * a popular Java based Amazon S3 client library. Enabling encryption can hide
  78.  * sensitive data from the operators of the S3 service.
  79.  */
  80. public class AmazonS3 {
  81.     private static final Set<String> SIGNED_HEADERS;

  82.     private static final String AWS_API_V2 = "2"; //$NON-NLS-1$

  83.     private static final String AWS_API_V4 = "4"; //$NON-NLS-1$

  84.     private static final String AWS_S3_SERVICE_NAME = "s3"; //$NON-NLS-1$

  85.     private static final String HMAC = "HmacSHA1"; //$NON-NLS-1$

  86.     private static final String X_AMZ_ACL = "x-amz-acl"; //$NON-NLS-1$

  87.     private static final String X_AMZ_META = "x-amz-meta-"; //$NON-NLS-1$

  88.     static {
  89.         SIGNED_HEADERS = new HashSet<>();
  90.         SIGNED_HEADERS.add("content-type"); //$NON-NLS-1$
  91.         SIGNED_HEADERS.add("content-md5"); //$NON-NLS-1$
  92.         SIGNED_HEADERS.add("date"); //$NON-NLS-1$
  93.     }

  94.     private static boolean isSignedHeader(String name) {
  95.         final String nameLC = StringUtils.toLowerCase(name);
  96.         return SIGNED_HEADERS.contains(nameLC) || nameLC.startsWith("x-amz-"); //$NON-NLS-1$
  97.     }

  98.     private static String toCleanString(List<String> list) {
  99.         final StringBuilder s = new StringBuilder();
  100.         for (String v : list) {
  101.             if (s.length() > 0)
  102.                 s.append(',');
  103.             s.append(v.replace("\n", "").trim()); //$NON-NLS-1$ //$NON-NLS-2$
  104.         }
  105.         return s.toString();
  106.     }

  107.     private static String remove(Map<String, String> m, String k) {
  108.         final String r = m.remove(k);
  109.         return r != null ? r : ""; //$NON-NLS-1$
  110.     }

  111.     private static String httpNow() {
  112.         final String tz = "GMT"; //$NON-NLS-1$
  113.         final SimpleDateFormat fmt;
  114.         fmt = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.US); //$NON-NLS-1$
  115.         fmt.setTimeZone(TimeZone.getTimeZone(tz));
  116.         return fmt.format(new Date()) + " " + tz; //$NON-NLS-1$
  117.     }

  118.     private static MessageDigest newMD5() {
  119.         try {
  120.             return MessageDigest.getInstance("MD5"); //$NON-NLS-1$
  121.         } catch (NoSuchAlgorithmException e) {
  122.             throw new RuntimeException(JGitText.get().JRELacksMD5Implementation, e);
  123.         }
  124.     }

  125.     /** AWS API Signature Version. */
  126.     private final String awsApiSignatureVersion;

  127.     /** AWSAccessKeyId, public string that identifies the user's account. */
  128.     private final String publicKey;

  129.     /** Decoded form of the private AWSSecretAccessKey, to sign requests. */
  130.     private final SecretKeySpec secretKeySpec;

  131.     /** AWSSecretAccessKey, private string used to access a user's account. */
  132.     private final char[] secretKey; // store as char[] for security

  133.     /** Our HTTP proxy support, in case we are behind a firewall. */
  134.     private final ProxySelector proxySelector;

  135.     /** ACL to apply to created objects. */
  136.     private final String acl;

  137.     /** Maximum number of times to try an operation. */
  138.     final int maxAttempts;

  139.     /** Encryption algorithm, may be a null instance that provides pass-through. */
  140.     private final WalkEncryption encryption;

  141.     /** Directory for locally buffered content. */
  142.     private final File tmpDir;

  143.     /** S3 Bucket Domain. */
  144.     private final String domain;

  145.     /** S3 Region. */
  146.     private final String region;

  147.     /** Property names used in amazon connection configuration file. */
  148.     interface Keys {
  149.         String AWS_API_SIGNATURE_VERSION = "aws.api.signature.version"; //$NON-NLS-1$
  150.         String ACCESS_KEY = "accesskey"; //$NON-NLS-1$
  151.         String SECRET_KEY = "secretkey"; //$NON-NLS-1$
  152.         String PASSWORD = "password"; //$NON-NLS-1$
  153.         String CRYPTO_ALG = "crypto.algorithm"; //$NON-NLS-1$
  154.         String CRYPTO_VER = "crypto.version"; //$NON-NLS-1$
  155.         String ACL = "acl"; //$NON-NLS-1$
  156.         String DOMAIN = "domain"; //$NON-NLS-1$
  157.         String REGION = "region"; //$NON-NLS-1$
  158.         String HTTP_RETRY = "httpclient.retry-max"; //$NON-NLS-1$
  159.         String TMP_DIR = "tmpdir"; //$NON-NLS-1$
  160.     }

  161.     /**
  162.      * Create a new S3 client for the supplied user information.
  163.      * <p>
  164.      * The connection properties are a subset of those supported by the popular
  165.      * <a href="http://jets3t.s3.amazonaws.com/index.html">jets3t</a> library.
  166.      * For example:
  167.      *
  168.      * <pre>
  169.      * # AWS API signature version, must be one of:
  170.      * #   2 - deprecated (not supported in all AWS regions)
  171.      * #   4 - latest (supported in all AWS regions)
  172.      * # Defaults to 2.
  173.      * aws.api.signature.version: 4
  174.      *
  175.      * # AWS Access and Secret Keys (required)
  176.      * accesskey: &lt;YourAWSAccessKey&gt;
  177.      * secretkey: &lt;YourAWSSecretKey&gt;
  178.      *
  179.      * # Access Control List setting to apply to uploads, must be one of:
  180.      * # PRIVATE, PUBLIC_READ (defaults to PRIVATE).
  181.      * acl: PRIVATE
  182.      *
  183.      * # S3 Domain
  184.      * # AWS S3 Region Domain (defaults to s3.amazonaws.com)
  185.      * domain: s3.amazonaws.com
  186.      *
  187.      * # AWS S3 Region (required if aws.api.signature.version = 4)
  188.      * region: us-west-2
  189.      *
  190.      * # Number of times to retry after internal error from S3.
  191.      * httpclient.retry-max: 3
  192.      *
  193.      * # End-to-end encryption (hides content from S3 owners)
  194.      * password: &lt;encryption pass-phrase&gt;
  195.      * crypto.algorithm: PBEWithMD5AndDES
  196.      * </pre>
  197.      *
  198.      * @param props
  199.      *            connection properties.
  200.      */
  201.     public AmazonS3(final Properties props) {
  202.         awsApiSignatureVersion = props
  203.                 .getProperty(Keys.AWS_API_SIGNATURE_VERSION, AWS_API_V2);
  204.         if (awsApiSignatureVersion.equals(AWS_API_V4)) {
  205.             region = props.getProperty(Keys.REGION);
  206.             if (region == null) {
  207.                 throw new IllegalArgumentException(
  208.                         JGitText.get().missingAwsRegion);
  209.             }
  210.         } else if (awsApiSignatureVersion.equals(AWS_API_V2)) {
  211.             region = null;
  212.         } else {
  213.             throw new IllegalArgumentException(MessageFormat.format(
  214.                     JGitText.get().invalidAwsApiSignatureVersion,
  215.                     awsApiSignatureVersion));
  216.         }

  217.         domain = props.getProperty(Keys.DOMAIN, "s3.amazonaws.com"); //$NON-NLS-1$

  218.         publicKey = props.getProperty(Keys.ACCESS_KEY);
  219.         if (publicKey == null)
  220.             throw new IllegalArgumentException(JGitText.get().missingAccesskey);

  221.         final String secretKeyStr = props.getProperty(Keys.SECRET_KEY);
  222.         if (secretKeyStr == null) {
  223.             throw new IllegalArgumentException(JGitText.get().missingSecretkey);
  224.         }
  225.         secretKeySpec = new SecretKeySpec(Constants.encodeASCII(secretKeyStr), HMAC);
  226.         secretKey = secretKeyStr.toCharArray();

  227.         final String pacl = props.getProperty(Keys.ACL, "PRIVATE"); //$NON-NLS-1$
  228.         if (StringUtils.equalsIgnoreCase("PRIVATE", pacl)) //$NON-NLS-1$
  229.             acl = "private"; //$NON-NLS-1$
  230.         else if (StringUtils.equalsIgnoreCase("PUBLIC", pacl)) //$NON-NLS-1$
  231.             acl = "public-read"; //$NON-NLS-1$
  232.         else if (StringUtils.equalsIgnoreCase("PUBLIC-READ", pacl)) //$NON-NLS-1$
  233.             acl = "public-read"; //$NON-NLS-1$
  234.         else if (StringUtils.equalsIgnoreCase("PUBLIC_READ", pacl)) //$NON-NLS-1$
  235.             acl = "public-read"; //$NON-NLS-1$
  236.         else
  237.             throw new IllegalArgumentException("Invalid acl: " + pacl); //$NON-NLS-1$

  238.         try {
  239.             encryption = WalkEncryption.instance(props);
  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 java.io.IOException
  260.      *             sending the request was not possible.
  261.      */
  262.     public URLConnection get(String bucket, 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, Collections.emptyMap(), 0, null);
  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 java.io.IOException
  288.      *             decryption could not be configured.
  289.      */
  290.     public InputStream decrypt(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.      * It returns the keys sorted in reverse order of LastModified time
  299.      * (freshest keys first).
  300.      *
  301.      * @param bucket
  302.      *            name of the bucket whose objects should be listed.
  303.      * @param prefix
  304.      *            common prefix to filter the results by. Must not be null.
  305.      *            Supplying the empty string will list all keys in the bucket.
  306.      *            Supplying a non-empty string will act as though a trailing '/'
  307.      *            appears in prefix, even if it does not.
  308.      * @return list of keys starting with <code>prefix</code>, after removing
  309.      *         <code>prefix</code> (or <code>prefix + "/"</code>)from all
  310.      *         of them.
  311.      * @throws java.io.IOException
  312.      *             sending the request was not possible, or the response XML
  313.      *             document could not be parsed properly.
  314.      */
  315.     public List<String> list(String bucket, String prefix)
  316.             throws IOException {
  317.         if (prefix.length() > 0 && !prefix.endsWith("/")) //$NON-NLS-1$
  318.             prefix += "/"; //$NON-NLS-1$
  319.         final ListParser lp = new ListParser(bucket, prefix);
  320.         do {
  321.             lp.list();
  322.         } while (lp.truncated);

  323.         Comparator<KeyInfo> comparator = Comparator.comparingLong(KeyInfo::getLastModifiedSecs);
  324.         return lp.entries.stream().sorted(comparator.reversed())
  325.             .map(KeyInfo::getName).collect(Collectors.toList());
  326.     }

  327.     /**
  328.      * Delete a single object.
  329.      * <p>
  330.      * Deletion always succeeds, even if the object does not exist.
  331.      *
  332.      * @param bucket
  333.      *            name of the bucket storing the object.
  334.      * @param key
  335.      *            key of the object within its bucket.
  336.      * @throws java.io.IOException
  337.      *             deletion failed due to communications error.
  338.      */
  339.     public void delete(String bucket, String key)
  340.             throws IOException {
  341.         for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
  342.             final HttpURLConnection c = open("DELETE", bucket, key); //$NON-NLS-1$
  343.             authorize(c, Collections.emptyMap(), 0, null);
  344.             switch (HttpSupport.response(c)) {
  345.             case HttpURLConnection.HTTP_NO_CONTENT:
  346.                 return;
  347.             case HttpURLConnection.HTTP_INTERNAL_ERROR:
  348.                 continue;
  349.             default:
  350.                 throw error(JGitText.get().s3ActionDeletion, key, c);
  351.             }
  352.         }
  353.         throw maxAttempts(JGitText.get().s3ActionDeletion, key);
  354.     }

  355.     /**
  356.      * Atomically create or replace a single small object.
  357.      * <p>
  358.      * This form is only suitable for smaller contents, where the caller can
  359.      * reasonable fit the entire thing into memory.
  360.      * <p>
  361.      * End-to-end data integrity is assured by internally computing the MD5
  362.      * checksum of the supplied data and transmitting the checksum along with
  363.      * the data itself.
  364.      *
  365.      * @param bucket
  366.      *            name of the bucket storing the object.
  367.      * @param key
  368.      *            key of the object within its bucket.
  369.      * @param data
  370.      *            new data content for the object. Must not be null. Zero length
  371.      *            array will create a zero length object.
  372.      * @throws java.io.IOException
  373.      *             creation/updating failed due to communications error.
  374.      */
  375.     public void put(String bucket, String key, byte[] data)
  376.             throws IOException {
  377.         if (encryption != WalkEncryption.NONE) {
  378.             // We have to copy to produce the cipher text anyway so use
  379.             // the large object code path as it supports that behavior.
  380.             //
  381.             try (OutputStream os = beginPut(bucket, key, null, null)) {
  382.                 os.write(data);
  383.             }
  384.             return;
  385.         }

  386.         final String md5str = Base64.encodeBytes(newMD5().digest(data));
  387.         final String bodyHash = awsApiSignatureVersion.equals(AWS_API_V4)
  388.                 ? AwsRequestSignerV4.calculateBodyHash(data)
  389.                 : null;
  390.         final String lenstr = String.valueOf(data.length);
  391.         for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
  392.             final HttpURLConnection c = open("PUT", bucket, key); //$NON-NLS-1$
  393.             c.setRequestProperty("Content-Length", lenstr); //$NON-NLS-1$
  394.             c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$
  395.             c.setRequestProperty(X_AMZ_ACL, acl);
  396.             authorize(c, Collections.emptyMap(), data.length, bodyHash);
  397.             c.setDoOutput(true);
  398.             c.setFixedLengthStreamingMode(data.length);
  399.             try (OutputStream os = c.getOutputStream()) {
  400.                 os.write(data);
  401.             }

  402.             switch (HttpSupport.response(c)) {
  403.             case HttpURLConnection.HTTP_OK:
  404.                 return;
  405.             case HttpURLConnection.HTTP_INTERNAL_ERROR:
  406.                 continue;
  407.             default:
  408.                 throw error(JGitText.get().s3ActionWriting, key, c);
  409.             }
  410.         }
  411.         throw maxAttempts(JGitText.get().s3ActionWriting, key);
  412.     }

  413.     /**
  414.      * Atomically create or replace a single large object.
  415.      * <p>
  416.      * Initially the returned output stream buffers data into memory, but if the
  417.      * total number of written bytes starts to exceed an internal limit the data
  418.      * is spooled to a temporary file on the local drive.
  419.      * <p>
  420.      * Network transmission is attempted only when <code>close()</code> gets
  421.      * called at the end of output. Closing the returned stream can therefore
  422.      * take significant time, especially if the written content is very large.
  423.      * <p>
  424.      * End-to-end data integrity is assured by internally computing the MD5
  425.      * checksum of the supplied data and transmitting the checksum along with
  426.      * the data itself.
  427.      *
  428.      * @param bucket
  429.      *            name of the bucket storing the object.
  430.      * @param key
  431.      *            key of the object within its bucket.
  432.      * @param monitor
  433.      *            (optional) progress monitor to post upload completion to
  434.      *            during the stream's close method.
  435.      * @param monitorTask
  436.      *            (optional) task name to display during the close method.
  437.      * @return a stream which accepts the new data, and transmits once closed.
  438.      * @throws java.io.IOException
  439.      *             if encryption was enabled it could not be configured.
  440.      */
  441.     public OutputStream beginPut(final String bucket, final String key,
  442.             final ProgressMonitor monitor, final String monitorTask)
  443.             throws IOException {
  444.         final MessageDigest md5 = newMD5();
  445.         final TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(tmpDir) {
  446.             @Override
  447.             public void close() throws IOException {
  448.                 super.close();
  449.                 try {
  450.                     putImpl(bucket, key, md5.digest(), this, monitor,
  451.                             monitorTask);
  452.                 } finally {
  453.                     destroy();
  454.                 }
  455.             }
  456.         };
  457.         return encryption.encrypt(new DigestOutputStream(buffer, md5));
  458.     }

  459.     void putImpl(final String bucket, final String key,
  460.             final byte[] csum, final TemporaryBuffer buf,
  461.             ProgressMonitor monitor, String monitorTask) throws IOException {
  462.         if (monitor == null)
  463.             monitor = NullProgressMonitor.INSTANCE;
  464.         if (monitorTask == null)
  465.             monitorTask = MessageFormat.format(JGitText.get().progressMonUploading, key);

  466.         final String md5str = Base64.encodeBytes(csum);
  467.         final String bodyHash = awsApiSignatureVersion.equals(AWS_API_V4)
  468.                 ? AwsRequestSignerV4.calculateBodyHash(buf.toByteArray())
  469.                 : null;
  470.         final long len = buf.length();
  471.         for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
  472.             final HttpURLConnection c = open("PUT", bucket, key); //$NON-NLS-1$
  473.             c.setFixedLengthStreamingMode(len);
  474.             c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$
  475.             c.setRequestProperty(X_AMZ_ACL, acl);
  476.             encryption.request(c, X_AMZ_META);
  477.             authorize(c, Collections.emptyMap(), len, bodyHash);
  478.             c.setDoOutput(true);
  479.             monitor.beginTask(monitorTask, (int) (len / 1024));
  480.             try (OutputStream os = c.getOutputStream()) {
  481.                 buf.writeTo(os, monitor);
  482.             } finally {
  483.                 monitor.endTask();
  484.             }

  485.             switch (HttpSupport.response(c)) {
  486.             case HttpURLConnection.HTTP_OK:
  487.                 return;
  488.             case HttpURLConnection.HTTP_INTERNAL_ERROR:
  489.                 continue;
  490.             default:
  491.                 throw error(JGitText.get().s3ActionWriting, key, c);
  492.             }
  493.         }
  494.         throw maxAttempts(JGitText.get().s3ActionWriting, key);
  495.     }

  496.     IOException error(final String action, final String key,
  497.             final HttpURLConnection c) throws IOException {
  498.         final IOException err = new IOException(MessageFormat.format(
  499.                 JGitText.get().amazonS3ActionFailed, action, key,
  500.                 Integer.valueOf(HttpSupport.response(c)),
  501.                 c.getResponseMessage()));
  502.         if (c.getErrorStream() == null) {
  503.             return err;
  504.         }

  505.         try (InputStream errorStream = c.getErrorStream()) {
  506.             final ByteArrayOutputStream b = new ByteArrayOutputStream();
  507.             byte[] buf = new byte[2048];
  508.             for (;;) {
  509.                 final int n = errorStream.read(buf);
  510.                 if (n < 0) {
  511.                     break;
  512.                 }
  513.                 if (n > 0) {
  514.                     b.write(buf, 0, n);
  515.                 }
  516.             }
  517.             buf = b.toByteArray();
  518.             if (buf.length > 0) {
  519.                 err.initCause(new IOException("\n" + new String(buf, UTF_8))); //$NON-NLS-1$
  520.             }
  521.         }
  522.         return err;
  523.     }

  524.     IOException maxAttempts(String action, String key) {
  525.         return new IOException(MessageFormat.format(
  526.                 JGitText.get().amazonS3ActionFailedGivingUp, action, key,
  527.                 Integer.valueOf(maxAttempts)));
  528.     }

  529.     private HttpURLConnection open(final String method, final String bucket,
  530.             final String key) throws IOException {
  531.         final Map<String, String> noArgs = Collections.emptyMap();
  532.         return open(method, bucket, key, noArgs);
  533.     }

  534.     HttpURLConnection open(final String method, final String bucket,
  535.             final String key, final Map<String, String> args)
  536.             throws IOException {
  537.         final StringBuilder urlstr = new StringBuilder();
  538.         urlstr.append("http://"); //$NON-NLS-1$
  539.         urlstr.append(bucket);
  540.         urlstr.append('.');
  541.         urlstr.append(domain);
  542.         urlstr.append('/');
  543.         if (key.length() > 0) {
  544.             if (awsApiSignatureVersion.equals(AWS_API_V2)) {
  545.                 HttpSupport.encode(urlstr, key);
  546.             } else if (awsApiSignatureVersion.equals(AWS_API_V4)) {
  547.                 urlstr.append(key);
  548.             }
  549.         }
  550.         if (!args.isEmpty()) {
  551.             final Iterator<Map.Entry<String, String>> i;

  552.             urlstr.append('?');
  553.             i = args.entrySet().iterator();
  554.             while (i.hasNext()) {
  555.                 final Map.Entry<String, String> e = i.next();
  556.                 urlstr.append(e.getKey());
  557.                 urlstr.append('=');
  558.                 HttpSupport.encode(urlstr, e.getValue());
  559.                 if (i.hasNext())
  560.                     urlstr.append('&');
  561.             }
  562.         }

  563.         final URL url = new URL(urlstr.toString());
  564.         final Proxy proxy = HttpSupport.proxyFor(proxySelector, url);
  565.         final HttpURLConnection c;

  566.         c = (HttpURLConnection) url.openConnection(proxy);
  567.         c.setRequestMethod(method);
  568.         c.setRequestProperty("User-Agent", "jgit/1.0"); //$NON-NLS-1$ //$NON-NLS-2$
  569.         c.setRequestProperty("Date", httpNow()); //$NON-NLS-1$
  570.         return c;
  571.     }

  572.     void authorize(HttpURLConnection httpURLConnection,
  573.             Map<String, String> queryParams, long contentLength,
  574.             final String bodyHash) throws IOException {
  575.         if (awsApiSignatureVersion.equals(AWS_API_V2)) {
  576.             authorizeV2(httpURLConnection);
  577.         } else if (awsApiSignatureVersion.equals(AWS_API_V4)) {
  578.             AwsRequestSignerV4.sign(httpURLConnection, queryParams, contentLength, bodyHash, AWS_S3_SERVICE_NAME,
  579.                     region, publicKey, secretKey);
  580.         }
  581.     }

  582.     void authorizeV2(HttpURLConnection c) throws IOException {
  583.         final Map<String, List<String>> reqHdr = c.getRequestProperties();
  584.         final SortedMap<String, String> sigHdr = new TreeMap<>();
  585.         for (Map.Entry<String, List<String>> entry : reqHdr.entrySet()) {
  586.             final String hdr = entry.getKey();
  587.             if (isSignedHeader(hdr))
  588.                 sigHdr.put(StringUtils.toLowerCase(hdr), toCleanString(entry.getValue()));
  589.         }

  590.         final StringBuilder s = new StringBuilder();
  591.         s.append(c.getRequestMethod());
  592.         s.append('\n');

  593.         s.append(remove(sigHdr, "content-md5")); //$NON-NLS-1$
  594.         s.append('\n');

  595.         s.append(remove(sigHdr, "content-type")); //$NON-NLS-1$
  596.         s.append('\n');

  597.         s.append(remove(sigHdr, "date")); //$NON-NLS-1$
  598.         s.append('\n');

  599.         for (Map.Entry<String, String> e : sigHdr.entrySet()) {
  600.             s.append(e.getKey());
  601.             s.append(':');
  602.             s.append(e.getValue());
  603.             s.append('\n');
  604.         }

  605.         final String host = c.getURL().getHost();
  606.         s.append('/');
  607.         s.append(host.substring(0, host.length() - domain.length() - 1));
  608.         s.append(c.getURL().getPath());

  609.         final String sec;
  610.         try {
  611.             final Mac m = Mac.getInstance(HMAC);
  612.             m.init(secretKeySpec);
  613.             sec = Base64.encodeBytes(m.doFinal(s.toString().getBytes(UTF_8)));
  614.         } catch (NoSuchAlgorithmException e) {
  615.             throw new IOException(MessageFormat.format(JGitText.get().noHMACsupport, HMAC, e.getMessage()));
  616.         } catch (InvalidKeyException e) {
  617.             throw new IOException(MessageFormat.format(JGitText.get().invalidKey, e.getMessage()));
  618.         }
  619.         c.setRequestProperty("Authorization", "AWS " + publicKey + ":" + sec); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
  620.     }

  621.     static Properties properties(File authFile)
  622.             throws FileNotFoundException, IOException {
  623.         final Properties p = new Properties();
  624.         try (FileInputStream in = new FileInputStream(authFile)) {
  625.             p.load(in);
  626.         }
  627.         return p;
  628.     }

  629.     /**
  630.      * KeyInfo enables sorting of keys by lastModified time
  631.      */
  632.     private static final class KeyInfo {
  633.         private final String name;
  634.         private final long lastModifiedSecs;
  635.         public KeyInfo(String aname, long lsecs) {
  636.             name = aname;
  637.             lastModifiedSecs = lsecs;
  638.         }
  639.         public String getName() {
  640.             return name;
  641.         }
  642.         public long getLastModifiedSecs() {
  643.             return lastModifiedSecs;
  644.         }
  645.     }

  646.     private final class ListParser extends DefaultHandler {
  647.         final List<KeyInfo> entries = new ArrayList<>();

  648.         private final String bucket;

  649.         private final String prefix;

  650.         boolean truncated;

  651.         private StringBuilder data;
  652.         private String keyName;
  653.         private Instant keyLastModified;

  654.         ListParser(String bn, String p) {
  655.             bucket = bn;
  656.             prefix = p;
  657.         }

  658.         void list() throws IOException {
  659.             final Map<String, String> args = new TreeMap<>();
  660.             if (prefix.length() > 0)
  661.                 args.put("prefix", prefix); //$NON-NLS-1$
  662.             if (!entries.isEmpty())
  663.                 args.put("marker", prefix + entries.get(entries.size() - 1).getName()); //$NON-NLS-1$

  664.             for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
  665.                 final HttpURLConnection c = open("GET", bucket, "", args); //$NON-NLS-1$ //$NON-NLS-2$
  666.                 authorize(c, args, 0, null);
  667.                 switch (HttpSupport.response(c)) {
  668.                 case HttpURLConnection.HTTP_OK:
  669.                     truncated = false;
  670.                     data = null;
  671.                     keyName = null;
  672.                     keyLastModified = null;

  673.                     final XMLReader xr;
  674.                     try {
  675.                         xr = SAXParserFactory.newInstance().newSAXParser()
  676.                                 .getXMLReader();
  677.                     } catch (SAXException | ParserConfigurationException e) {
  678.                         throw new IOException(
  679.                                 JGitText.get().noXMLParserAvailable, e);
  680.                     }
  681.                     xr.setContentHandler(this);
  682.                     try (InputStream in = c.getInputStream()) {
  683.                         xr.parse(new InputSource(in));
  684.                     } catch (SAXException parsingError) {
  685.                         throw new IOException(
  686.                                 MessageFormat.format(
  687.                                         JGitText.get().errorListing, prefix),
  688.                                 parsingError);
  689.                     }
  690.                     return;

  691.                 case HttpURLConnection.HTTP_INTERNAL_ERROR:
  692.                     continue;

  693.                 default:
  694.                     throw AmazonS3.this.error("Listing", prefix, c); //$NON-NLS-1$
  695.                 }
  696.             }
  697.             throw maxAttempts("Listing", prefix); //$NON-NLS-1$
  698.         }

  699.         @Override
  700.         public void startElement(final String uri, final String name,
  701.                 final String qName, final Attributes attributes)
  702.                 throws SAXException {
  703.             if ("Key".equals(name) || "IsTruncated".equals(name) || "LastModified".equals(name)) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
  704.                 data = new StringBuilder();
  705.             }
  706.             if ("Contents".equals(name)) { //$NON-NLS-1$
  707.                 keyName = null;
  708.                 keyLastModified = null;
  709.             }
  710.         }

  711.         @Override
  712.         public void ignorableWhitespace(final char[] ch, final int s,
  713.                 final int n) throws SAXException {
  714.             if (data != null)
  715.                 data.append(ch, s, n);
  716.         }

  717.         @Override
  718.         public void characters(char[] ch, int s, int n)
  719.                 throws SAXException {
  720.             if (data != null)
  721.                 data.append(ch, s, n);
  722.         }

  723.         @Override
  724.         public void endElement(final String uri, final String name,
  725.                 final String qName) throws SAXException {
  726.             if ("Key".equals(name))  { //$NON-NLS-1$
  727.                 keyName = data.toString().substring(prefix.length());
  728.             } else if ("IsTruncated".equals(name)) { //$NON-NLS-1$
  729.                 truncated = StringUtils.equalsIgnoreCase("true", data.toString()); //$NON-NLS-1$
  730.             } else if ("LastModified".equals(name)) { //$NON-NLS-1$
  731.                 keyLastModified = Instant.parse(data.toString());
  732.             } else if ("Contents".equals(name)) { //$NON-NLS-1$
  733.                 entries.add(new KeyInfo(keyName, keyLastModified.getEpochSecond()));
  734.             }

  735.             data = null;
  736.         }
  737.     }
  738. }