diff --git a/CHANGELOG.md b/CHANGELOG.md index 451b2ac8..f426ef37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ # Changelog +## Unreleased +* Extract HTTP client logic to `HttpClient` and `JavaHttpClient` +* Make `HttpClient` configurable. + ## 3.40.0 * Add prepaid_reloadable from bin data in credit card responses * Add support for `PayPalPaymentResource` requests diff --git a/src/main/java/com/braintreegateway/Configuration.java b/src/main/java/com/braintreegateway/Configuration.java index 3afe85dd..858bd1b3 100644 --- a/src/main/java/com/braintreegateway/Configuration.java +++ b/src/main/java/com/braintreegateway/Configuration.java @@ -2,6 +2,9 @@ import com.braintreegateway.exceptions.ConfigurationException; import com.braintreegateway.util.ClientLibraryProperties; +import com.braintreegateway.util.HttpClient; +import com.braintreegateway.util.JavaHttpClient; + import java.net.InetSocketAddress; import java.net.Proxy; import java.util.logging.Level; @@ -18,6 +21,7 @@ public class Configuration { private String merchantId; private String privateKey; private String publicKey; + private HttpClient httpClient; private static Logger logger; static { @@ -155,4 +159,16 @@ public int getConnectTimeout() { public void setConnectTimeout(Integer timeout) { this.connectTimeout = timeout; } + + public HttpClient getHttpClient() { + if (httpClient == null) { + this.httpClient = new JavaHttpClient(this); + } + + return httpClient; + } + + public void setHttpClient(HttpClient httpClient) { + this.httpClient = httpClient; + } } diff --git a/src/main/java/com/braintreegateway/ThreeDSecureGateway.java b/src/main/java/com/braintreegateway/ThreeDSecureGateway.java index 5cd4379f..de22337a 100644 --- a/src/main/java/com/braintreegateway/ThreeDSecureGateway.java +++ b/src/main/java/com/braintreegateway/ThreeDSecureGateway.java @@ -3,15 +3,16 @@ import com.braintreegateway.exceptions.BraintreeException; import com.braintreegateway.exceptions.UnexpectedException; import com.braintreegateway.util.Http; -import com.braintreegateway.util.StringUtils; +import com.braintreegateway.util.HttpClient; import com.fasterxml.jackson.jr.ob.JSON; + import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.Map; -import java.util.zip.GZIPInputStream; + +import static com.braintreegateway.util.HttpClient.HttpResponse; +import static com.braintreegateway.util.HttpClient.Payload; +import static com.braintreegateway.util.HttpClient.RequestMethod; public class ThreeDSecureGateway { private final Configuration configuration; @@ -28,27 +29,19 @@ public Result lookup(ThreeDSecureLookupRequest reque } try { - URL url = new URL(configuration.getBaseURL() + configuration.getMerchantPath() + - "/client_api/v1/payment_methods/" + request.getNonce() + "/three_d_secure/lookup"); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("POST"); - connection.addRequestProperty("X-ApiVersion", Configuration.apiVersion()); - connection.addRequestProperty("Content-Type", "application/json"); - connection.setDoOutput(true); - connection.getOutputStream().write(request.toJSON().getBytes(StandardCharsets.UTF_8)); - connection.getOutputStream().close(); + Map headers = new HashMap<>(); + headers.put("X-ApiVersion", Configuration.apiVersion()); - boolean isError = connection.getResponseCode() != 201; - InputStream responseStream = isError ? connection.getErrorStream() : connection.getInputStream(); - if ("gzip".equalsIgnoreCase(connection.getContentEncoding())) { - responseStream = new GZIPInputStream(responseStream); - } + String url = configuration.getBaseURL() + configuration.getMerchantPath() + "/client_api/v1/payment_methods/" + request.getNonce() + + "/three_d_secure/lookup"; - String rawResponse = StringUtils.inputStreamToString(responseStream); - responseStream.close(); + HttpClient httpClient = configuration.getHttpClient(); + HttpResponse httpResponse = httpClient.request(RequestMethod.POST, url, Payload.json(headers, request.toJSON())); - if (isError) { - Http.throwExceptionIfErrorStatusCode(connection.getResponseCode(), rawResponse); + int responseCode = httpResponse.getResponseCode(); + String rawResponse = httpResponse.getResponseBody(); + if (responseCode != 201) { + Http.throwExceptionIfErrorStatusCode(responseCode, rawResponse); } Map jsonResponse = JSON.std.mapFrom(rawResponse); diff --git a/src/main/java/com/braintreegateway/util/GraphQLClient.java b/src/main/java/com/braintreegateway/util/GraphQLClient.java index 0bfe5bb5..7cc54eda 100644 --- a/src/main/java/com/braintreegateway/util/GraphQLClient.java +++ b/src/main/java/com/braintreegateway/util/GraphQLClient.java @@ -15,12 +15,14 @@ import com.braintreegateway.exceptions.UpgradeRequiredException; import com.fasterxml.jackson.jr.ob.JSON; import java.io.IOException; -import java.net.HttpURLConnection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; +import static com.braintreegateway.util.HttpClient.Payload; +import static com.braintreegateway.util.HttpClient.RequestMethod; + public class GraphQLClient extends Http { private Configuration configuration; public enum ErrorClass { @@ -52,22 +54,14 @@ public Map query(String definition) { // little difference to the consumer of the SDK, but should make developing // it a little easier public Map query(String definition, Map variables) { - HttpURLConnection connection = null; Map jsonMap = null; String requestString = formatGraphQLRequest(definition, variables); - String contentType = "application/json"; - Map headers = constructHeaders(contentType, contentType); + Map headers = constructHeaders("application/json"); headers.put("Braintree-Version", Configuration.GRAPHQL_API_VERSION); - try { - connection = buildConnection(Http.RequestMethod.POST, configuration.getGraphQLURL(), headers); - } catch (IOException e) { - throw new UnexpectedException(e.getMessage(), e); - } - - String jsonString = httpDo(Http.RequestMethod.POST, "/graphql", requestString, null, connection, headers, null); + String jsonString = httpDo(RequestMethod.POST, configuration.getGraphQLURL(), "/graphql", Payload.json(headers, requestString)); try { jsonMap = JSON.std.mapFrom(jsonString); diff --git a/src/main/java/com/braintreegateway/util/Http.java b/src/main/java/com/braintreegateway/util/Http.java index 357acb9d..5e5e5da5 100644 --- a/src/main/java/com/braintreegateway/util/Http.java +++ b/src/main/java/com/braintreegateway/util/Http.java @@ -9,65 +9,33 @@ import com.braintreegateway.exceptions.RequestTimeoutException; import com.braintreegateway.exceptions.ServerException; import com.braintreegateway.exceptions.ServiceUnavailableException; -import com.braintreegateway.exceptions.TimeoutException; import com.braintreegateway.exceptions.TooManyRequestsException; import com.braintreegateway.exceptions.UnexpectedException; import com.braintreegateway.exceptions.UpgradeRequiredException; import com.braintreegateway.org.apache.commons.codec.binary.Base64; -import com.fasterxml.jackson.jr.ob.JSON; + import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.SocketTimeoutException; -import java.net.URL; -import java.net.URLConnection; import java.net.URLDecoder; -import java.security.KeyStore; -import java.security.NoSuchAlgorithmException; -import java.security.Principal; -import java.security.SecureRandom; -import java.security.cert.Certificate; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; import java.text.SimpleDateFormat; -import java.util.Collection; import java.util.Date; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.zip.GZIPInputStream; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManagerFactory; + +import static com.braintreegateway.util.HttpClient.HttpResponse; +import static com.braintreegateway.util.HttpClient.Payload; +import static com.braintreegateway.util.HttpClient.RequestMethod; public class Http { - public static final String LINE_FEED = "\r\n"; private static final Pattern NUMBER_PATTERN = Pattern.compile("(.{6}).+?(.{4})"); private static final Pattern START_GROUP_PATTERN = Pattern.compile("(^)", Pattern.MULTILINE); private static final Pattern CVV_PATTERN = Pattern.compile(".+?"); private static final Pattern ENCRYPTED_CARD_DATA_PATTERN = Pattern.compile(".+?"); - - - private volatile SSLSocketFactory sslSocketFactory; - - public enum RequestMethod { - DELETE, GET, POST, PUT; - } - - private Configuration configuration; + private final Configuration configuration; public Http(Configuration configuration) { this.configuration = configuration; @@ -109,125 +77,52 @@ private NodeWrapper xmlHttpRequest(RequestMethod requestMethod, String url) { return xmlHttpRequest(requestMethod, url, null, null); } - protected String httpDo(RequestMethod requestMethod, - String url, - String postBody, - File file, - HttpURLConnection connection, - Map headers, - String boundary) { - String response = null; - - try { - Logger logger = configuration.getLogger(); - - if (postBody != null && logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, formatSanitizeBodyForLog(postBody)); - } - - if (connection instanceof HttpsURLConnection) { - ((HttpsURLConnection) connection).setSSLSocketFactory(getSSLSocketFactory()); - } - - if (postBody != null) { - OutputStream outputStream = null; - try { - outputStream = connection.getOutputStream(); - PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, "UTF-8"), true); - - if (file == null) { - outputStream.write(postBody.getBytes("UTF-8")); - } else { - Map map = JSON.std.mapFrom(postBody); - Iterator it = map.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry pair = (Map.Entry) it.next(); - addFormField((String) pair.getKey(), (String) pair.getValue(), writer, boundary); - } - addFilePart("file", file, writer, outputStream, boundary); - finish(writer, boundary); - } - } finally { - if (outputStream != null) { - outputStream.close(); - } - } - } - - throwExceptionIfErrorStatusCode(connection.getResponseCode(), connection.getResponseMessage()); - - InputStream responseStream = null; - try { - responseStream = - connection.getResponseCode() == 422 ? connection.getErrorStream() : connection.getInputStream(); + protected String httpDo(RequestMethod requestMethod, String url, String logUrl, Payload payload) { + Logger logger = configuration.getLogger(); - if ("gzip".equalsIgnoreCase(connection.getContentEncoding())) { - responseStream = new GZIPInputStream(responseStream); - } + if (payload.getBody() != null && logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, formatSanitizeBodyForLog(payload.getBody())); + } - response = StringUtils.inputStreamToString(responseStream); + HttpClient httpClient = configuration.getHttpClient(); + HttpResponse httpResponse = httpClient.request(requestMethod, url, payload); + throwExceptionIfErrorStatusCode(httpResponse.getResponseCode(), httpResponse.getResponseMessage()); - if (logger.isLoggable(Level.INFO)) { - logger.log(Level.INFO, - "[Braintree] [{0}]] {1} {2}", - new Object[]{getCurrentTime(), requestMethod.toString(), url}); - } - - if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, - "[Braintree] [{0}] {1} {2} {3}", - new Object[]{getCurrentTime(), requestMethod.toString(), url, connection.getResponseCode()}); + if (logger.isLoggable(Level.INFO)) { + logger.log(Level.INFO, + "[Braintree] [{0}]] {1} {2}", + new Object[] {getCurrentTime(), requestMethod.toString(), logUrl}); + } - if (response != null) { - logger.log(Level.FINE, formatSanitizeBodyForLog(response)); - } - } + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, + "[Braintree] [{0}] {1} {2} {3}", + new Object[] {getCurrentTime(), requestMethod.toString(), logUrl, httpResponse.getResponseCode()}); - if (response == null || response.trim().equals("")) { - return null; - } - } finally { - if (responseStream != null) { - responseStream.close(); - } + if (httpResponse.getResponseBody() != null) { + logger.log(Level.FINE, formatSanitizeBodyForLog(httpResponse.getResponseBody())); } - } catch (SocketTimeoutException e) { - throw new TimeoutException(e.getMessage(), e); - } catch (IOException e) { - throw new UnexpectedException(e.getMessage(), e); - } finally { - connection.disconnect(); } - return response; + return httpResponse.getResponseBody(); } - protected Map constructHeaders(String acceptType, String contentType) { + protected Map constructHeaders(String acceptType) { Map headers = new HashMap(); headers.put("Accept", acceptType); headers.put("User-Agent", "Braintree Java " + Configuration.VERSION); headers.put("X-ApiVersion", Configuration.apiVersion()); headers.put("Authorization", authorizationHeader()); headers.put("Accept-Encoding", "gzip"); - headers.put("Content-Type", contentType); return headers; } private NodeWrapper xmlHttpRequest(RequestMethod requestMethod, String url, String postBody, File file) { - HttpURLConnection connection = null; - String boundary = "boundary" + System.currentTimeMillis(); - String contentType = file == null ? "application/xml" : "multipart/form-data; boundary=" + boundary; + Map headers = constructHeaders("application/xml"); - Map headers = constructHeaders("application/xml", contentType); - - try { - connection = buildConnection(requestMethod, configuration.getBaseURL() + url, headers); - } catch (IOException e) { - throw new UnexpectedException(e.getMessage(), e); - } - - String response = httpDo(requestMethod, url, postBody, file, connection, headers, boundary); + Payload payload = file == null ? Payload.xml(headers, postBody) : Payload.multipart(headers, postBody, file); + String response = httpDo(requestMethod, configuration.getBaseURL() + url, url, payload); if (response != null) { return NodeWrapperFactory.instance.create(response); @@ -236,51 +131,6 @@ private NodeWrapper xmlHttpRequest(RequestMethod requestMethod, String url, Stri return null; } - private void addFormField(String key, String value, PrintWriter writer, String boundary) { - writer.append("--" + boundary).append(LINE_FEED); - writer.append("Content-Disposition: form-data; name=\"" + key + "\"").append(LINE_FEED); - writer.append(LINE_FEED); - writer.append(value).append(LINE_FEED); - writer.flush(); - } - - private void addFilePart(String fieldName, - File uploadFile, - PrintWriter writer, - OutputStream outputStream, - String boundary) - throws IOException { - String filename = uploadFile.getName(); - - writer.append("--" + boundary).append(LINE_FEED); - writer.append("Content-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"" + filename + "\"") - .append(LINE_FEED); - writer.append("Content-Type: " + URLConnection.guessContentTypeFromName(filename)).append(LINE_FEED); - writer.append(LINE_FEED); - writer.flush(); - - FileInputStream inputStream = new FileInputStream(uploadFile); - try { - byte[] buffer = new byte[4096]; - int bytesRead; - while ((bytesRead = inputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, bytesRead); - } - outputStream.flush(); - } finally { - inputStream.close(); - } - - writer.append(LINE_FEED); - writer.flush(); - } - - private void finish(PrintWriter writer, String boundary) { - writer.append("--" + boundary + "--").append(LINE_FEED); - writer.append(LINE_FEED).flush(); - writer.close(); - } - protected String formatSanitizeBodyForLog(String body) { if (body == null) { return body; @@ -306,94 +156,6 @@ protected String getCurrentTime() { return new SimpleDateFormat("d/MMM/yyyy HH:mm:ss Z").format(new Date()); } - protected SSLSocketFactory getSSLSocketFactory() { - if (sslSocketFactory == null) { - synchronized (this) { - if (sslSocketFactory == null) { - try { - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - keyStore.load(null); - - for (String certificateFilename : configuration.getEnvironment().certificateFilenames) { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - InputStream certStream = null; - try { - certStream = Http.class.getClassLoader().getResourceAsStream(certificateFilename); - - Collection coll = cf.generateCertificates(certStream); - for (Certificate cert : coll) { - if (cert instanceof X509Certificate) { - X509Certificate x509cert = (X509Certificate) cert; - Principal principal = x509cert.getSubjectDN(); - String subject = principal.getName(); - keyStore.setCertificateEntry(subject, cert); - } - } - } finally { - if (certStream != null) { - certStream.close(); - } - } - } - - KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - kmf.init(keyStore, null); - TrustManagerFactory tmf = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(keyStore); - - SSLContext sslContext = null; - try { - // Use TLS v1.2 explicitly for Java 1.6 or Java 7 JVMs that support it but do not turn it on by - // default - sslContext = SSLContext.getInstance("TLSv1.2"); - } catch (NoSuchAlgorithmException e) { - sslContext = SSLContext.getInstance("TLS"); - } - sslContext.init((KeyManager[]) kmf.getKeyManagers(), - tmf.getTrustManagers(), - SecureRandom.getInstance("SHA1PRNG")); - - sslSocketFactory = sslContext.getSocketFactory(); - } catch (Exception e) { - Logger logger = configuration.getLogger(); - logger.log(Level.SEVERE, - "SSL Verification failed. Error message: {0}", - new Object[]{e.getMessage()}); - throw new UnexpectedException(e.getMessage(), e); - } - } - } - } - return sslSocketFactory; - } - - protected HttpURLConnection buildConnection(RequestMethod requestMethod, - String urlString, - Map headers) throws java.io.IOException { - URL url = new URL(urlString); - int connectTimeout = configuration.getConnectTimeout(); - HttpURLConnection connection; - if (configuration.usesProxy()) { - connection = (HttpURLConnection) url.openConnection(configuration.getProxy()); - } else { - connection = (HttpURLConnection) url.openConnection(); - } - connection.setRequestMethod(requestMethod.toString()); - for (Map.Entry entry : headers.entrySet()) { - connection.addRequestProperty(entry.getKey(), entry.getValue()); - } - - connection.setDoOutput(true); - connection.setReadTimeout(configuration.getTimeout()); - - if (connectTimeout > 0) { - connection.setConnectTimeout(connectTimeout); - } - - return connection; - } - public static void throwExceptionIfErrorStatusCode(int statusCode, String message) { if (isErrorCode(statusCode)) { String decodedMessage = null; diff --git a/src/main/java/com/braintreegateway/util/HttpClient.java b/src/main/java/com/braintreegateway/util/HttpClient.java new file mode 100644 index 00000000..dd91b884 --- /dev/null +++ b/src/main/java/com/braintreegateway/util/HttpClient.java @@ -0,0 +1,84 @@ +package com.braintreegateway.util; + +import java.io.File; +import java.util.Map; + +public interface HttpClient { + + HttpResponse request(RequestMethod requestMethod, String url, Payload payload); + + enum RequestMethod { + DELETE, GET, POST, PUT; + } + + class Payload { + + public static final String APPLICATION_JSON = "application/json"; + public static final String APPLICATION_XML = "application/xml"; + public static final String MULTIPART_FORM_DATA = "multipart/form-data"; + + private final Map headers; + private final String contentType; + private final String body; + private final File file; + + private Payload(Map headers, String contentType, String body, File file) { + this.headers = headers; + this.contentType = contentType; + this.body = body; + this.file = file; + } + + public static Payload json(Map headers, String content) { + return new Payload(headers, APPLICATION_JSON, content, null); + } + + public static Payload xml(Map headers, String content) { + return new Payload(headers, APPLICATION_XML, content, null); + } + + public static Payload multipart(Map headers, String content, File file) { + return new Payload(headers, MULTIPART_FORM_DATA, content, file); + } + + public Map getHeaders() { + return headers; + } + + public String getContentType() { + return contentType; + } + + public String getBody() { + return body; + } + + public File getFile() { + return file; + } + } + + class HttpResponse { + private final int responseCode; + private final String responseMessage; + private final String responseBody; + + public HttpResponse(int responseCode, String responseMessage, String responseBody) { + this.responseCode = responseCode; + this.responseMessage = responseMessage; + this.responseBody = responseBody; + } + + public int getResponseCode() { + return responseCode; + } + + public String getResponseMessage() { + return responseMessage; + } + + public String getResponseBody() { + return responseBody; + } + } +} diff --git a/src/main/java/com/braintreegateway/util/JavaHttpClient.java b/src/main/java/com/braintreegateway/util/JavaHttpClient.java new file mode 100644 index 00000000..e3b6830a --- /dev/null +++ b/src/main/java/com/braintreegateway/util/JavaHttpClient.java @@ -0,0 +1,271 @@ +package com.braintreegateway.util; + +import com.braintreegateway.Configuration; +import com.braintreegateway.exceptions.TimeoutException; +import com.braintreegateway.exceptions.UnexpectedException; +import com.fasterxml.jackson.jr.ob.JSON; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.net.URLConnection; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.GZIPInputStream; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; + +import static com.braintreegateway.util.HttpClient.Payload.MULTIPART_FORM_DATA; + +public class JavaHttpClient implements HttpClient { + public static final String LINE_FEED = "\r\n"; + + private final Configuration configuration; + private volatile SSLSocketFactory sslSocketFactory; + + public JavaHttpClient(Configuration configuration) { + this.configuration = configuration; + } + + @Override + public HttpResponse request(RequestMethod requestMethod, String url, Payload payload) { + HttpURLConnection connection = null; + String boundary = "boundary" + System.currentTimeMillis(); + + try { + connection = buildConnection(requestMethod, url, payload, boundary); + } catch (IOException e) { + throw new UnexpectedException(e.getMessage(), e); + } + + try { + if (connection instanceof HttpsURLConnection) { + ((HttpsURLConnection) connection).setSSLSocketFactory(getSSLSocketFactory()); + } + + String body = payload.getBody(); + if (body != null) { + OutputStream outputStream = null; + try { + outputStream = connection.getOutputStream(); + PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, "UTF-8"), true); + + File file = payload.getFile(); + if (file == null) { + outputStream.write(body.getBytes("UTF-8")); + } else { + Map map = JSON.std.mapFrom(body); + Iterator it = map.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry pair = (Map.Entry) it.next(); + addFormField((String) pair.getKey(), (String) pair.getValue(), writer, boundary); + } + addFilePart("file", file, writer, outputStream, boundary); + finish(writer, boundary); + } + } finally { + if (outputStream != null) { + outputStream.close(); + } + } + } + + return new HttpResponse(connection.getResponseCode(), connection.getResponseMessage(), readResponseBody(connection)); + } catch (SocketTimeoutException e) { + throw new TimeoutException(e.getMessage(), e); + } catch (IOException e) { + throw new UnexpectedException(e.getMessage(), e); + } finally { + connection.disconnect(); + } + } + + private HttpURLConnection buildConnection(RequestMethod requestMethod, + String urlString, + Payload payload, + String boundary) throws IOException { + URL url = new URL(urlString); + int connectTimeout = configuration.getConnectTimeout(); + HttpURLConnection connection; + if (configuration.usesProxy()) { + connection = (HttpURLConnection) url.openConnection(configuration.getProxy()); + } else { + connection = (HttpURLConnection) url.openConnection(); + } + connection.setRequestMethod(requestMethod.toString()); + for (Map.Entry entry : payload.getHeaders().entrySet()) { + connection.addRequestProperty(entry.getKey(), entry.getValue()); + } + + String contentType = payload.getContentType(); + if (contentType.equals(MULTIPART_FORM_DATA)) { + contentType += "; boundary=" + boundary; + } + + connection.addRequestProperty("Content-Type", contentType); + + connection.setDoOutput(true); + connection.setReadTimeout(configuration.getTimeout()); + + if (connectTimeout > 0) { + connection.setConnectTimeout(connectTimeout); + } + + return connection; + } + + private SSLSocketFactory getSSLSocketFactory() { + if (sslSocketFactory == null) { + synchronized (this) { + if (sslSocketFactory == null) { + try { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null); + + for (String certificateFilename : configuration.getEnvironment().certificateFilenames) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + InputStream certStream = null; + try { + certStream = Http.class.getClassLoader().getResourceAsStream(certificateFilename); + + Collection coll = cf.generateCertificates(certStream); + for (Certificate cert : coll) { + if (cert instanceof X509Certificate) { + X509Certificate x509cert = (X509Certificate) cert; + Principal principal = x509cert.getSubjectDN(); + String subject = principal.getName(); + keyStore.setCertificateEntry(subject, cert); + } + } + } finally { + if (certStream != null) { + certStream.close(); + } + } + } + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, null); + TrustManagerFactory tmf = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keyStore); + + SSLContext sslContext = null; + try { + // Use TLS v1.2 explicitly for Java 1.6 or Java 7 JVMs that support it but do not turn it on by + // default + sslContext = SSLContext.getInstance("TLSv1.2"); + } catch (NoSuchAlgorithmException e) { + sslContext = SSLContext.getInstance("TLS"); + } + sslContext.init((KeyManager[]) kmf.getKeyManagers(), + tmf.getTrustManagers(), + SecureRandom.getInstance("SHA1PRNG")); + + sslSocketFactory = sslContext.getSocketFactory(); + } catch (Exception e) { + Logger logger = configuration.getLogger(); + logger.log(Level.SEVERE, + "SSL Verification failed. Error message: {0}", + new Object[] {e.getMessage()}); + throw new UnexpectedException(e.getMessage(), e); + } + } + } + } + return sslSocketFactory; + } + + private void addFormField(String key, String value, PrintWriter writer, String boundary) { + writer.append("--" + boundary).append(LINE_FEED); + writer.append("Content-Disposition: form-data; name=\"" + key + "\"").append(LINE_FEED); + writer.append(LINE_FEED); + writer.append(value).append(LINE_FEED); + writer.flush(); + } + + private void addFilePart(String fieldName, + File uploadFile, + PrintWriter writer, + OutputStream outputStream, + String boundary) + throws IOException { + String filename = uploadFile.getName(); + + writer.append("--" + boundary).append(LINE_FEED); + writer.append("Content-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"" + filename + "\"") + .append(LINE_FEED); + writer.append("Content-Type: " + URLConnection.guessContentTypeFromName(filename)).append(LINE_FEED); + writer.append(LINE_FEED); + writer.flush(); + + FileInputStream inputStream = new FileInputStream(uploadFile); + try { + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + outputStream.flush(); + } finally { + inputStream.close(); + } + + writer.append(LINE_FEED); + writer.flush(); + } + + private void finish(PrintWriter writer, String boundary) { + writer.append("--" + boundary + "--").append(LINE_FEED); + writer.append(LINE_FEED).flush(); + writer.close(); + } + + private String readResponseBody(HttpURLConnection connection) throws IOException { + if (connection.getResponseCode() >= 400 && connection.getResponseCode() != 422) { + return null; + } + + String response = null; + InputStream responseStream = null; + try { + InputStream errorStream = connection.getErrorStream(); + responseStream = errorStream != null ? errorStream : connection.getInputStream(); + if ("gzip".equalsIgnoreCase(connection.getContentEncoding())) { + responseStream = new GZIPInputStream(responseStream); + } + + response = StringUtils.inputStreamToString(responseStream); + if (response == null || response.trim().equals("")) { + response = null; + } + } finally { + if (responseStream != null) { + responseStream.close(); + } + } + + return response; + } +}