/* * Copyright 2015-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ package com.amazonaws.http; import com.amazonaws.ClientConfiguration; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.ProtocolException; import java.net.URL; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.security.GeneralSecurityException; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; /** * An implementation of {@link HttpClient} by {@link HttpURLConnection}. This is * the recommended HTTP client in Android. Compared to {@link ApacheHttpClient}, * it has one limitation. When handling 'Expected 100-continue' header, it only * accepts either 100 continue or 417 reject, and throws * {@link ProtocolException} on other status code. Such limitation will cause * some issue when talking to S3 service. See <a * href="http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html">S3's * Put Object API</a> for the requirement of handling 100-continue. */ public class UrlHttpClient implements HttpClient { private static final String TAG = "amazonaws"; private static final Log log = LogFactory.getLog(UrlHttpClient.class); private final ClientConfiguration config; public UrlHttpClient(ClientConfiguration config) { this.config = config; } @Override public HttpResponse execute(final HttpRequest request) throws IOException { final URL url = request.getUri().toURL(); final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); final CurlBuilder curlBuilder = config.isCurlLogging() ? new CurlBuilder(request.getUri().toURL()) : null; configureConnection(request, connection); applyHeadersAndMethod(request, connection, curlBuilder); writeContentToConnection(request, connection, curlBuilder); if (curlBuilder != null) { if (curlBuilder.isValid()) { printToLog(curlBuilder.build()); } else { printToLog("Failed to create curl, content too long"); } } return createHttpResponse(request, connection); } HttpResponse createHttpResponse(final HttpRequest request, final HttpURLConnection connection) throws IOException { // connection.setDoOutput(true); final String statusText = connection.getResponseMessage(); final int statusCode = connection.getResponseCode(); InputStream content = connection.getErrorStream(); if (content == null) { // HEAD method doesn't have a body if (!request.getMethod().equals("HEAD")) { try { content = connection.getInputStream(); } catch (final IOException ioe) { // getInputStream() can throw an exception when there is no // input stream. } } } final HttpResponse.Builder builder = HttpResponse.builder() .statusCode(statusCode) .statusText(statusText) .content(content); for (final Map.Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) { // skip null field that stores connection status if (header.getKey() == null) { continue; } // No AWS service return a list of header values, so it's safe to // take the first one. builder.header(header.getKey(), header.getValue().get(0)); } return builder.build(); } /** * This is no op. */ @Override public void shutdown() { // No op } /** * Needed to pass UrlHttpClientTest. * * @see #writeContentToConnection(HttpRequest, HttpURLConnection, * CurlBuilder) */ void writeContentToConnection(HttpRequest request, HttpURLConnection connection) throws IOException { writeContentToConnection(request, connection, null /* curlBuilder */); } /** * Writes the content (if any) of the request to the passed connection * * @param request * @param connection * @param curlBuilder * @throws IOException */ void writeContentToConnection(final HttpRequest request, final HttpURLConnection connection, final CurlBuilder curlBuilder) throws IOException { // Note: if DoOutput is set to true and method is GET, HttpUrlConnection // will silently change the method to POST. if (request.getContent() != null && request.getContentLength() >= 0) { connection.setDoOutput(true); // This is for backward compatibility, because // setFixedLengthStreamingMode(long) is available in API level 19. if (!request.isStreaming()) { connection.setFixedLengthStreamingMode((int) request.getContentLength()); } final OutputStream os = connection.getOutputStream(); ByteBuffer curlBuffer = null; if (curlBuilder != null) { if (request.getContentLength() < Integer.MAX_VALUE) { curlBuffer = ByteBuffer.allocate((int) request.getContentLength()); } else { curlBuilder.setContentOverflow(true); } } write(request.getContent(), os, curlBuilder, curlBuffer); if (curlBuilder != null && curlBuffer != null && curlBuffer.position() != 0) { // has content curlBuilder.setContent(new String(curlBuffer.array(), "UTF-8")); } os.flush(); os.close(); } } /** * Needed to pass UrlHttpClientTest. * * @see #applyHeadersAndMethod(HttpRequest, HttpURLConnection, CurlBuilder) */ HttpURLConnection applyHeadersAndMethod(final HttpRequest request, final HttpURLConnection connection) throws ProtocolException { return applyHeadersAndMethod(request, connection, null /* curlBuilder */); } HttpURLConnection applyHeadersAndMethod(final HttpRequest request, final HttpURLConnection connection, final CurlBuilder curlBuilder) throws ProtocolException { // add headers if (request.getHeaders() != null && !request.getHeaders().isEmpty()) { if (curlBuilder != null) { curlBuilder.setHeaders(request.getHeaders()); } for (final Map.Entry<String, String> header : request.getHeaders().entrySet()) { final String key = header.getKey(); // Skip reserved headers for HttpURLConnection if (key.equals(HttpHeader.CONTENT_LENGTH) || key.equals(HttpHeader.HOST)) { continue; } /* * Amazon S3 suggests set 100-continue header prior to sending * the request body in order to improve efficiency. S3 may * return '100 Continue' or 417 (Expectation failed). It may * also respond with 307 to redirect the request to the correct * regional location, in which case HttpURLConection will throw * ProtocolException because it only expects either a 100 or a * 417 response. As a result, this feature is explicitly * disabled. To prevent sending the request body twice due to * redirect, please choose the correct endpoint. */ if (key.equals(HttpHeader.EXPECT)) { // continue; } connection.setRequestProperty(key, header.getValue()); } } final String method = request.getMethod(); connection.setRequestMethod(method); if (curlBuilder != null) { curlBuilder.setMethod(method); } return connection; } protected void printToLog(String message) { log.debug(message); } protected HttpURLConnection getUrlConnection(URL url) throws IOException { return (HttpURLConnection) url.openConnection(); } private void write(InputStream is, OutputStream os, CurlBuilder curlBuilder, ByteBuffer curlBuffer) throws IOException { final byte[] buf = new byte[1024 * 8]; int len; while ((len = is.read(buf)) != -1) { try { if (curlBuffer != null) { curlBuffer.put(buf, 0 /* offset */, len); } } catch (final BufferOverflowException e) { curlBuilder.setContentOverflow(true); } os.write(buf, 0, len); } } void configureConnection(HttpRequest request, HttpURLConnection connection) { // configure the connection connection.setConnectTimeout(config.getConnectionTimeout()); connection.setReadTimeout(config.getSocketTimeout()); // disable redirect and cache connection.setInstanceFollowRedirects(false); connection.setUseCaches(false); // is streaming if (request.isStreaming()) { connection.setChunkedStreamingMode(0); } // configure https connection if (connection instanceof HttpsURLConnection) { final HttpsURLConnection https = (HttpsURLConnection) connection; // disable cert check /* * Commented as per https://support.google.com/faqs/answer/6346016. Uncomment for testing. if (System.getProperty(DISABLE_CERT_CHECKING_SYSTEM_PROPERTY) != null) { disableCertificateValidation(https); } */ if (config.getTrustManager() != null) { enableCustomTrustManager(https); } } } private SSLContext sc = null; private void enableCustomTrustManager(HttpsURLConnection connection) { if (sc == null) { final TrustManager[] customTrustManagers = new TrustManager[] { config.getTrustManager() }; try { sc = SSLContext.getInstance("TLS"); sc.init(null, customTrustManagers, null); } catch (final GeneralSecurityException e) { throw new RuntimeException(e); } } connection.setSSLSocketFactory(sc.getSocketFactory()); } /* private void disableCertificateValidation(HttpsURLConnection connection) { if (sc == null) { TrustManager[] trustAllCerts = new TrustManager[] { new TrustAllManager() }; try { // Install the all-trusting trust manager sc = SSLContext.getInstance("TLS"); sc.init(null, trustAllCerts, null); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } } connection.setSSLSocketFactory(sc.getSocketFactory()); connection.setHostnameVerifier(new AllowAllHostnameVerifier()); } */ /** * An allow all hostname verifier, only used internally for testing purpose. */ /* static class AllowAllHostnameVerifier implements HostnameVerifier { @Override public boolean verify(String hostname, SSLSession session) { // Always return true to bypass host name verification return true; } } */ /** * A trust all policy manager, only used internally for testing purpose. */ /* static class TrustAllManager implements X509TrustManager { @Override public X509Certificate[] getAcceptedIssuers() { return null; } @Override public void checkClientTrusted(X509Certificate[] certs, String authType) { // No-op, to trust all certs } @Override public void checkServerTrusted(X509Certificate[] certs, String authType) { // No-op, to trust all certs } } */ /** * Helper class to build a curl message. */ private final class CurlBuilder { /** The {@link URL} of the operation. */ private final URL url; /** The method to execute on the given url. */ private String method = null; /** * A map of headers and their values to be sent with the curl request. */ private final HashMap<String, String> headers = new HashMap<String, String>(); /** The content to send with the curl request. */ private String content = null; /** Whether or not the content cannot be written to the curl command. */ private boolean contentOverflow = false; /** * Builds a new curl command for the given {@link URL}. * * @param url The {@link URL} for the operation, must not be * {@code null}. */ public CurlBuilder(URL url) { if (url == null) { throw new IllegalArgumentException("Must have a valid url"); } this.url = url; } /** * Set the method to call for the given curl command. This method will * override the previous value. * * @param method The method to use for the request. * @return This object for chaining. */ public CurlBuilder setMethod(String method) { this.method = method; return this; } /** * Set the headers used for the given curl command. This method will * override the previous values. * * @param headers The headers to use for the request. * @return This object for chaining. */ public CurlBuilder setHeaders(Map<String, String> headers) { this.headers.clear(); this.headers.putAll(headers); return this; } /** * Set the content used for the given curl command. This method will * override the previous value. * * @param content The content to use for the request. * @return This object for chaining. */ public CurlBuilder setContent(String content) { this.content = content; return this; } /** * Sets whether or not the content is too large for the curl command. * Content of length greater than {@link Integer#MAX_VALUE} are * considered too long. If set, the curl should not be logged as it will * be invalid. * * @param contentOverflow Whether or not the content is too long to * print. * @return This object for chaining. */ public CurlBuilder setContentOverflow(boolean contentOverflow) { this.contentOverflow = contentOverflow; return this; } /** * @return Whether or not this object is valid for printing. */ public boolean isValid() { return !contentOverflow; } /** * Creates a curl command that can be replayed from command line. * * @return The curl command. * @throws IllegalStateException If {@link #isValid()} returns false. */ public String build() { if (!isValid()) { throw new IllegalStateException("Invalid state, cannot create curl command"); } final StringBuilder stringBuilder = new StringBuilder("curl"); if (method != null) { stringBuilder.append(" -X ") .append(method); } for (final Map.Entry<String, String> entry : headers.entrySet()) { stringBuilder.append(" -H \"") .append(entry.getKey()) .append(":") .append(entry.getValue()) .append("\""); } if (content != null) { stringBuilder.append(" -d '") .append(content) .append("'"); } return stringBuilder.append(" ") .append(url.toString()) .toString(); } } }