Switching to Netty's ALPN support.

Also adding documentation for using gRPC with TLS-ALPN within Jetty.

Fixes #180
This commit is contained in:
nmittler 2015-05-22 11:17:12 -07:00
parent f770ffb18f
commit 59f6f45cc4
3 changed files with 34 additions and 148 deletions

View File

@ -20,6 +20,7 @@ Note that you must use the release of the Jetty-ALPN jar specific to the version
An option is provided to use GRPC over plaintext without TLS. This is convenient for testing environments, however users must be aware of the secuirty risks of doing so for real production systems.
### TLS-ALPN on Android
On Android, it is needed to <a href="https://developer.android.com/training/articles/security-gms-provider.html">update your security provider</a> to enable ALPN support, especially for Android versions < 5.0. If the provider fails to update, ALPN may not work.
After the update is done, you'll need to pass an SSLSocketFactorty to OkHttpChannelBuilder, like the code snippet below shows.
@ -29,6 +30,23 @@ OkHttpChannelBuilder channelBuilder = OkHttpChannelBuilder.forAddress(host, port
.sslSocketFactory(SSLContext.getDefault().getSocketFactory());
```
### TLS-ALPN in Jetty
Some web containers, such as <a href="http://www.eclipse.org/jetty/documentation/current/jetty-classloading.html">Jetty</a> restrict access to server classes for web applications. A gRPC client running within such a container must be properly configured to allow access to the ALPN classes.
In Jetty, this is done by including a `WEB-INF/jetty-env.xml` file containing the following:
```xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
<!-- Must be done in jetty-env.xml, since jetty-web.xml is loaded too late. -->
<!-- Removing ALPN from the blacklisted server classes (using "-" to remove). -->
<!-- Must prepend to the blacklist since order matters. -->
<Call name="prependServerClass">
<Arg>-org.eclipse.jetty.alpn.</Arg>
</Call>
</Configure>
```
# Using OAuth2

View File

@ -32,6 +32,10 @@
package io.grpc.transport.netty;
import io.netty.handler.codec.http2.Http2SecurityUtil;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol;
import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior;
import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
@ -43,6 +47,16 @@ import java.io.File;
public class GrpcSslContexts {
private GrpcSslContexts() {}
private static ApplicationProtocolConfig DEFAULT_APN = new ApplicationProtocolConfig(
Protocol.ALPN,
SelectorFailureBehavior.FATAL_ALERT,
SelectedListenerFailureBehavior.FATAL_ALERT,
"h2",
"h2-17",
"h2-16",
"h2-15",
"h2-14");
/**
* Creates a SslContextBuilder with ciphers and APN appropriate for gRPC.
*
@ -80,7 +94,6 @@ public class GrpcSslContexts {
*/
public static SslContextBuilder configure(SslContextBuilder builder) {
return builder.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
// We currently handle ALPN ourselves, so we require ALPN in Netty disabled.
.applicationProtocolConfig(null);
.applicationProtocolConfig(DEFAULT_APN);
}
}

View File

@ -32,7 +32,6 @@
package io.grpc.transport.netty;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.SettableFuture;
import io.netty.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
@ -55,17 +54,12 @@ import io.netty.util.ByteString;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.InetSocketAddress;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
@ -74,20 +68,6 @@ import javax.net.ssl.SSLParameters;
* Common {@link ProtocolNegotiator}s used by gRPC.
*/
public final class ProtocolNegotiators {
private static final Logger log = Logger.getLogger(ProtocolNegotiators.class.getName());
// TODO(madongfly): Remove "h2-xx" at a right time.
private static final List<String> SUPPORTED_PROTOCOLS = Collections.unmodifiableList(
Arrays.asList(
"h2",
Http2OrHttpChooser.SelectedProtocol.HTTP_2.protocolName(),
"h2-14",
"h2-15",
"h2-16"));
// Prefer ALPN to NPN so try it first.
private static final String[] JETTY_TLS_NEGOTIATION_IMPL =
{"org.eclipse.jetty.alpn.ALPN", "org.eclipse.jetty.npn.NextProtoNego"};
private ProtocolNegotiators() {
}
@ -97,12 +77,6 @@ public final class ProtocolNegotiators {
*/
public static ChannelHandler serverTls(SSLEngine sslEngine) {
Preconditions.checkNotNull(sslEngine, "sslEngine");
if (!isOpenSsl(sslEngine.getClass())) {
// Using JDK SSL
if (!installJettyTlsProtocolSelection(sslEngine, SettableFuture.<Void>create(), true)) {
throw new IllegalStateException("NPN/ALPN extensions not installed");
}
}
return new SslHandler(sslEngine, false);
}
@ -126,28 +100,14 @@ public final class ProtocolNegotiators {
sslParams.setEndpointIdentificationAlgorithm("HTTPS");
sslEngine.setSSLParameters(sslParams);
final SettableFuture<Void> completeFuture = SettableFuture.create();
if (isOpenSsl(sslContext.getClass())) {
completeFuture.set(null);
} else {
// Using JDK SSL
if (!installJettyTlsProtocolSelection(sslEngine, completeFuture, false)) {
throw new IllegalStateException("NPN/ALPN extensions not installed");
}
}
SslHandler sslHandler = new SslHandler(sslEngine, false);
sslHandler.handshakeFuture().addListener(
new GenericFutureListener<Future<? super Channel>>() {
@Override
public void operationComplete(Future<? super Channel> future) throws Exception {
// If an error occurred during the handshake, throw it to the pipeline.
if (future.isSuccess()) {
completeFuture.get();
} else {
future.get();
}
}
});
ctx.pipeline().replace(this, "sslHandler", sslHandler);
}
@ -190,13 +150,6 @@ public final class ProtocolNegotiators {
};
}
/**
* Returns {@code true} if the given class is for use with Netty OpenSsl.
*/
private static boolean isOpenSsl(Class<?> clazz) {
return clazz.getSimpleName().toLowerCase().contains("openssl");
}
/**
* Buffers all writes until either {@link #writeBufferedAndRemove(ChannelHandlerContext)} or
* {@link #failBufferedAndClose(ChannelHandlerContext)} is called. This handler allows us to
@ -419,102 +372,4 @@ public final class ProtocolNegotiators {
super.userEventTriggered(ctx, evt);
}
}
/**
* Find Jetty's TLS NPN/ALPN extensions and attempt to use them
*
* @return true if NPN/ALPN support is available.
*/
private static boolean installJettyTlsProtocolSelection(final SSLEngine engine,
final SettableFuture<Void> protocolNegotiated, boolean server) {
for (String protocolNegoClassName : JETTY_TLS_NEGOTIATION_IMPL) {
try {
Class<?> negoClass;
try {
negoClass = Class.forName(protocolNegoClassName, true, null);
} catch (ClassNotFoundException ignored) {
// Not on the classpath.
log.warning("Jetty extension " + protocolNegoClassName + " not found");
continue;
}
Class<?> providerClass = Class.forName(protocolNegoClassName + "$Provider", true, null);
Class<?> clientProviderClass
= Class.forName(protocolNegoClassName + "$ClientProvider", true, null);
Class<?> serverProviderClass
= Class.forName(protocolNegoClassName + "$ServerProvider", true, null);
Method putMethod = negoClass.getMethod("put", SSLEngine.class, providerClass);
final Method removeMethod = negoClass.getMethod("remove", SSLEngine.class);
putMethod.invoke(null, engine, Proxy.newProxyInstance(
null,
new Class[] {server ? serverProviderClass : clientProviderClass},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if ("supports".equals(methodName)) {
// NPN client
return true;
}
if ("unsupported".equals(methodName)) {
// all
removeMethod.invoke(null, engine);
protocolNegotiated.setException(new RuntimeException(
"Endpoint does not support any of " + SUPPORTED_PROTOCOLS
+ " in ALPN/NPN negotiation"));
return null;
}
if ("protocols".equals(methodName)) {
// ALPN client, NPN server
return SUPPORTED_PROTOCOLS;
}
if ("selected".equals(methodName) || "protocolSelected".equals(methodName)) {
// ALPN client, NPN server
removeMethod.invoke(null, engine);
String protocol = (String) args[0];
if (!SUPPORTED_PROTOCOLS.contains(protocol)) {
RuntimeException e = new RuntimeException(
"Unsupported protocol selected via ALPN/NPN: " + protocol);
protocolNegotiated.setException(e);
if ("selected".equals(methodName)) {
// ALPN client
// Throwing exception causes TLS alert.
throw e;
} else {
return null;
}
}
protocolNegotiated.set(null);
return null;
}
if ("select".equals(methodName) || "selectProtocol".equals(methodName)) {
// ALPN server, NPN client
removeMethod.invoke(null, engine);
@SuppressWarnings("unchecked")
List<String> names = (List<String>) args[0];
for (String name : names) {
if (SUPPORTED_PROTOCOLS.contains(name)) {
protocolNegotiated.set(null);
return name;
}
}
RuntimeException e =
new RuntimeException("Protocol not available via ALPN/NPN: " + names);
protocolNegotiated.setException(e);
if ("select".equals(methodName)) {
// ALPN server
throw e; // Throwing exception causes TLS alert.
}
return null;
}
throw new IllegalStateException("Unknown method " + methodName);
}
}));
return true;
} catch (Exception e) {
log.log(Level.SEVERE,
"Unable to initialize protocol negotation for " + protocolNegoClassName, e);
}
}
return false;
}
}