diff --git a/core/src/main/java/com/google/net/stubby/http2/netty/Http2Server.java b/core/src/main/java/com/google/net/stubby/http2/netty/Http2Server.java index 7ccc89fdda..be5dae25b7 100644 --- a/core/src/main/java/com/google/net/stubby/http2/netty/Http2Server.java +++ b/core/src/main/java/com/google/net/stubby/http2/netty/Http2Server.java @@ -1,5 +1,7 @@ package com.google.net.stubby.http2.netty; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.SettableFuture; import com.google.net.stubby.RequestRegistry; import com.google.net.stubby.Session; @@ -13,20 +15,58 @@ import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http2.Http2OrHttpChooser; +import io.netty.handler.ssl.SslContext; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.annotation.Nullable; +import javax.net.ssl.SSLEngine; /** * Simple server connection startup that attaches a {@link Session} implementation to a connection. */ public class Http2Server implements Runnable { + + // 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"}; + + public static final String HTTP_VERSION_NAME = + Http2OrHttpChooser.SelectedProtocol.HTTP_2.protocolName(); + + private static final Logger log = Logger.getLogger(Http2Server.class.getName()); + private final int port; private final Session session; private final RequestRegistry operations; private Channel channel; + private final SslContext sslContext; + private SettableFuture tlsNegotiatedHttp2; + public Http2Server(int port, Session session, RequestRegistry operations) { + this(port, session, operations, null); + } + + public Http2Server(int port, Session session, RequestRegistry operations, + @Nullable SslContext sslContext) { this.port = port; this.session = session; this.operations = operations; + this.sslContext = sslContext; + this.tlsNegotiatedHttp2 = null; + if (sslContext != null) { + tlsNegotiatedHttp2 = SettableFuture.create(); + if (!installJettyTLSProtocolSelection(sslContext.newEngine(null), tlsNegotiatedHttp2)) { + throw new IllegalStateException("NPN/ALPN extensions not installed"); + } + } } @Override @@ -41,6 +81,9 @@ public class Http2Server implements Runnable { .childHandler(new ChannelInitializer() { // (4) @Override public void initChannel(SocketChannel ch) throws Exception { + if (sslContext != null) { + ch.pipeline().addLast(sslContext.newHandler(ch.alloc())); + } ch.pipeline().addLast(new Http2Codec(session, operations)); } }).option(ChannelOption.SO_BACKLOG, 128) // (5) @@ -65,4 +108,78 @@ public class Http2Server implements Runnable { channel.close().get(); } } + + /** + * 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 protocolNegotiated) { + for (String protocolNegoClassName : JETTY_TLS_NEGOTIATION_IMPL) { + try { + Class negoClass; + try { + negoClass = Class.forName(protocolNegoClassName); + } catch (ClassNotFoundException ignored) { + // Not on the classpath. + log.warning("Jetty extension " + protocolNegoClassName + " not found"); + continue; + } + Class providerClass = Class.forName(protocolNegoClassName + "$Provider"); + Class serverProviderClass = Class.forName(protocolNegoClassName + "$ServerProvider"); + Method putMethod = negoClass.getMethod("put", SSLEngine.class, providerClass); + final Method removeMethod = negoClass.getMethod("remove", SSLEngine.class); + putMethod.invoke(null, engine, Proxy.newProxyInstance( + Http2Server.class.getClassLoader(), new Class[] {serverProviderClass}, + new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String methodName = method.getName(); + if ("unsupported".equals(methodName)) { + // both + log.warning("Calling unsupported"); + removeMethod.invoke(null, engine); + protocolNegotiated.setException(new IllegalStateException( + "ALPN/NPN protocol " + HTTP_VERSION_NAME + " not supported by server")); + return null; + } + if ("protocols".equals(methodName)) { + // NPN only + return ImmutableList.of(HTTP_VERSION_NAME); + } + if ("protocolSelected".equals(methodName)) { + // NPN only + // Only 'supports' one protocol so we know what was selected. + removeMethod.invoke(null, engine); + protocolNegotiated.set(null); + return null; + } + if ("select".equals(methodName)) { + // ALPN only + log.warning("Calling select"); + @SuppressWarnings("unchecked") + List names = (List) args[0]; + for (String name : names) { + if (name.startsWith(HTTP_VERSION_NAME)) { + protocolNegotiated.set(null); + return name; + } + } + protocolNegotiated.setException( + new IllegalStateException("Protocol not available via ALPN/NPN: " + names)); + removeMethod.invoke(null, engine); + 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; + } }