From c2f039411a4e6be707c35db7e6a07fff010c46dc Mon Sep 17 00:00:00 2001 From: Xudong Ma Date: Fri, 27 Mar 2015 19:24:59 +0800 Subject: [PATCH] TLS support for okhttp transport. Resolves #22 Add an API to let users specify ConnectionSpec. --- .../integration/TestServiceClient.java | 45 ++++++- .../squareup/okhttp/OkHttpTlsUpgrader.java | 114 ++++++++++++++++++ .../okhttp/OkHttpChannelBuilder.java | 27 ++++- .../okhttp/OkHttpClientTransport.java | 30 ++++- .../okhttp/OkHttpClientTransportFactory.java | 9 +- 5 files changed, 211 insertions(+), 14 deletions(-) create mode 100644 okhttp/src/main/java/com/squareup/okhttp/OkHttpTlsUpgrader.java diff --git a/integration-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java b/integration-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java index af21651b54..19e1a63d2f 100644 --- a/integration-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java +++ b/integration-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java @@ -37,9 +37,20 @@ import io.grpc.transport.netty.NettyChannelBuilder; import io.grpc.transport.okhttp.OkHttpChannelBuilder; import io.netty.handler.ssl.SslContext; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; +import java.security.KeyStore; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; +import javax.security.auth.x500.X500Principal; /** * Application that starts a client for the {@link TestServiceGrpc.TestService} and runs through a @@ -223,14 +234,42 @@ public class TestServiceClient { .sslContext(sslContext) .build(); } else { + OkHttpChannelBuilder builder = OkHttpChannelBuilder.forAddress(serverHost, serverPort); if (serverHostOverride != null) { - throw new IllegalStateException("Server host override unsupported with okhttp"); + // Force the hostname to match the cert the server uses. + builder.overrideHostForAuthority(serverHostOverride); } if (useTls) { - throw new IllegalStateException("TLS unsupported with okhttp"); + try { + builder.sslSocketFactory(getSslSocketFactory()); + } catch (Exception e) { + throw new RuntimeException(e); + } } - return OkHttpChannelBuilder.forAddress(serverHost, serverPort).build(); + return builder.build(); } } + + private SSLSocketFactory getSslSocketFactory() throws Exception { + if (!useTestCa) { + return (SSLSocketFactory) SSLSocketFactory.getDefault(); + } + File certChainFile = Util.loadCert("ca.pem"); + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(null, null); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate cert = (X509Certificate) cf.generateCertificate( + new BufferedInputStream(new FileInputStream(certChainFile))); + X500Principal principal = cert.getSubjectX500Principal(); + ks.setCertificateEntry(principal.getName("RFC2253"), cert); + + // Set up trust manager factory to use our key store. + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(ks); + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, trustManagerFactory.getTrustManagers(), null); + return context.getSocketFactory(); + } } } diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkHttpTlsUpgrader.java b/okhttp/src/main/java/com/squareup/okhttp/OkHttpTlsUpgrader.java new file mode 100644 index 0000000000..a41223ee4d --- /dev/null +++ b/okhttp/src/main/java/com/squareup/okhttp/OkHttpTlsUpgrader.java @@ -0,0 +1,114 @@ +/* + * Copyright 2014, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.squareup.okhttp; + +import com.google.common.base.Preconditions; + +import com.squareup.okhttp.internal.Platform; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.Socket; +import java.util.Arrays; +import java.util.Collections; + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +/** + * A helper class that located in package com.squareup.okhttp, so that we can use OkHttp internals + * to do TLS upgrading. + */ +public final class OkHttpTlsUpgrader { + + // A dummy address used to bypass null check. + private static final InetSocketAddress DUMMY_INET_SOCKET_ADDRESS = + InetSocketAddress.createUnresolved("fake", 73); + + /** + * Upgrades given Socket to be a SSLSocket. + */ + public static SSLSocket upgrade(SSLSocketFactory sslSocketFactory, + Socket socket, String host, int port, ConnectionSpec spec) throws IOException { + Preconditions.checkNotNull(sslSocketFactory); + Preconditions.checkNotNull(socket); + Preconditions.checkNotNull(spec); + SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket( + socket, host, port, true /* auto close */); + spec.apply(sslSocket, getOkHttpRoute(host, port, spec)); + + Platform platform = Platform.get(); + try { + // Force handshake. + sslSocket.startHandshake(); + + String negotiatedProtocol = platform.getSelectedProtocol(sslSocket); + if (negotiatedProtocol == null) { + throw new RuntimeException("protocol negotiation failed"); + } + Preconditions.checkState(Protocol.HTTP_2.equals(Protocol.get(negotiatedProtocol)), + "negotiated protocol is %s instead of %s.", + negotiatedProtocol, Protocol.HTTP_2.toString()); + } finally { + platform.afterHandshake(sslSocket); + } + + return sslSocket; + } + + private static Route getOkHttpRoute(String host, int port, ConnectionSpec spec) { + return new Route(getOkHttpAddress(host, port), Proxy.NO_PROXY, DUMMY_INET_SOCKET_ADDRESS, spec); + } + + private static Address getOkHttpAddress(String host, int port) { + return new Address(host, port, null, null, null, null, + DummyAuthenticator.INSTANCE, Proxy.NO_PROXY, Arrays.asList(Protocol.HTTP_2), + Collections.emptyList(), ProxySelector.getDefault()); + } + + /** + * A dummy implementation does nothing. + */ + private static class DummyAuthenticator implements Authenticator { + static final DummyAuthenticator INSTANCE = new DummyAuthenticator(); + + @Override public Request authenticate(Proxy proxy, Response response) throws IOException { + return null; + } + + @Override public Request authenticateProxy(Proxy proxy, Response response) throws IOException { + return null; + } + } +} diff --git a/okhttp/src/main/java/io/grpc/transport/okhttp/OkHttpChannelBuilder.java b/okhttp/src/main/java/io/grpc/transport/okhttp/OkHttpChannelBuilder.java index 1a62f7bdfc..721b38f076 100644 --- a/okhttp/src/main/java/io/grpc/transport/okhttp/OkHttpChannelBuilder.java +++ b/okhttp/src/main/java/io/grpc/transport/okhttp/OkHttpChannelBuilder.java @@ -34,6 +34,8 @@ package io.grpc.transport.okhttp; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.squareup.okhttp.ConnectionSpec; + import io.grpc.AbstractChannelBuilder; import io.grpc.SharedResourceHolder; import io.grpc.SharedResourceHolder.Resource; @@ -47,8 +49,8 @@ import javax.net.ssl.SSLSocketFactory; /** Convenience class for building channels with the OkHttp transport. */ public final class OkHttpChannelBuilder extends AbstractChannelBuilder { - private static final Resource DEFAULT_TRANSPORT_THREAD_POOL - = new Resource() { + private static final Resource DEFAULT_TRANSPORT_THREAD_POOL = + new Resource() { @Override public ExecutorService create() { return Executors.newCachedThreadPool(new ThreadFactoryBuilder() @@ -71,6 +73,7 @@ public final class OkHttpChannelBuilder extends AbstractChannelBuilderShould only used by tests. */ - public void overrideHostForAuthority(String host) { + public OkHttpChannelBuilder overrideHostForAuthority(String host) { this.host = host; + return this; } /** - * Provides a SSLSocketFactory to establish a secure connection. By default TLS is not enabled. + * Provides a SSLSocketFactory to establish a secure connection. */ public OkHttpChannelBuilder sslSocketFactory(SSLSocketFactory factory) { this.sslSocketFactory = factory; return this; } + /** + * For secure connection, provides a ConnectionSpec to specify Cipher suite and + * TLS versions. + * + *

By default OkHttpClientTransport.DEFAULT_CONNECTION_SPEC will be used. + */ + public OkHttpChannelBuilder setConnectionSpec(ConnectionSpec connectionSpec) { + this.connectionSpec = connectionSpec; + return this; + } + @Override protected ChannelEssentials buildEssentials() { final ExecutorService executor = (transportExecutor == null) ? SharedResourceHolder.get(DEFAULT_TRANSPORT_THREAD_POOL) : transportExecutor; - ClientTransportFactory transportFactory - = new OkHttpClientTransportFactory(serverAddress, host, executor, sslSocketFactory); + ClientTransportFactory transportFactory = new OkHttpClientTransportFactory( + serverAddress, host, executor, sslSocketFactory, connectionSpec); Runnable terminationRunnable = null; // We shut down the executor only if we created it. if (transportExecutor == null) { diff --git a/okhttp/src/main/java/io/grpc/transport/okhttp/OkHttpClientTransport.java b/okhttp/src/main/java/io/grpc/transport/okhttp/OkHttpClientTransport.java index 1e7774bf76..fa9ba117e7 100644 --- a/okhttp/src/main/java/io/grpc/transport/okhttp/OkHttpClientTransport.java +++ b/okhttp/src/main/java/io/grpc/transport/okhttp/OkHttpClientTransport.java @@ -35,6 +35,10 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.SettableFuture; +import com.squareup.okhttp.CipherSuite; +import com.squareup.okhttp.ConnectionSpec; +import com.squareup.okhttp.OkHttpTlsUpgrader; +import com.squareup.okhttp.TlsVersion; import com.squareup.okhttp.internal.spdy.ErrorCode; import com.squareup.okhttp.internal.spdy.FrameReader; import com.squareup.okhttp.internal.spdy.Header; @@ -80,6 +84,22 @@ import javax.net.ssl.SSLSocketFactory; * A okhttp-based {@link ClientTransport} implementation. */ public class OkHttpClientTransport implements ClientTransport { + public static final ConnectionSpec DEFAULT_CONNECTION_SPEC = + new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .cipherSuites( + // The following items should be sync with Netty's Http2SecurityUtil.CIPHERS. + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_DHE_DSS_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_DHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_DHE_DSS_WITH_AES_256_GCM_SHA384) + .tlsVersions(TlsVersion.TLS_1_2) + .supportsTlsExtensions(true) + .build(); + /** The default initial window size in HTTP/2 is 64 KiB for the stream and connection. */ @VisibleForTesting static final int DEFAULT_INITIAL_WINDOW_SIZE = 64 * 1024; @@ -146,9 +166,10 @@ public class OkHttpClientTransport implements ClientTransport { private int maxConcurrentStreams = Integer.MAX_VALUE; @GuardedBy("lock") private LinkedList pendingStreams = new LinkedList(); + private ConnectionSpec connectionSpec = DEFAULT_CONNECTION_SPEC; OkHttpClientTransport(InetSocketAddress address, String authorityHost, Executor executor, - SSLSocketFactory sslSocketFactory) { + @Nullable SSLSocketFactory sslSocketFactory, @Nullable ConnectionSpec connectionSpec) { this.address = Preconditions.checkNotNull(address); this.authorityHost = authorityHost; defaultAuthority = authorityHost + ":" + address.getPort(); @@ -157,6 +178,9 @@ public class OkHttpClientTransport implements ClientTransport { // use it. We start clients at 3 to avoid conflicting with HTTP negotiation. nextStreamId = 3; this.sslSocketFactory = sslSocketFactory; + if (connectionSpec != null) { + this.connectionSpec = connectionSpec; + } } /** @@ -266,8 +290,8 @@ public class OkHttpClientTransport implements ClientTransport { try { socket = new Socket(address.getAddress(), address.getPort()); if (sslSocketFactory != null) { - // We assume the sslSocketFactory will verify the server hostname. - socket = sslSocketFactory.createSocket(socket, authorityHost, address.getPort(), true); + socket = OkHttpTlsUpgrader.upgrade( + sslSocketFactory, socket, authorityHost, address.getPort(), connectionSpec); } socket.setTcpNoDelay(true); source = Okio.buffer(Okio.source(socket)); diff --git a/okhttp/src/main/java/io/grpc/transport/okhttp/OkHttpClientTransportFactory.java b/okhttp/src/main/java/io/grpc/transport/okhttp/OkHttpClientTransportFactory.java index 8f4dbcaa01..3bf1201fa5 100644 --- a/okhttp/src/main/java/io/grpc/transport/okhttp/OkHttpClientTransportFactory.java +++ b/okhttp/src/main/java/io/grpc/transport/okhttp/OkHttpClientTransportFactory.java @@ -33,6 +33,8 @@ package io.grpc.transport.okhttp; import com.google.common.base.Preconditions; +import com.squareup.okhttp.ConnectionSpec; + import io.grpc.transport.ClientTransport; import io.grpc.transport.ClientTransportFactory; @@ -49,18 +51,21 @@ class OkHttpClientTransportFactory implements ClientTransportFactory { private final ExecutorService executor; private final String authorityHost; private final SSLSocketFactory sslSocketFactory; + private final ConnectionSpec connectionSpec; public OkHttpClientTransportFactory(InetSocketAddress address, String authorityHost, - ExecutorService executor, SSLSocketFactory factory) { + ExecutorService executor, SSLSocketFactory factory, ConnectionSpec connectionSpec) { this.address = Preconditions.checkNotNull(address, "address"); this.executor = Preconditions.checkNotNull(executor, "executor"); this.authorityHost = Preconditions.checkNotNull(authorityHost, "authorityHost"); this.sslSocketFactory = factory; + this.connectionSpec = connectionSpec; } @Override public ClientTransport newClientTransport() { - return new OkHttpClientTransport(address, authorityHost, executor, sslSocketFactory); + return new OkHttpClientTransport( + address, authorityHost, executor, sslSocketFactory, connectionSpec); } }