blob: 1a986f908d03451aec21a763f36edb02412e084c [file] [log] [blame]
/*
* Copyright 2014 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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 io.grpc.okhttp;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.grpc.internal.GrpcUtil.DEFAULT_KEEPALIVE_TIMEOUT_NANOS;
import static io.grpc.internal.GrpcUtil.DEFAULT_KEEPALIVE_TIME_NANOS;
import static io.grpc.internal.GrpcUtil.KEEPALIVE_TIME_NANOS_DISABLED;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import io.grpc.Attributes;
import io.grpc.ExperimentalApi;
import io.grpc.Internal;
import io.grpc.NameResolver;
import io.grpc.internal.AbstractManagedChannelImplBuilder;
import io.grpc.internal.AtomicBackoff;
import io.grpc.internal.ClientTransportFactory;
import io.grpc.internal.ConnectionClientTransport;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.KeepAliveManager;
import io.grpc.internal.SharedResourceHolder;
import io.grpc.internal.SharedResourceHolder.Resource;
import io.grpc.internal.TransportTracer;
import io.grpc.okhttp.internal.CipherSuite;
import io.grpc.okhttp.internal.ConnectionSpec;
import io.grpc.okhttp.internal.Platform;
import io.grpc.okhttp.internal.TlsVersion;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
/** Convenience class for building channels with the OkHttp transport. */
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/1785")
public class OkHttpChannelBuilder extends
AbstractManagedChannelImplBuilder<OkHttpChannelBuilder> {
/** Identifies the negotiation used for starting up HTTP/2. */
private enum NegotiationType {
/** Uses TLS ALPN/NPN negotiation, assumes an SSL connection. */
TLS,
/**
* Just assume the connection is plaintext (non-SSL) and the remote endpoint supports HTTP/2
* directly without an upgrade.
*/
PLAINTEXT
}
/**
* ConnectionSpec closely matching the default configuration that could be used as a basis for
* modification.
*
* <p>Since this field is the only reference in gRPC to ConnectionSpec that may not be ProGuarded,
* we are removing the field to reduce method count. We've been unable to find any existing users
* of the field, and any such user would highly likely at least be changing the cipher suites,
* which is sort of the only part that's non-obvious. Any existing user should instead create
* their own spec from scratch or base it off ConnectionSpec.MODERN_TLS if believed to be
* necessary. If this was providing you with value and don't want to see it removed, open a GitHub
* issue to discuss keeping it.
*
* @deprecated Deemed of little benefit and users weren't using it. Just define one yourself
*/
@Deprecated
public static final com.squareup.okhttp.ConnectionSpec DEFAULT_CONNECTION_SPEC =
new com.squareup.okhttp.ConnectionSpec.Builder(com.squareup.okhttp.ConnectionSpec.MODERN_TLS)
.cipherSuites(
// The following items should be sync with Netty's Http2SecurityUtil.CIPHERS.
com.squareup.okhttp.CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
com.squareup.okhttp.CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
com.squareup.okhttp.CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
com.squareup.okhttp.CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
com.squareup.okhttp.CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,
com.squareup.okhttp.CipherSuite.TLS_DHE_DSS_WITH_AES_128_GCM_SHA256,
com.squareup.okhttp.CipherSuite.TLS_DHE_RSA_WITH_AES_256_GCM_SHA384,
com.squareup.okhttp.CipherSuite.TLS_DHE_DSS_WITH_AES_256_GCM_SHA384)
.tlsVersions(com.squareup.okhttp.TlsVersion.TLS_1_2)
.supportsTlsExtensions(true)
.build();
@VisibleForTesting
static final ConnectionSpec INTERNAL_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();
private static final long AS_LARGE_AS_INFINITE = TimeUnit.DAYS.toNanos(1000L);
private static final Resource<ExecutorService> SHARED_EXECUTOR =
new Resource<ExecutorService>() {
@Override
public ExecutorService create() {
return Executors.newCachedThreadPool(GrpcUtil.getThreadFactory("grpc-okhttp-%d", true));
}
@Override
public void close(ExecutorService executor) {
executor.shutdown();
}
};
/** Creates a new builder for the given server host and port. */
public static OkHttpChannelBuilder forAddress(String host, int port) {
return new OkHttpChannelBuilder(host, port);
}
/**
* Creates a new builder for the given target that will be resolved by
* {@link io.grpc.NameResolver}.
*/
public static OkHttpChannelBuilder forTarget(String target) {
return new OkHttpChannelBuilder(target);
}
private Executor transportExecutor;
private ScheduledExecutorService scheduledExecutorService;
private SSLSocketFactory sslSocketFactory;
private HostnameVerifier hostnameVerifier;
private ConnectionSpec connectionSpec = INTERNAL_DEFAULT_CONNECTION_SPEC;
private NegotiationType negotiationType = NegotiationType.TLS;
private long keepAliveTimeNanos = KEEPALIVE_TIME_NANOS_DISABLED;
private long keepAliveTimeoutNanos = DEFAULT_KEEPALIVE_TIMEOUT_NANOS;
private boolean keepAliveWithoutCalls;
protected OkHttpChannelBuilder(String host, int port) {
this(GrpcUtil.authorityFromHostAndPort(host, port));
}
private OkHttpChannelBuilder(String target) {
super(target);
}
@VisibleForTesting
final OkHttpChannelBuilder setTransportTracerFactory(
TransportTracer.Factory transportTracerFactory) {
this.transportTracerFactory = transportTracerFactory;
return this;
}
/**
* Override the default executor necessary for internal transport use.
*
* <p>The channel does not take ownership of the given executor. It is the caller' responsibility
* to shutdown the executor when appropriate.
*/
public final OkHttpChannelBuilder transportExecutor(@Nullable Executor transportExecutor) {
this.transportExecutor = transportExecutor;
return this;
}
/**
* Sets the negotiation type for the HTTP/2 connection.
*
* <p>If TLS is enabled a default {@link SSLSocketFactory} is created using the best
* {@link java.security.Provider} available and is NOT based on
* {@link SSLSocketFactory#getDefault}. To more precisely control the TLS configuration call
* {@link #sslSocketFactory} to override the socket factory used.
*
* <p>Default: <code>TLS</code>
*
* @deprecated use {@link #usePlaintext()} or {@link #useTransportSecurity()} instead.
*/
@Deprecated
public final OkHttpChannelBuilder negotiationType(io.grpc.okhttp.NegotiationType type) {
Preconditions.checkNotNull(type, "type");
switch (type) {
case TLS:
negotiationType = NegotiationType.TLS;
break;
case PLAINTEXT:
negotiationType = NegotiationType.PLAINTEXT;
break;
default:
throw new AssertionError("Unknown negotiation type: " + type);
}
return this;
}
/**
* Enable keepalive with default delay and timeout.
*
* @deprecated Use {@link #keepAliveTime} instead
*/
@Deprecated
public final OkHttpChannelBuilder enableKeepAlive(boolean enable) {
if (enable) {
return keepAliveTime(DEFAULT_KEEPALIVE_TIME_NANOS, TimeUnit.NANOSECONDS);
} else {
return keepAliveTime(KEEPALIVE_TIME_NANOS_DISABLED, TimeUnit.NANOSECONDS);
}
}
/**
* Enable keepalive with custom delay and timeout.
*
* @deprecated Use {@link #keepAliveTime} and {@link #keepAliveTimeout} instead
*/
@Deprecated
public final OkHttpChannelBuilder enableKeepAlive(boolean enable, long keepAliveTime,
TimeUnit delayUnit, long keepAliveTimeout, TimeUnit timeoutUnit) {
if (enable) {
return keepAliveTime(keepAliveTime, delayUnit)
.keepAliveTimeout(keepAliveTimeout, timeoutUnit);
} else {
return keepAliveTime(KEEPALIVE_TIME_NANOS_DISABLED, TimeUnit.NANOSECONDS);
}
}
/**
* {@inheritDoc}
*
* @since 1.3.0
*/
@Override
public OkHttpChannelBuilder keepAliveTime(long keepAliveTime, TimeUnit timeUnit) {
Preconditions.checkArgument(keepAliveTime > 0L, "keepalive time must be positive");
keepAliveTimeNanos = timeUnit.toNanos(keepAliveTime);
keepAliveTimeNanos = KeepAliveManager.clampKeepAliveTimeInNanos(keepAliveTimeNanos);
if (keepAliveTimeNanos >= AS_LARGE_AS_INFINITE) {
// Bump keepalive time to infinite. This disables keepalive.
keepAliveTimeNanos = KEEPALIVE_TIME_NANOS_DISABLED;
}
return this;
}
/**
* {@inheritDoc}
*
* @since 1.3.0
*/
@Override
public OkHttpChannelBuilder keepAliveTimeout(long keepAliveTimeout, TimeUnit timeUnit) {
Preconditions.checkArgument(keepAliveTimeout > 0L, "keepalive timeout must be positive");
keepAliveTimeoutNanos = timeUnit.toNanos(keepAliveTimeout);
keepAliveTimeoutNanos = KeepAliveManager.clampKeepAliveTimeoutInNanos(keepAliveTimeoutNanos);
return this;
}
/**
* {@inheritDoc}
*
* @since 1.3.0
* @see #keepAliveTime(long, TimeUnit)
*/
@Override
public OkHttpChannelBuilder keepAliveWithoutCalls(boolean enable) {
keepAliveWithoutCalls = enable;
return this;
}
/**
* Override the default {@link SSLSocketFactory} and enable TLS negotiation.
*/
public final OkHttpChannelBuilder sslSocketFactory(SSLSocketFactory factory) {
this.sslSocketFactory = factory;
negotiationType = NegotiationType.TLS;
return this;
}
/**
* Set the hostname verifier to use when using TLS negotiation. The hostnameVerifier is only used
* if using TLS negotiation. If the hostname verifier is not set, a default hostname verifier is
* used.
*
* <p>Be careful when setting a custom hostname verifier! By setting a non-null value, you are
* replacing all default verification behavior. If the hostname verifier you supply does not
* effectively supply the same checks, you may be removing the security assurances that TLS aims
* to provide.</p>
*
* <p>This method should not be used to avoid hostname verification, even during testing, since
* {@link #overrideAuthority} is a safer alternative as it does not disable any security checks.
* </p>
*
* @see io.grpc.okhttp.internal.OkHostnameVerifier
*
* @since 1.6.0
* @return this
*
*/
public final OkHttpChannelBuilder hostnameVerifier(@Nullable HostnameVerifier hostnameVerifier) {
this.hostnameVerifier = hostnameVerifier;
return this;
}
/**
* For secure connection, provides a ConnectionSpec to specify Cipher suite and
* TLS versions.
*
* <p>By default a modern, HTTP/2-compatible spec will be used.
*
* <p>This method is only used when building a secure connection. For plaintext
* connection, use {@link #usePlaintext()} instead.
*
* @throws IllegalArgumentException
* If {@code connectionSpec} is not with TLS
*/
public final OkHttpChannelBuilder connectionSpec(
com.squareup.okhttp.ConnectionSpec connectionSpec) {
Preconditions.checkArgument(connectionSpec.isTls(), "plaintext ConnectionSpec is not accepted");
this.connectionSpec = Utils.convertSpec(connectionSpec);
return this;
}
/**
* Equivalent to using {@link #negotiationType} with {@code PLAINTEXT}.
*
* @deprecated use {@link #usePlaintext()} instead.
*/
@Override
@Deprecated
public final OkHttpChannelBuilder usePlaintext(boolean skipNegotiation) {
if (skipNegotiation) {
negotiationType(io.grpc.okhttp.NegotiationType.PLAINTEXT);
} else {
throw new IllegalArgumentException("Plaintext negotiation not currently supported");
}
return this;
}
/** Sets the negotiation type for the HTTP/2 connection to plaintext. */
@Override
public final OkHttpChannelBuilder usePlaintext() {
negotiationType = NegotiationType.PLAINTEXT;
return this;
}
/**
* Sets the negotiation type for the HTTP/2 connection to TLS (this is the default).
*
* <p>With TLS enabled, a default {@link SSLSocketFactory} is created using the best {@link
* java.security.Provider} available and is NOT based on {@link SSLSocketFactory#getDefault}. To
* more precisely control the TLS configuration call {@link #sslSocketFactory} to override the
* socket factory used.
*/
@Override
public final OkHttpChannelBuilder useTransportSecurity() {
negotiationType = NegotiationType.TLS;
return this;
}
/**
* Provides a custom scheduled executor service.
*
* <p>It's an optional parameter. If the user has not provided a scheduled executor service when
* the channel is built, the builder will use a static cached thread pool.
*
* @return this
*
* @since 1.11.0
*/
public final OkHttpChannelBuilder scheduledExecutorService(
ScheduledExecutorService scheduledExecutorService) {
this.scheduledExecutorService =
checkNotNull(scheduledExecutorService, "scheduledExecutorService");
return this;
}
@Override
@Internal
protected final ClientTransportFactory buildTransportFactory() {
boolean enableKeepAlive = keepAliveTimeNanos != KEEPALIVE_TIME_NANOS_DISABLED;
return new OkHttpTransportFactory(transportExecutor, scheduledExecutorService,
createSocketFactory(), hostnameVerifier, connectionSpec, maxInboundMessageSize(),
enableKeepAlive, keepAliveTimeNanos, keepAliveTimeoutNanos, keepAliveWithoutCalls,
transportTracerFactory);
}
@Override
protected Attributes getNameResolverParams() {
int defaultPort;
switch (negotiationType) {
case PLAINTEXT:
defaultPort = GrpcUtil.DEFAULT_PORT_PLAINTEXT;
break;
case TLS:
defaultPort = GrpcUtil.DEFAULT_PORT_SSL;
break;
default:
throw new AssertionError(negotiationType + " not handled");
}
return Attributes.newBuilder()
.set(NameResolver.Factory.PARAMS_DEFAULT_PORT, defaultPort).build();
}
@VisibleForTesting
@Nullable
SSLSocketFactory createSocketFactory() {
switch (negotiationType) {
case TLS:
try {
if (sslSocketFactory == null) {
SSLContext sslContext;
if (GrpcUtil.IS_RESTRICTED_APPENGINE) {
// The following auth code circumvents the following AccessControlException:
// access denied ("java.util.PropertyPermission" "javax.net.ssl.keyStore" "read")
// Conscrypt will attempt to load the default KeyStore if a trust manager is not
// provided, which is forbidden on AppEngine
sslContext = SSLContext.getInstance("TLS", Platform.get().getProvider());
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
sslContext.init(
null,
trustManagerFactory.getTrustManagers(),
// Use an algorithm that doesn't need /dev/urandom
SecureRandom.getInstance("SHA1PRNG", Platform.get().getProvider()));
} else {
sslContext = SSLContext.getInstance("Default", Platform.get().getProvider());
}
sslSocketFactory = sslContext.getSocketFactory();
}
return sslSocketFactory;
} catch (GeneralSecurityException gse) {
throw new RuntimeException("TLS Provider failure", gse);
}
case PLAINTEXT:
return null;
default:
throw new RuntimeException("Unknown negotiation type: " + negotiationType);
}
}
/**
* Creates OkHttp transports. Exposed for internal use, as it should be private.
*/
@Internal
static final class OkHttpTransportFactory implements ClientTransportFactory {
private final Executor executor;
private final boolean usingSharedExecutor;
private final boolean usingSharedScheduler;
private final TransportTracer.Factory transportTracerFactory;
@Nullable
private final SSLSocketFactory socketFactory;
@Nullable
private final HostnameVerifier hostnameVerifier;
private final ConnectionSpec connectionSpec;
private final int maxMessageSize;
private final boolean enableKeepAlive;
private final AtomicBackoff keepAliveTimeNanos;
private final long keepAliveTimeoutNanos;
private final boolean keepAliveWithoutCalls;
private final ScheduledExecutorService timeoutService;
private boolean closed;
private OkHttpTransportFactory(Executor executor,
@Nullable ScheduledExecutorService timeoutService,
@Nullable SSLSocketFactory socketFactory,
@Nullable HostnameVerifier hostnameVerifier,
ConnectionSpec connectionSpec,
int maxMessageSize,
boolean enableKeepAlive,
long keepAliveTimeNanos,
long keepAliveTimeoutNanos,
boolean keepAliveWithoutCalls,
TransportTracer.Factory transportTracerFactory) {
usingSharedScheduler = timeoutService == null;
this.timeoutService = usingSharedScheduler
? SharedResourceHolder.get(GrpcUtil.TIMER_SERVICE) : timeoutService;
this.socketFactory = socketFactory;
this.hostnameVerifier = hostnameVerifier;
this.connectionSpec = connectionSpec;
this.maxMessageSize = maxMessageSize;
this.enableKeepAlive = enableKeepAlive;
this.keepAliveTimeNanos = new AtomicBackoff("keepalive time nanos", keepAliveTimeNanos);
this.keepAliveTimeoutNanos = keepAliveTimeoutNanos;
this.keepAliveWithoutCalls = keepAliveWithoutCalls;
usingSharedExecutor = executor == null;
this.transportTracerFactory =
Preconditions.checkNotNull(transportTracerFactory, "transportTracerFactory");
if (usingSharedExecutor) {
// The executor was unspecified, using the shared executor.
this.executor = SharedResourceHolder.get(SHARED_EXECUTOR);
} else {
this.executor = executor;
}
}
@Override
public ConnectionClientTransport newClientTransport(
SocketAddress addr, ClientTransportOptions options) {
if (closed) {
throw new IllegalStateException("The transport factory is closed.");
}
final AtomicBackoff.State keepAliveTimeNanosState = keepAliveTimeNanos.getState();
Runnable tooManyPingsRunnable = new Runnable() {
@Override
public void run() {
keepAliveTimeNanosState.backoff();
}
};
InetSocketAddress inetSocketAddr = (InetSocketAddress) addr;
OkHttpClientTransport transport = new OkHttpClientTransport(
inetSocketAddr,
options.getAuthority(),
options.getUserAgent(),
executor,
socketFactory,
hostnameVerifier,
connectionSpec,
maxMessageSize,
options.getProxyParameters(),
tooManyPingsRunnable,
transportTracerFactory.create());
if (enableKeepAlive) {
transport.enableKeepAlive(
true, keepAliveTimeNanosState.get(), keepAliveTimeoutNanos, keepAliveWithoutCalls);
}
return transport;
}
@Override
public ScheduledExecutorService getScheduledExecutorService() {
return timeoutService;
}
@Override
public void close() {
if (closed) {
return;
}
closed = true;
if (usingSharedScheduler) {
SharedResourceHolder.release(GrpcUtil.TIMER_SERVICE, timeoutService);
}
if (usingSharedExecutor) {
SharedResourceHolder.release(SHARED_EXECUTOR, (ExecutorService) executor);
}
}
}
}