/* * Copyright (C) 2008, Shawn O. Pearce and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.transport; import static java.nio.charset.StandardCharsets.UTF_8; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.Proxy; import java.net.ProxySelector; import java.net.URL; import java.net.URLConnection; import java.security.DigestOutputStream; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.SortedMap; import java.util.TimeZone; import java.util.TreeMap; import java.util.stream.Collectors; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.util.Base64; import org.eclipse.jgit.util.HttpSupport; import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.TemporaryBuffer; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.DefaultHandler; /** * A simple HTTP REST client for the Amazon S3 service. *

* This client uses the REST API to communicate with the Amazon S3 servers and * read or write content through a bucket that the user has access to. It is a * very lightweight implementation of the S3 API and therefore does not have all * of the bells and whistles of popular client implementations. *

* Authentication is always performed using the user's AWSAccessKeyId and their * private AWSSecretAccessKey. *

* Optional client-side encryption may be enabled if requested. The format is * compatible with jets3t, * a popular Java based Amazon S3 client library. Enabling encryption can hide * sensitive data from the operators of the S3 service. */ public class AmazonS3 { private static final Set SIGNED_HEADERS; private static final String AWS_API_V2 = "2"; //$NON-NLS-1$ private static final String AWS_API_V4 = "4"; //$NON-NLS-1$ private static final String AWS_S3_SERVICE_NAME = "s3"; //$NON-NLS-1$ private static final String HMAC = "HmacSHA1"; //$NON-NLS-1$ private static final String X_AMZ_ACL = "x-amz-acl"; //$NON-NLS-1$ private static final String X_AMZ_META = "x-amz-meta-"; //$NON-NLS-1$ static { SIGNED_HEADERS = new HashSet<>(); SIGNED_HEADERS.add("content-type"); //$NON-NLS-1$ SIGNED_HEADERS.add("content-md5"); //$NON-NLS-1$ SIGNED_HEADERS.add("date"); //$NON-NLS-1$ } private static boolean isSignedHeader(String name) { final String nameLC = StringUtils.toLowerCase(name); return SIGNED_HEADERS.contains(nameLC) || nameLC.startsWith("x-amz-"); //$NON-NLS-1$ } private static String toCleanString(List list) { final StringBuilder s = new StringBuilder(); for (String v : list) { if (s.length() > 0) s.append(','); s.append(v.replace("\n", "").trim()); //$NON-NLS-1$ //$NON-NLS-2$ } return s.toString(); } private static String remove(Map m, String k) { final String r = m.remove(k); return r != null ? r : ""; //$NON-NLS-1$ } private static String httpNow() { final String tz = "GMT"; //$NON-NLS-1$ final SimpleDateFormat fmt; fmt = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.US); //$NON-NLS-1$ fmt.setTimeZone(TimeZone.getTimeZone(tz)); return fmt.format(new Date()) + " " + tz; //$NON-NLS-1$ } private static MessageDigest newMD5() { try { return MessageDigest.getInstance("MD5"); //$NON-NLS-1$ } catch (NoSuchAlgorithmException e) { throw new RuntimeException(JGitText.get().JRELacksMD5Implementation, e); } } /** AWS API Signature Version. */ private final String awsApiSignatureVersion; /** AWSAccessKeyId, public string that identifies the user's account. */ private final String publicKey; /** Decoded form of the private AWSSecretAccessKey, to sign requests. */ private final SecretKeySpec secretKeySpec; /** AWSSecretAccessKey, private string used to access a user's account. */ private final char[] secretKey; // store as char[] for security /** Our HTTP proxy support, in case we are behind a firewall. */ private final ProxySelector proxySelector; /** ACL to apply to created objects. */ private final String acl; /** Maximum number of times to try an operation. */ final int maxAttempts; /** Encryption algorithm, may be a null instance that provides pass-through. */ private final WalkEncryption encryption; /** Directory for locally buffered content. */ private final File tmpDir; /** S3 Bucket Domain. */ private final String domain; /** S3 Protocol, "https" or "http"; defaults to "http". */ private final String protocol; /** S3 Region. */ private final String region; /** Property names used in amazon connection configuration file. */ interface Keys { String AWS_API_SIGNATURE_VERSION = "aws.api.signature.version"; //$NON-NLS-1$ String ACCESS_KEY = "accesskey"; //$NON-NLS-1$ String SECRET_KEY = "secretkey"; //$NON-NLS-1$ String PASSWORD = "password"; //$NON-NLS-1$ String CRYPTO_ALG = "crypto.algorithm"; //$NON-NLS-1$ String CRYPTO_VER = "crypto.version"; //$NON-NLS-1$ String ACL = "acl"; //$NON-NLS-1$ String PROTOCOL = "protocol"; //$NON-NLS-1$ String DOMAIN = "domain"; //$NON-NLS-1$ String REGION = "region"; //$NON-NLS-1$ String HTTP_RETRY = "httpclient.retry-max"; //$NON-NLS-1$ String TMP_DIR = "tmpdir"; //$NON-NLS-1$ } /** * Create a new S3 client for the supplied user information. *

* The connection properties are a subset of those supported by the popular * jets3t library. * For example: * *

	 * # AWS API signature version, must be one of:
	 * #   2 - deprecated (not supported in all AWS regions)
	 * #   4 - latest (supported in all AWS regions)
	 * # Defaults to 2.
	 * aws.api.signature.version: 4
	 *
	 * # AWS Access and Secret Keys (required)
	 * accesskey: <YourAWSAccessKey>
	 * secretkey: <YourAWSSecretKey>
	 *
	 * # Access Control List setting to apply to uploads, must be one of:
	 * # PRIVATE, PUBLIC_READ (defaults to PRIVATE).
	 * acl: PRIVATE
	 *
	 * # S3 Domain
	 * # AWS S3 Region Domain (defaults to s3.amazonaws.com)
	 * domain: s3.amazonaws.com
	 *
	 * # AWS S3 Region (required if aws.api.signature.version = 4)
	 * region: us-west-2
	 *
	 * # Number of times to retry after internal error from S3.
	 * httpclient.retry-max: 3
	 *
	 * # End-to-end encryption (hides content from S3 owners)
	 * password: <encryption pass-phrase>
	 * crypto.algorithm: PBEWithMD5AndDES
	 * 
* * @param props * connection properties. */ public AmazonS3(final Properties props) { awsApiSignatureVersion = props .getProperty(Keys.AWS_API_SIGNATURE_VERSION, AWS_API_V2); if (awsApiSignatureVersion.equals(AWS_API_V4)) { region = props.getProperty(Keys.REGION); if (region == null) { throw new IllegalArgumentException( JGitText.get().missingAwsRegion); } } else if (awsApiSignatureVersion.equals(AWS_API_V2)) { region = null; } else { throw new IllegalArgumentException(MessageFormat.format( JGitText.get().invalidAwsApiSignatureVersion, awsApiSignatureVersion)); } protocol = props.getProperty(Keys.PROTOCOL, "http"); //$NON-NLS-1$ domain = props.getProperty(Keys.DOMAIN, "s3.amazonaws.com"); //$NON-NLS-1$ publicKey = props.getProperty(Keys.ACCESS_KEY); if (publicKey == null) throw new IllegalArgumentException(JGitText.get().missingAccesskey); final String secretKeyStr = props.getProperty(Keys.SECRET_KEY); if (secretKeyStr == null) { throw new IllegalArgumentException(JGitText.get().missingSecretkey); } secretKeySpec = new SecretKeySpec(Constants.encodeASCII(secretKeyStr), HMAC); secretKey = secretKeyStr.toCharArray(); final String pacl = props.getProperty(Keys.ACL, "PRIVATE"); //$NON-NLS-1$ if (StringUtils.equalsIgnoreCase("PRIVATE", pacl)) //$NON-NLS-1$ acl = "private"; //$NON-NLS-1$ else if (StringUtils.equalsIgnoreCase("PUBLIC", pacl)) //$NON-NLS-1$ acl = "public-read"; //$NON-NLS-1$ else if (StringUtils.equalsIgnoreCase("PUBLIC-READ", pacl)) //$NON-NLS-1$ acl = "public-read"; //$NON-NLS-1$ else if (StringUtils.equalsIgnoreCase("PUBLIC_READ", pacl)) //$NON-NLS-1$ acl = "public-read"; //$NON-NLS-1$ else throw new IllegalArgumentException("Invalid acl: " + pacl); //$NON-NLS-1$ try { encryption = WalkEncryption.instance(props); } catch (GeneralSecurityException e) { throw new IllegalArgumentException(JGitText.get().invalidEncryption, e); } maxAttempts = Integer .parseInt(props.getProperty(Keys.HTTP_RETRY, "3")); //$NON-NLS-1$ proxySelector = ProxySelector.getDefault(); String tmp = props.getProperty(Keys.TMP_DIR); tmpDir = tmp != null && tmp.length() > 0 ? new File(tmp) : null; } /** * Get the content of a bucket object. * * @param bucket * name of the bucket storing the object. * @param key * key of the object within its bucket. * @return connection to stream the content of the object. The request * properties of the connection may not be modified by the caller as * the request parameters have already been signed. * @throws java.io.IOException * sending the request was not possible. */ public URLConnection get(String bucket, String key) throws IOException { for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { final HttpURLConnection c = open("GET", bucket, key); //$NON-NLS-1$ authorize(c, Collections.emptyMap(), 0, null); switch (HttpSupport.response(c)) { case HttpURLConnection.HTTP_OK: encryption.validate(c, X_AMZ_META); return c; case HttpURLConnection.HTTP_NOT_FOUND: throw new FileNotFoundException(key); case HttpURLConnection.HTTP_INTERNAL_ERROR: continue; default: throw error(JGitText.get().s3ActionReading, key, c); } } throw maxAttempts(JGitText.get().s3ActionReading, key); } /** * Decrypt an input stream from {@link #get(String, String)}. * * @param u * connection previously created by {@link #get(String, String)}}. * @return stream to read plain text from. * @throws java.io.IOException * decryption could not be configured. */ public InputStream decrypt(URLConnection u) throws IOException { return encryption.decrypt(u.getInputStream()); } /** * List the names of keys available within a bucket. *

* This method is primarily meant for obtaining a "recursive directory * listing" rooted under the specified bucket and prefix location. * It returns the keys sorted in reverse order of LastModified time * (freshest keys first). * * @param bucket * name of the bucket whose objects should be listed. * @param prefix * common prefix to filter the results by. Must not be null. * Supplying the empty string will list all keys in the bucket. * Supplying a non-empty string will act as though a trailing '/' * appears in prefix, even if it does not. * @return list of keys starting with prefix, after removing * prefix (or prefix + "/")from all * of them. * @throws java.io.IOException * sending the request was not possible, or the response XML * document could not be parsed properly. */ public List list(String bucket, String prefix) throws IOException { if (prefix.length() > 0 && !prefix.endsWith("/")) //$NON-NLS-1$ prefix += "/"; //$NON-NLS-1$ final ListParser lp = new ListParser(bucket, prefix); do { lp.list(); } while (lp.truncated); Comparator comparator = Comparator.comparingLong(KeyInfo::getLastModifiedSecs); return lp.entries.stream().sorted(comparator.reversed()) .map(KeyInfo::getName).collect(Collectors.toList()); } /** * Delete a single object. *

* Deletion always succeeds, even if the object does not exist. * * @param bucket * name of the bucket storing the object. * @param key * key of the object within its bucket. * @throws java.io.IOException * deletion failed due to communications error. */ public void delete(String bucket, String key) throws IOException { for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { final HttpURLConnection c = open("DELETE", bucket, key); //$NON-NLS-1$ authorize(c, Collections.emptyMap(), 0, null); switch (HttpSupport.response(c)) { case HttpURLConnection.HTTP_NO_CONTENT: return; case HttpURLConnection.HTTP_INTERNAL_ERROR: continue; default: throw error(JGitText.get().s3ActionDeletion, key, c); } } throw maxAttempts(JGitText.get().s3ActionDeletion, key); } /** * Atomically create or replace a single small object. *

* This form is only suitable for smaller contents, where the caller can * reasonable fit the entire thing into memory. *

* End-to-end data integrity is assured by internally computing the MD5 * checksum of the supplied data and transmitting the checksum along with * the data itself. * * @param bucket * name of the bucket storing the object. * @param key * key of the object within its bucket. * @param data * new data content for the object. Must not be null. Zero length * array will create a zero length object. * @throws java.io.IOException * creation/updating failed due to communications error. */ public void put(String bucket, String key, byte[] data) throws IOException { if (encryption != WalkEncryption.NONE) { // We have to copy to produce the cipher text anyway so use // the large object code path as it supports that behavior. // try (OutputStream os = beginPut(bucket, key, null, null)) { os.write(data); } return; } final String md5str = Base64.encodeBytes(newMD5().digest(data)); final String bodyHash = awsApiSignatureVersion.equals(AWS_API_V4) ? AwsRequestSignerV4.calculateBodyHash(data) : null; final String lenstr = String.valueOf(data.length); for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { final HttpURLConnection c = open("PUT", bucket, key); //$NON-NLS-1$ c.setRequestProperty("Content-Length", lenstr); //$NON-NLS-1$ c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$ c.setRequestProperty(X_AMZ_ACL, acl); authorize(c, Collections.emptyMap(), data.length, bodyHash); c.setDoOutput(true); c.setFixedLengthStreamingMode(data.length); try (OutputStream os = c.getOutputStream()) { os.write(data); } switch (HttpSupport.response(c)) { case HttpURLConnection.HTTP_OK: return; case HttpURLConnection.HTTP_INTERNAL_ERROR: continue; default: throw error(JGitText.get().s3ActionWriting, key, c); } } throw maxAttempts(JGitText.get().s3ActionWriting, key); } /** * Atomically create or replace a single large object. *

* Initially the returned output stream buffers data into memory, but if the * total number of written bytes starts to exceed an internal limit the data * is spooled to a temporary file on the local drive. *

* Network transmission is attempted only when close() gets * called at the end of output. Closing the returned stream can therefore * take significant time, especially if the written content is very large. *

* End-to-end data integrity is assured by internally computing the MD5 * checksum of the supplied data and transmitting the checksum along with * the data itself. * * @param bucket * name of the bucket storing the object. * @param key * key of the object within its bucket. * @param monitor * (optional) progress monitor to post upload completion to * during the stream's close method. * @param monitorTask * (optional) task name to display during the close method. * @return a stream which accepts the new data, and transmits once closed. * @throws java.io.IOException * if encryption was enabled it could not be configured. */ public OutputStream beginPut(final String bucket, final String key, final ProgressMonitor monitor, final String monitorTask) throws IOException { final MessageDigest md5 = newMD5(); final TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(tmpDir) { @Override public void close() throws IOException { super.close(); try { putImpl(bucket, key, md5.digest(), this, monitor, monitorTask); } finally { destroy(); } } }; return encryption.encrypt(new DigestOutputStream(buffer, md5)); } void putImpl(final String bucket, final String key, final byte[] csum, final TemporaryBuffer buf, ProgressMonitor monitor, String monitorTask) throws IOException { if (monitor == null) monitor = NullProgressMonitor.INSTANCE; if (monitorTask == null) monitorTask = MessageFormat.format(JGitText.get().progressMonUploading, key); final String md5str = Base64.encodeBytes(csum); final String bodyHash = awsApiSignatureVersion.equals(AWS_API_V4) ? AwsRequestSignerV4.calculateBodyHash(buf.toByteArray()) : null; final long len = buf.length(); for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { final HttpURLConnection c = open("PUT", bucket, key); //$NON-NLS-1$ c.setFixedLengthStreamingMode(len); c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$ c.setRequestProperty(X_AMZ_ACL, acl); encryption.request(c, X_AMZ_META); authorize(c, Collections.emptyMap(), len, bodyHash); c.setDoOutput(true); monitor.beginTask(monitorTask, (int) (len / 1024)); try (OutputStream os = c.getOutputStream()) { buf.writeTo(os, monitor); } finally { monitor.endTask(); } switch (HttpSupport.response(c)) { case HttpURLConnection.HTTP_OK: return; case HttpURLConnection.HTTP_INTERNAL_ERROR: continue; default: throw error(JGitText.get().s3ActionWriting, key, c); } } throw maxAttempts(JGitText.get().s3ActionWriting, key); } IOException error(final String action, final String key, final HttpURLConnection c) throws IOException { final IOException err = new IOException(MessageFormat.format( JGitText.get().amazonS3ActionFailed, action, key, Integer.valueOf(HttpSupport.response(c)), c.getResponseMessage())); if (c.getErrorStream() == null) { return err; } try (InputStream errorStream = c.getErrorStream()) { final ByteArrayOutputStream b = new ByteArrayOutputStream(); byte[] buf = new byte[2048]; for (;;) { final int n = errorStream.read(buf); if (n < 0) { break; } if (n > 0) { b.write(buf, 0, n); } } buf = b.toByteArray(); if (buf.length > 0) { err.initCause(new IOException("\n" + new String(buf, UTF_8))); //$NON-NLS-1$ } } return err; } IOException maxAttempts(String action, String key) { return new IOException(MessageFormat.format( JGitText.get().amazonS3ActionFailedGivingUp, action, key, Integer.valueOf(maxAttempts))); } private HttpURLConnection open(final String method, final String bucket, final String key) throws IOException { final Map noArgs = Collections.emptyMap(); return open(method, bucket, key, noArgs); } HttpURLConnection open(final String method, final String bucket, final String key, final Map args) throws IOException { final StringBuilder urlstr = new StringBuilder(); urlstr.append(protocol); urlstr.append("://"); //$NON-NLS-1$ urlstr.append(bucket); urlstr.append('.'); urlstr.append(domain); urlstr.append('/'); if (key.length() > 0) { if (awsApiSignatureVersion.equals(AWS_API_V2)) { HttpSupport.encode(urlstr, key); } else if (awsApiSignatureVersion.equals(AWS_API_V4)) { urlstr.append(key); } } if (!args.isEmpty()) { final Iterator> i; urlstr.append('?'); i = args.entrySet().iterator(); while (i.hasNext()) { final Map.Entry e = i.next(); urlstr.append(e.getKey()); urlstr.append('='); HttpSupport.encode(urlstr, e.getValue()); if (i.hasNext()) urlstr.append('&'); } } final URL url = new URL(urlstr.toString()); final Proxy proxy = HttpSupport.proxyFor(proxySelector, url); final HttpURLConnection c; c = (HttpURLConnection) url.openConnection(proxy); c.setRequestMethod(method); c.setRequestProperty("User-Agent", "jgit/1.0"); //$NON-NLS-1$ //$NON-NLS-2$ c.setRequestProperty("Date", httpNow()); //$NON-NLS-1$ return c; } void authorize(HttpURLConnection httpURLConnection, Map queryParams, long contentLength, final String bodyHash) throws IOException { if (awsApiSignatureVersion.equals(AWS_API_V2)) { authorizeV2(httpURLConnection); } else if (awsApiSignatureVersion.equals(AWS_API_V4)) { AwsRequestSignerV4.sign(httpURLConnection, queryParams, contentLength, bodyHash, AWS_S3_SERVICE_NAME, region, publicKey, secretKey); } } void authorizeV2(HttpURLConnection c) throws IOException { final Map> reqHdr = c.getRequestProperties(); final SortedMap sigHdr = new TreeMap<>(); for (Map.Entry> entry : reqHdr.entrySet()) { final String hdr = entry.getKey(); if (isSignedHeader(hdr)) sigHdr.put(StringUtils.toLowerCase(hdr), toCleanString(entry.getValue())); } final StringBuilder s = new StringBuilder(); s.append(c.getRequestMethod()); s.append('\n'); s.append(remove(sigHdr, "content-md5")); //$NON-NLS-1$ s.append('\n'); s.append(remove(sigHdr, "content-type")); //$NON-NLS-1$ s.append('\n'); s.append(remove(sigHdr, "date")); //$NON-NLS-1$ s.append('\n'); for (Map.Entry e : sigHdr.entrySet()) { s.append(e.getKey()); s.append(':'); s.append(e.getValue()); s.append('\n'); } final String host = c.getURL().getHost(); s.append('/'); s.append(host.substring(0, host.length() - domain.length() - 1)); s.append(c.getURL().getPath()); final String sec; try { final Mac m = Mac.getInstance(HMAC); m.init(secretKeySpec); sec = Base64.encodeBytes(m.doFinal(s.toString().getBytes(UTF_8))); } catch (NoSuchAlgorithmException e) { throw new IOException(MessageFormat.format(JGitText.get().noHMACsupport, HMAC, e.getMessage())); } catch (InvalidKeyException e) { throw new IOException(MessageFormat.format(JGitText.get().invalidKey, e.getMessage())); } c.setRequestProperty("Authorization", "AWS " + publicKey + ":" + sec); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } static Properties properties(File authFile) throws FileNotFoundException, IOException { final Properties p = new Properties(); try (FileInputStream in = new FileInputStream(authFile)) { p.load(in); } return p; } /** * KeyInfo enables sorting of keys by lastModified time */ private static final class KeyInfo { private final String name; private final long lastModifiedSecs; public KeyInfo(String aname, long lsecs) { name = aname; lastModifiedSecs = lsecs; } public String getName() { return name; } public long getLastModifiedSecs() { return lastModifiedSecs; } } private final class ListParser extends DefaultHandler { final List entries = new ArrayList<>(); private final String bucket; private final String prefix; boolean truncated; private StringBuilder data; private String keyName; private Instant keyLastModified; ListParser(String bn, String p) { bucket = bn; prefix = p; } void list() throws IOException { final Map args = new TreeMap<>(); if (prefix.length() > 0) args.put("prefix", prefix); //$NON-NLS-1$ if (!entries.isEmpty()) args.put("marker", prefix + entries.get(entries.size() - 1).getName()); //$NON-NLS-1$ for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { final HttpURLConnection c = open("GET", bucket, "", args); //$NON-NLS-1$ //$NON-NLS-2$ authorize(c, args, 0, null); switch (HttpSupport.response(c)) { case HttpURLConnection.HTTP_OK: truncated = false; data = null; keyName = null; keyLastModified = null; final XMLReader xr; try { SAXParserFactory saxParserFactory = SAXParserFactory .newInstance(); saxParserFactory.setNamespaceAware(true); xr = saxParserFactory.newSAXParser().getXMLReader(); } catch (SAXException | ParserConfigurationException e) { throw new IOException( JGitText.get().noXMLParserAvailable, e); } xr.setContentHandler(this); try (InputStream in = c.getInputStream()) { xr.parse(new InputSource(in)); } catch (SAXException parsingError) { throw new IOException( MessageFormat.format( JGitText.get().errorListing, prefix), parsingError); } return; case HttpURLConnection.HTTP_INTERNAL_ERROR: continue; default: throw AmazonS3.this.error("Listing", prefix, c); //$NON-NLS-1$ } } throw maxAttempts("Listing", prefix); //$NON-NLS-1$ } @Override public void startElement(final String uri, final String name, final String qName, final Attributes attributes) throws SAXException { if ("Key".equals(name) || "IsTruncated".equals(name) || "LastModified".equals(name)) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ data = new StringBuilder(); } if ("Contents".equals(name)) { //$NON-NLS-1$ keyName = null; keyLastModified = null; } } @Override public void ignorableWhitespace(final char[] ch, final int s, final int n) throws SAXException { if (data != null) data.append(ch, s, n); } @Override public void characters(char[] ch, int s, int n) throws SAXException { if (data != null) data.append(ch, s, n); } @Override public void endElement(final String uri, final String name, final String qName) throws SAXException { if ("Key".equals(name)) { //$NON-NLS-1$ keyName = data.toString().substring(prefix.length()); } else if ("IsTruncated".equals(name)) { //$NON-NLS-1$ truncated = StringUtils.equalsIgnoreCase("true", data.toString()); //$NON-NLS-1$ } else if ("LastModified".equals(name)) { //$NON-NLS-1$ keyLastModified = Instant.parse(data.toString()); } else if ("Contents".equals(name)) { //$NON-NLS-1$ entries.add(new KeyInfo(keyName, keyLastModified.getEpochSecond())); } data = null; } } }