From e600c7fd316d55a14362091e5a76bd7980c13db0 Mon Sep 17 00:00:00 2001 From: Jeff Lee Date: Fri, 3 Mar 2017 13:52:44 -0800 Subject: [PATCH] sdk/java: backport Java SDK 1.1.1 to 1.1-stable Cherry-pick of #688, #690, and #689 (in that order) Closes #692 --- docs/core/get-started/sdk.md | 6 +- sdk/java/CHANGELOG.md | 25 +++++ sdk/java/pom.xml | 2 +- .../src/main/java/com/chain/http/Client.java | 98 +++++++++++++++++-- .../com/chain/http/LoggingInterceptor.java | 76 ++++++++++++++ 5 files changed, 195 insertions(+), 12 deletions(-) create mode 100644 sdk/java/src/main/java/com/chain/http/LoggingInterceptor.java diff --git a/docs/core/get-started/sdk.md b/docs/core/get-started/sdk.md index 0e873a78d8..4dc08562ab 100644 --- a/docs/core/get-started/sdk.md +++ b/docs/core/get-started/sdk.md @@ -19,7 +19,7 @@ The Java SDK is available via [Maven](https://search.maven.org/#search%7Cga%7C1% com.chain chain-sdk-java - 1.1.0 + 1.1.1 ``` @@ -27,10 +27,10 @@ The Java SDK is available via [Maven](https://search.maven.org/#search%7Cga%7C1% **Gradle** users should add the following to `build.gradle`: ``` -compile 'com.chain:chain-sdk-java:1.1.0' +compile 'com.chain:chain-sdk-java:1.1.1' ``` -You can also [download the JAR](https://search.maven.org/remotecontent?filepath=com/chain/chain-sdk-java/1.1.0/chain-sdk-java-1.1.0.jar) as a binary. +You can also [download the JAR](https://search.maven.org/remotecontent?filepath=com/chain/chain-sdk-java/1.1.0/chain-sdk-java-1.1.1.jar) as a binary. ## Node.js diff --git a/sdk/java/CHANGELOG.md b/sdk/java/CHANGELOG.md index 1b8d5985b2..1b86f6e9c7 100644 --- a/sdk/java/CHANGELOG.md +++ b/sdk/java/CHANGELOG.md @@ -1,5 +1,30 @@ # Chain Java SDK changelog +## 1.1.1 (March 3, 2017) + +### Trusted SSL certs + +You can now specify trusted server SSL certificates via a PEM-encoded file: + +``` +Client client = new Client.Builder() + .setURL("https://example:443") + .setTrustedCerts("path/to/certs.pem") + .build(); +``` + +### Request logging + +To log requests, pass an `OutputStream` to the client builder: + +``` +OutputStream os = System.out; +Client client = new Client.Builder() + .setLogger(os) + .setLogLevel(LoggingInterceptor.Level.ALL) + .build(); +``` + ## 1.1.0 (February 24, 2017) This release is a minor version update, and contains **new features** and **deprecations**. It is not compatible with cored 1.0.x; please upgrade cored before updating your SDKs. diff --git a/sdk/java/pom.xml b/sdk/java/pom.xml index 07abac4c12..319e980956 100644 --- a/sdk/java/pom.xml +++ b/sdk/java/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.chain chain-sdk-java - 1.1.0 + 1.1.1 jar ${project.groupId}:${project.artifactId} diff --git a/sdk/java/src/main/java/com/chain/http/Client.java b/sdk/java/src/main/java/com/chain/http/Client.java index 43eb1bd6c0..59258f4ec1 100644 --- a/sdk/java/src/main/java/com/chain/http/Client.java +++ b/sdk/java/src/main/java/com/chain/http/Client.java @@ -3,18 +3,16 @@ import com.chain.exception.*; import com.chain.common.*; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; +import java.io.*; import java.lang.reflect.Type; import java.net.*; -import java.util.Arrays; -import java.util.ArrayList; -import java.util.Random; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.List; -import java.util.Objects; import com.google.gson.Gson; @@ -27,6 +25,8 @@ import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; +import javax.net.ssl.*; + /** * The Client object contains all information necessary to * perform an HTTP request against a remote API. Typically, @@ -380,6 +380,11 @@ private T post(String path, Object body, ResponseCreator respCreator) private OkHttpClient buildHttpClient(Builder builder) { OkHttpClient httpClient = new OkHttpClient(); + + if (builder.sslSocketFactory != null) { + httpClient.setSslSocketFactory(builder.sslSocketFactory); + } + httpClient.setFollowRedirects(false); httpClient.setReadTimeout(builder.readTimeout, builder.readTimeoutUnit); httpClient.setWriteTimeout(builder.writeTimeout, builder.writeTimeoutUnit); @@ -393,6 +398,9 @@ private OkHttpClient buildHttpClient(Builder builder) { if (builder.cp != null) { httpClient.setCertificatePinner(builder.cp); } + if (builder.logger != null) { + httpClient.interceptors().add(new LoggingInterceptor(builder.logger, builder.logLevel)); + } return httpClient; } @@ -519,6 +527,7 @@ public static class Builder { private List urls; private String accessToken; private CertificatePinner cp; + private SSLSocketFactory sslSocketFactory; private long connectTimeout; private TimeUnit connectTimeoutUnit; private long readTimeout; @@ -527,6 +536,8 @@ public static class Builder { private TimeUnit writeTimeoutUnit; private Proxy proxy; private ConnectionPool pool; + private OutputStream logger; + private LoggingInterceptor.Level logLevel = LoggingInterceptor.Level.ERRORS; public Builder() { this.urls = new ArrayList(); @@ -595,6 +606,59 @@ public Builder setAccessToken(String accessToken) { return this; } + /** + * Trusts the given CA certs, and no others. Use this if you are running + * your own CA, or are using a self-signed server certificate. + * + * @param path The path of a file containing certificates to trust, in PEM + * format. + */ + public Builder setTrustedCerts(String path) + throws GeneralSecurityException, IOException, IllegalArgumentException, + IllegalArgumentException { + // Extract certs from PEM-encoded input. + InputStream pemStream = new FileInputStream(path); + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + Collection certificates = + certificateFactory.generateCertificates(pemStream); + if (certificates.isEmpty()) { + throw new IllegalArgumentException("expected non-empty set of trusted certificates"); + } + + // Create empty key store. + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + char[] password = + "password".toCharArray(); // The password is unimportant as long as it used consistently. + keyStore.load(null, password); + + // Load certs into key store. + int index = 0; + for (Certificate certificate : certificates) { + String certificateAlias = Integer.toString(index++); + keyStore.setCertificateEntry(certificateAlias, certificate); + } + + // Use key store to build an X509 trust manager. + KeyManagerFactory keyManagerFactory = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, password); + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException( + "Unexpected default trust managers:" + Arrays.toString(trustManagers)); + } + + // Finally, configure the socket factory. + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustManagers, null); + sslSocketFactory = sslContext.getSocketFactory(); + + return this; + } + /** * Sets the certificate pinner for the client * @param provider certificate provider @@ -658,6 +722,24 @@ public Builder setConnectionPool(int maxIdle, long timeout, TimeUnit unit) { return this; } + /** + * Sets the request logger. + * @param logger the output stream to log the requests to + */ + public Builder setLogger(OutputStream logger) { + this.logger = logger; + return this; + } + + /** + * Sets the level of the request logger. + * @param level all, errors or none + */ + public Builder setLogLevel(LoggingInterceptor.Level level) { + this.logLevel = level; + return this; + } + /** * Builds a client with all of the provided parameters. */ diff --git a/sdk/java/src/main/java/com/chain/http/LoggingInterceptor.java b/sdk/java/src/main/java/com/chain/http/LoggingInterceptor.java new file mode 100644 index 0000000000..79a31a5d89 --- /dev/null +++ b/sdk/java/src/main/java/com/chain/http/LoggingInterceptor.java @@ -0,0 +1,76 @@ +package com.chain.http; + +import com.squareup.okhttp.Interceptor; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; +import okio.BufferedSink; +import okio.BufferedSource; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + +/** + * The LoggingInterceptor object logs http requests given + * an output stream. + */ +public class LoggingInterceptor implements Interceptor { + private Level level; + private OutputStream logger; + + public enum Level { + ALL, + ERRORS, + NONE, + } + + public LoggingInterceptor(OutputStream logger, Level logAllRequests) { + this.logger = logger; + this.level = logAllRequests; + } + + @Override + public Response intercept(Interceptor.Chain chain) throws IOException { + Request request = chain.request(); + Response response = chain.proceed(request); + + boolean isError = (response.code() / 100) == 5 || (response.code() / 100) == 4; + if ((isError && level == level.ERRORS) || level == level.ALL) { + logRequestData(request, response); + } + + return response; + } + + public void logRequestData(Request request, Response response) throws IOException { + String reqid = response.header("Chain-Request-Id"); + + String requestBody; + try { + BufferedSink reqBodySink = new okio.Buffer(); + request.body().writeTo(reqBodySink); + requestBody = new String(reqBodySink.buffer().readByteArray()); + } catch (IOException e) { + requestBody = "Unable to read request body."; + } + + String label = "chain-request"; + if (response.code() / 100 == 5) { + label = "chain-error"; + } + + BufferedSource source = response.body().source(); + source.request(Long.MAX_VALUE); + + logger.write( + String.format( + "%s:\n\treqid=%s\n\turl=%s\n\tcode=%d\n\trequest=%s\n\tresponse=%s\n", + label, + reqid, + request.urlString(), + response.code(), + requestBody, + source.buffer().clone().readString(Charset.forName("UTF-8"))) + .getBytes()); + } +}