diff --git a/codec-http2/pom.xml b/codec-http2/pom.xml index f5e9c9cfb6..02914ebeb0 100644 --- a/codec-http2/pom.xml +++ b/codec-http2/pom.xml @@ -48,7 +48,6 @@ org.mockito mockito-all - 1.9.5 test diff --git a/example/src/main/java/io/netty/example/http2/client/Http2Client.java b/example/src/main/java/io/netty/example/http2/client/Http2Client.java index b60f58b8ea..9203e1d0d6 100644 --- a/example/src/main/java/io/netty/example/http2/client/Http2Client.java +++ b/example/src/main/java/io/netty/example/http2/client/Http2Client.java @@ -28,6 +28,7 @@ import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http2.Http2OrHttpChooser.SelectedProtocol; +import io.netty.handler.ssl.JettyAlpnSslEngineWrapper; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.netty.util.CharsetUtil; @@ -55,8 +56,15 @@ public final class Http2Client { final SslContext sslCtx; if (SSL) { sslCtx = SslContext.newClientContext( - null, InsecureTrustManagerFactory.INSTANCE, null, + null, InsecureTrustManagerFactory.INSTANCE, + Arrays.asList("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + // NOTE: Block ciphers are prohibited by the HTTP/2 specification + // http://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-9.2.2. + // The following cipher exists to allow these examples to run with older JREs. + // Please consult the HTTP/2 specification when selecting cipher suites. + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA"), Arrays.asList(SelectedProtocol.HTTP_2.protocolName(), SelectedProtocol.HTTP_1_1.protocolName()), + JettyAlpnSslEngineWrapper.instance(), 0, 0); } else { sslCtx = null; @@ -90,7 +98,8 @@ public final class Http2Client { // Create a simple GET request. FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, URL); request.headers().add(HttpHeaders.Names.HOST, hostName); - request.headers().add(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.GZIP); + // TODO: fix me when HTTP/2 supports decompression + // request.headers().add(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.GZIP); channel.writeAndFlush(request); responseHandler.put(streamId, channel.newPromise()); streamId += 2; @@ -100,7 +109,8 @@ public final class Http2Client { FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, POST, URL2, Unpooled.copiedBuffer(URL2DATA.getBytes(CharsetUtil.UTF_8))); request.headers().add(HttpHeaders.Names.HOST, hostName); - request.headers().add(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.GZIP); + // TODO: fix me when HTTP/2 supports decompression + // request.headers().add(HttpHeaders.Names.ACCEPT_ENCODING, HttpHeaders.Values.GZIP); channel.writeAndFlush(request); responseHandler.put(streamId, channel.newPromise()); streamId += 2; diff --git a/example/src/main/java/io/netty/example/http2/client/HttpResponseHandler.java b/example/src/main/java/io/netty/example/http2/client/HttpResponseHandler.java index cfec8575c3..eb1e9519d5 100644 --- a/example/src/main/java/io/netty/example/http2/client/HttpResponseHandler.java +++ b/example/src/main/java/io/netty/example/http2/client/HttpResponseHandler.java @@ -76,7 +76,13 @@ public class HttpResponseHandler extends SimpleChannelInboundHandler 1) { + SelectedProtocol selectedProtocol = SelectedProtocol.protocol(protocol[1]); + System.err.println("Selected Protocol is " + selectedProtocol); + return selectedProtocol; + } + return SelectedProtocol.UNKNOWN; } @Override diff --git a/example/src/main/java/io/netty/example/http2/server/Http2Server.java b/example/src/main/java/io/netty/example/http2/server/Http2Server.java index 467939c548..4fec25cd67 100644 --- a/example/src/main/java/io/netty/example/http2/server/Http2Server.java +++ b/example/src/main/java/io/netty/example/http2/server/Http2Server.java @@ -25,6 +25,7 @@ import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http2.Http2OrHttpChooser.SelectedProtocol; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.ssl.JettyAlpnSslEngineWrapper; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.util.SelfSignedCertificate; @@ -45,10 +46,17 @@ public final class Http2Server { if (SSL) { SelfSignedCertificate ssc = new SelfSignedCertificate(); sslCtx = SslContext.newServerContext( - ssc.certificate(), ssc.privateKey(), null, null, + ssc.certificate(), ssc.privateKey(), null, + Arrays.asList("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + // NOTE: Block ciphers are prohibited by the HTTP/2 specification + // http://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-9.2.2. + // The following cipher exists to allow these examples to run with older JREs. + // Please consult the HTTP/2 specification when selecting cipher suites. + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA"), Arrays.asList( SelectedProtocol.HTTP_2.protocolName(), SelectedProtocol.HTTP_1_1.protocolName()), + JettyAlpnSslEngineWrapper.instance(), 0, 0); } else { sslCtx = null; diff --git a/example/src/main/java/io/netty/example/memcache/binary/MemcacheClient.java b/example/src/main/java/io/netty/example/memcache/binary/MemcacheClient.java index b6b042958a..95ea49dfe3 100644 --- a/example/src/main/java/io/netty/example/memcache/binary/MemcacheClient.java +++ b/example/src/main/java/io/netty/example/memcache/binary/MemcacheClient.java @@ -27,6 +27,7 @@ import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.http2.Http2OrHttpChooser.SelectedProtocol; import io.netty.handler.codec.memcache.binary.BinaryMemcacheClientCodec; import io.netty.handler.codec.memcache.binary.BinaryMemcacheObjectAggregator; +import io.netty.handler.ssl.JettyAlpnSslEngineWrapper; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; @@ -50,6 +51,7 @@ public final class MemcacheClient { sslCtx = SslContext.newClientContext( null, InsecureTrustManagerFactory.INSTANCE, null, Arrays.asList(SelectedProtocol.HTTP_2.protocolName(), SelectedProtocol.HTTP_1_1.protocolName()), + JettyAlpnSslEngineWrapper.instance(), 0, 0); } else { sslCtx = null; diff --git a/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java b/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java index bccbe9e560..bccdf208d6 100644 --- a/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java +++ b/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java @@ -27,6 +27,7 @@ import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.spdy.SpdyOrHttpChooser.SelectedProtocol; +import io.netty.handler.ssl.JettyNpnSslEngineWrapper; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; @@ -56,6 +57,7 @@ public final class SpdyClient { final SslContext sslCtx = SslContext.newClientContext( null, InsecureTrustManagerFactory.INSTANCE, null, Arrays.asList(SelectedProtocol.SPDY_3_1.protocolName(), SelectedProtocol.HTTP_1_1.protocolName()), + JettyNpnSslEngineWrapper.instance(), 0, 0); HttpResponseClientHandler httpResponseHandler = new HttpResponseClientHandler(); diff --git a/example/src/main/java/io/netty/example/spdy/server/SpdyServer.java b/example/src/main/java/io/netty/example/spdy/server/SpdyServer.java index 991d727a22..05d9c69037 100644 --- a/example/src/main/java/io/netty/example/spdy/server/SpdyServer.java +++ b/example/src/main/java/io/netty/example/spdy/server/SpdyServer.java @@ -24,6 +24,7 @@ import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.spdy.SpdyOrHttpChooser.SelectedProtocol; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.ssl.JettyNpnSslEngineWrapper; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.util.SelfSignedCertificate; @@ -57,6 +58,7 @@ public final class SpdyServer { SslContext sslCtx = SslContext.newServerContext( ssc.certificate(), ssc.privateKey(), null, null, Arrays.asList(SelectedProtocol.SPDY_3_1.protocolName(), SelectedProtocol.HTTP_1_1.protocolName()), + JettyNpnSslEngineWrapper.instance(), 0, 0); // Configure the server. diff --git a/handler/pom.xml b/handler/pom.xml index aa06a88553..3e679f4df9 100644 --- a/handler/pom.xml +++ b/handler/pom.xml @@ -66,6 +66,22 @@ provided true + + org.eclipse.jetty.alpn + alpn-api + true + + + org.mortbay.jetty.alpn + alpn-boot + provided + true + + + org.mockito + mockito-all + test + diff --git a/handler/src/main/java/io/netty/handler/ssl/DefaultSslWrapperFactory.java b/handler/src/main/java/io/netty/handler/ssl/DefaultSslWrapperFactory.java new file mode 100644 index 0000000000..c7db27c58a --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/DefaultSslWrapperFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you 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.netty.handler.ssl; + +import java.util.List; + +import javax.net.ssl.SSLEngine; + +/** + * Factory for not wrapping {@link SSLEngine} object and just returning it + */ +public final class DefaultSslWrapperFactory implements SslEngineWrapperFactory { + private static final DefaultSslWrapperFactory INSTANCE = new DefaultSslWrapperFactory(); + + private DefaultSslWrapperFactory() { + } + + public static DefaultSslWrapperFactory instance() { + return INSTANCE; + } + + @Override + public SSLEngine wrapSslEngine(SSLEngine engine, List protocols, boolean isServer) { + return engine; + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java b/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java index 07a0d1db98..99affaa2cd 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java @@ -19,19 +19,19 @@ package io.netty.handler.ssl; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; +import java.io.File; +import java.security.KeyStore; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.List; + import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSessionContext; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.security.auth.x500.X500Principal; -import java.io.File; -import java.security.KeyStore; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; /** * A client-side {@link SslContext} which uses JDK's SSL/TLS implementation. @@ -45,7 +45,7 @@ public final class JdkSslClientContext extends JdkSslContext { * Creates a new instance. */ public JdkSslClientContext() throws SSLException { - this(null, null, null, null, 0, 0); + this(null, null); } /** @@ -79,7 +79,7 @@ public final class JdkSslClientContext extends JdkSslContext { * {@code null} to use the default. */ public JdkSslClientContext(File certChainFile, TrustManagerFactory trustManagerFactory) throws SSLException { - this(certChainFile, trustManagerFactory, null, null, 0, 0); + this(certChainFile, trustManagerFactory, null, null, DefaultSslWrapperFactory.instance(), 0, 0); } /** @@ -94,6 +94,8 @@ public final class JdkSslClientContext extends JdkSslContext { * {@code null} to use the default cipher suites. * @param nextProtocols the application layer protocols to accept, in the order of preference. * {@code null} to disable TLS NPN/ALPN extension. + * @param wrapperFactory a factory used to wrap the underlying {@link SSLEngine}. + * This is required if {@code nextProtocols} is not {@code null} or empty * @param sessionCacheSize the size of the cache used for storing SSL session objects. * {@code 0} to use the default value. * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. @@ -102,26 +104,12 @@ public final class JdkSslClientContext extends JdkSslContext { public JdkSslClientContext( File certChainFile, TrustManagerFactory trustManagerFactory, Iterable ciphers, Iterable nextProtocols, + SslEngineWrapperFactory wrapperFactory, long sessionCacheSize, long sessionTimeout) throws SSLException { - super(ciphers); + super(ciphers, wrapperFactory); - if (nextProtocols != null && nextProtocols.iterator().hasNext()) { - if (!JettyNpnSslEngine.isAvailable()) { - throw new SSLException("NPN/ALPN unsupported: " + nextProtocols); - } - - List nextProtoList = new ArrayList(); - for (String p: nextProtocols) { - if (p == null) { - break; - } - nextProtoList.add(p); - } - this.nextProtocols = Collections.unmodifiableList(nextProtoList); - } else { - this.nextProtocols = Collections.emptyList(); - } + this.nextProtocols = translateProtocols(nextProtocols); try { if (certChainFile == null) { diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java b/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java index 15402a8c2f..0ce4119a85 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java @@ -20,14 +20,15 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLSessionContext; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSessionContext; + /** * An {@link SslContext} which uses JDK's SSL/TLS implementation. */ @@ -110,10 +111,15 @@ public abstract class JdkSslContext extends SslContext { private final String[] cipherSuites; private final List unmodifiableCipherSuites; + private final SslEngineWrapperFactory wrapperFactory; - JdkSslContext(Iterable ciphers) { + JdkSslContext(Iterable ciphers, SslEngineWrapperFactory wrapperFactory) { + if (wrapperFactory == null) { + throw new NullPointerException("wrapperFactory"); + } cipherSuites = toCipherSuiteArray(ciphers); unmodifiableCipherSuites = Collections.unmodifiableList(Arrays.asList(cipherSuites)); + this.wrapperFactory = wrapperFactory; } /** @@ -166,11 +172,7 @@ public abstract class JdkSslContext extends SslContext { } private SSLEngine wrapEngine(SSLEngine engine) { - if (nextProtocols().isEmpty()) { - return engine; - } else { - return new JettyNpnSslEngine(engine, nextProtocols(), isServer()); - } + return wrapperFactory.wrapSslEngine(engine, nextProtocols(), isServer()); } private static String[] toCipherSuiteArray(Iterable ciphers) { diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java b/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java index 34b08f29c3..955cc16a31 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java @@ -19,16 +19,6 @@ package io.netty.handler.ssl; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; -import javax.crypto.Cipher; -import javax.crypto.EncryptedPrivateKeyInfo; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLSessionContext; import java.io.File; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; @@ -43,9 +33,20 @@ import java.security.cert.CertificateFactory; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.ArrayList; -import java.util.Collections; import java.util.List; +import javax.crypto.Cipher; +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSessionContext; + /** * A server-side {@link SslContext} which uses JDK's SSL/TLS implementation. */ @@ -73,7 +74,7 @@ public final class JdkSslServerContext extends JdkSslContext { * {@code null} if it's not password-protected. */ public JdkSslServerContext(File certChainFile, File keyFile, String keyPassword) throws SSLException { - this(certChainFile, keyFile, keyPassword, null, null, 0, 0); + this(certChainFile, keyFile, keyPassword, null, null, DefaultSslWrapperFactory.instance(), 0, 0); } /** @@ -87,6 +88,8 @@ public final class JdkSslServerContext extends JdkSslContext { * {@code null} to use the default cipher suites. * @param nextProtocols the application layer protocols to accept, in the order of preference. * {@code null} to disable TLS NPN/ALPN extension. + * @param wrapperFactory a factory used to wrap the underlying {@link SSLEngine}. + * This is required if {@code nextProtocols} is not {@code null} or empty * @param sessionCacheSize the size of the cache used for storing SSL session objects. * {@code 0} to use the default value. * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. @@ -95,9 +98,12 @@ public final class JdkSslServerContext extends JdkSslContext { public JdkSslServerContext( File certChainFile, File keyFile, String keyPassword, Iterable ciphers, Iterable nextProtocols, + SslEngineWrapperFactory wrapperFactory, long sessionCacheSize, long sessionTimeout) throws SSLException { - super(ciphers); + super(ciphers, wrapperFactory); + + this.nextProtocols = translateProtocols(nextProtocols); if (certChainFile == null) { throw new NullPointerException("certChainFile"); @@ -110,24 +116,6 @@ public final class JdkSslServerContext extends JdkSslContext { keyPassword = ""; } - if (nextProtocols != null && nextProtocols.iterator().hasNext()) { - if (!JettyNpnSslEngine.isAvailable()) { - throw new SSLException("NPN/ALPN unsupported: " + nextProtocols); - } - - List list = new ArrayList(); - for (String p: nextProtocols) { - if (p == null) { - break; - } - list.add(p); - } - - this.nextProtocols = Collections.unmodifiableList(list); - } else { - this.nextProtocols = Collections.emptyList(); - } - String algorithm = Security.getProperty("ssl.KeyManagerFactory.algorithm"); if (algorithm == null) { algorithm = "SunX509"; diff --git a/handler/src/main/java/io/netty/handler/ssl/JettyAlpnSslEngine.java b/handler/src/main/java/io/netty/handler/ssl/JettyAlpnSslEngine.java new file mode 100644 index 0000000000..7a2b452343 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/JettyAlpnSslEngine.java @@ -0,0 +1,117 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you 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.netty.handler.ssl; + +import java.util.List; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +import org.eclipse.jetty.alpn.ALPN; +import org.eclipse.jetty.alpn.ALPN.ClientProvider; +import org.eclipse.jetty.alpn.ALPN.ServerProvider; + +final class JettyAlpnSslEngine extends JettySslEngine { + private static boolean available; + + static boolean isAvailable() { + updateAvailability(); + return available; + } + + private static void updateAvailability() { + if (available) { + return; + } + + try { + // Try to get the bootstrap class loader. + ClassLoader bootloader = ClassLoader.getSystemClassLoader().getParent(); + if (bootloader == null) { + // If failed, use the system class loader, + // although it's not perfect to tell if APLN extension has been loaded. + bootloader = ClassLoader.getSystemClassLoader(); + } + Class.forName("sun.security.ssl.ALPNExtension", true, bootloader); + available = true; + } catch (Exception ignore) { + // alpn-boot was not loaded. + } + } + + JettyAlpnSslEngine(SSLEngine engine, final List nextProtocols, boolean server) { + super(engine, nextProtocols, server); + + if (server) { + final String[] array = nextProtocols.toArray(new String[nextProtocols.size()]); + final String fallback = array[array.length - 1]; + + ALPN.put(engine, new ServerProvider() { + @Override + public String select(List protocols) { + for (int i = 0; i < array.length; ++i) { + String p = array[i]; + if (protocols.contains(p)) { + session.setApplicationProtocol(p); + return p; + } + } + session.setApplicationProtocol(fallback); + return fallback; + } + + @Override + public void unsupported() { + session.setApplicationProtocol(null); + } + }); + } else { + ALPN.put(engine, new ClientProvider() { + @Override + public List protocols() { + return nextProtocols; + } + + @Override + public void selected(String protocol) { + getSession().setApplicationProtocol(protocol); + } + + @Override + public boolean supports() { + return true; + } + + @Override + public void unsupported() { + getSession().setApplicationProtocol(nextProtocols.get(nextProtocols.size() - 1)); + } + }); + } + } + + @Override + public void closeInbound() throws SSLException { + ALPN.remove(engine); + super.closeInbound(); + } + + @Override + public void closeOutbound() { + ALPN.remove(engine); + super.closeOutbound(); + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/JettyAlpnSslEngineWrapper.java b/handler/src/main/java/io/netty/handler/ssl/JettyAlpnSslEngineWrapper.java new file mode 100644 index 0000000000..a6d7aae7fa --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/JettyAlpnSslEngineWrapper.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you 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.netty.handler.ssl; + +import java.util.List; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +/** + * Factory for wrapping {@link SSLEngine} object in {@link JettyAlpnSslEngine} objects + */ +public final class JettyAlpnSslEngineWrapper implements SslEngineWrapperFactory { + private static JettyAlpnSslEngineWrapper instance; + + private JettyAlpnSslEngineWrapper() throws SSLException { + if (!JettyAlpnSslEngine.isAvailable()) { + throw new SSLException("ALPN unsupported. Is your classpatch configured correctly?" + + " See http://www.eclipse.org/jetty/documentation/current/alpn-chapter.html#alpn-starting"); + } + } + + public static JettyAlpnSslEngineWrapper instance() throws SSLException { + if (instance == null) { + instance = new JettyAlpnSslEngineWrapper(); + } + return instance; + } + + @Override + public SSLEngine wrapSslEngine(SSLEngine engine, List protocols, boolean isServer) { + return new JettyAlpnSslEngine(engine, protocols, isServer); + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/JettyNpnSslEngine.java b/handler/src/main/java/io/netty/handler/ssl/JettyNpnSslEngine.java index 49e9c3dc2d..cee6267bae 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JettyNpnSslEngine.java +++ b/handler/src/main/java/io/netty/handler/ssl/JettyNpnSslEngine.java @@ -16,21 +16,16 @@ package io.netty.handler.ssl; +import java.util.List; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + import org.eclipse.jetty.npn.NextProtoNego; import org.eclipse.jetty.npn.NextProtoNego.ClientProvider; import org.eclipse.jetty.npn.NextProtoNego.ServerProvider; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLEngineResult; -import javax.net.ssl.SSLEngineResult.HandshakeStatus; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLParameters; -import javax.net.ssl.SSLSession; -import java.nio.ByteBuffer; -import java.util.List; - -final class JettyNpnSslEngine extends SSLEngine { - +final class JettyNpnSslEngine extends JettySslEngine { private static boolean available; static boolean isAvailable() { @@ -57,14 +52,8 @@ final class JettyNpnSslEngine extends SSLEngine { } } - private final SSLEngine engine; - private final JettyNpnSslSession session; - JettyNpnSslEngine(SSLEngine engine, final List nextProtocols, boolean server) { - assert !nextProtocols.isEmpty(); - - this.engine = engine; - session = new JettyNpnSslSession(engine); + super(engine, nextProtocols, server); if (server) { NextProtoNego.put(engine, new ServerProvider() { @@ -84,8 +73,8 @@ final class JettyNpnSslEngine extends SSLEngine { } }); } else { - final String[] list = nextProtocols.toArray(new String[nextProtocols.size()]); - final String fallback = list[list.length - 1]; + final String[] array = nextProtocols.toArray(new String[nextProtocols.size()]); + final String fallback = array[array.length - 1]; NextProtoNego.put(engine, new ClientProvider() { @Override @@ -100,181 +89,29 @@ final class JettyNpnSslEngine extends SSLEngine { @Override public String selectProtocol(List protocols) { - for (String p: list) { + for (int i = 0; i < array.length; ++i) { + String p = array[i]; if (protocols.contains(p)) { + session.setApplicationProtocol(p); return p; } } + session.setApplicationProtocol(fallback); return fallback; } }); } } - @Override - public JettyNpnSslSession getSession() { - return session; - } - @Override public void closeInbound() throws SSLException { NextProtoNego.remove(engine); - engine.closeInbound(); + super.closeInbound(); } @Override public void closeOutbound() { NextProtoNego.remove(engine); - engine.closeOutbound(); - } - - @Override - public String getPeerHost() { - return engine.getPeerHost(); - } - - @Override - public int getPeerPort() { - return engine.getPeerPort(); - } - - @Override - public SSLEngineResult wrap(ByteBuffer byteBuffer, ByteBuffer byteBuffer2) throws SSLException { - return engine.wrap(byteBuffer, byteBuffer2); - } - - @Override - public SSLEngineResult wrap(ByteBuffer[] byteBuffers, ByteBuffer byteBuffer) throws SSLException { - return engine.wrap(byteBuffers, byteBuffer); - } - - @Override - public SSLEngineResult wrap(ByteBuffer[] byteBuffers, int i, int i2, ByteBuffer byteBuffer) throws SSLException { - return engine.wrap(byteBuffers, i, i2, byteBuffer); - } - - @Override - public SSLEngineResult unwrap(ByteBuffer byteBuffer, ByteBuffer byteBuffer2) throws SSLException { - return engine.unwrap(byteBuffer, byteBuffer2); - } - - @Override - public SSLEngineResult unwrap(ByteBuffer byteBuffer, ByteBuffer[] byteBuffers) throws SSLException { - return engine.unwrap(byteBuffer, byteBuffers); - } - - @Override - public SSLEngineResult unwrap(ByteBuffer byteBuffer, ByteBuffer[] byteBuffers, int i, int i2) throws SSLException { - return engine.unwrap(byteBuffer, byteBuffers, i, i2); - } - - @Override - public Runnable getDelegatedTask() { - return engine.getDelegatedTask(); - } - - @Override - public boolean isInboundDone() { - return engine.isInboundDone(); - } - - @Override - public boolean isOutboundDone() { - return engine.isOutboundDone(); - } - - @Override - public String[] getSupportedCipherSuites() { - return engine.getSupportedCipherSuites(); - } - - @Override - public String[] getEnabledCipherSuites() { - return engine.getEnabledCipherSuites(); - } - - @Override - public void setEnabledCipherSuites(String[] strings) { - engine.setEnabledCipherSuites(strings); - } - - @Override - public String[] getSupportedProtocols() { - return engine.getSupportedProtocols(); - } - - @Override - public String[] getEnabledProtocols() { - return engine.getEnabledProtocols(); - } - - @Override - public void setEnabledProtocols(String[] strings) { - engine.setEnabledProtocols(strings); - } - - @Override - public SSLSession getHandshakeSession() { - return engine.getHandshakeSession(); - } - - @Override - public void beginHandshake() throws SSLException { - engine.beginHandshake(); - } - - @Override - public HandshakeStatus getHandshakeStatus() { - return engine.getHandshakeStatus(); - } - - @Override - public void setUseClientMode(boolean b) { - engine.setUseClientMode(b); - } - - @Override - public boolean getUseClientMode() { - return engine.getUseClientMode(); - } - - @Override - public void setNeedClientAuth(boolean b) { - engine.setNeedClientAuth(b); - } - - @Override - public boolean getNeedClientAuth() { - return engine.getNeedClientAuth(); - } - - @Override - public void setWantClientAuth(boolean b) { - engine.setWantClientAuth(b); - } - - @Override - public boolean getWantClientAuth() { - return engine.getWantClientAuth(); - } - - @Override - public void setEnableSessionCreation(boolean b) { - engine.setEnableSessionCreation(b); - } - - @Override - public boolean getEnableSessionCreation() { - return engine.getEnableSessionCreation(); - } - - @Override - public SSLParameters getSSLParameters() { - return engine.getSSLParameters(); - } - - @Override - public void setSSLParameters(SSLParameters sslParameters) { - engine.setSSLParameters(sslParameters); + super.closeOutbound(); } } diff --git a/handler/src/main/java/io/netty/handler/ssl/JettyNpnSslEngineWrapper.java b/handler/src/main/java/io/netty/handler/ssl/JettyNpnSslEngineWrapper.java new file mode 100644 index 0000000000..0fa05d91a3 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/JettyNpnSslEngineWrapper.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you 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.netty.handler.ssl; + +import java.util.List; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +/** + * Factory for wrapping {@link SSLEngine} object in {@link JettyNpnSslEngine} objects + */ +public final class JettyNpnSslEngineWrapper implements SslEngineWrapperFactory { + private static JettyNpnSslEngineWrapper instance; + + private JettyNpnSslEngineWrapper() throws SSLException { + if (!JettyNpnSslEngine.isAvailable()) { + throw new SSLException("NPN unsupported. Is your classpatch configured correctly?" + + " See http://www.eclipse.org/jetty/documentation/current/npn-chapter.html#npn-starting"); + } + } + + public static JettyNpnSslEngineWrapper instance() throws SSLException { + if (instance == null) { + instance = new JettyNpnSslEngineWrapper(); + } + return instance; + } + + @Override + public SSLEngine wrapSslEngine(SSLEngine engine, List protocols, boolean isServer) { + return new JettyNpnSslEngine(engine, protocols, isServer); + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/JettySslEngine.java b/handler/src/main/java/io/netty/handler/ssl/JettySslEngine.java new file mode 100644 index 0000000000..a72cf5648b --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/JettySslEngine.java @@ -0,0 +1,208 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you 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.netty.handler.ssl; + +import java.nio.ByteBuffer; +import java.util.List; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; + +class JettySslEngine extends SSLEngine { + protected final SSLEngine engine; + protected final JettySslSession session; + + JettySslEngine(SSLEngine engine, final List nextProtocols, boolean server) { + if (nextProtocols == null) { + throw new NullPointerException("nextProtocols"); + } + if (nextProtocols.isEmpty()) { + throw new IllegalArgumentException("nextProtocols can not be empty"); + } + + this.engine = engine; + session = new JettySslSession(engine); + } + + @Override + public JettySslSession getSession() { + return session; + } + + @Override + public void closeInbound() throws SSLException { + engine.closeInbound(); + } + + @Override + public void closeOutbound() { + engine.closeOutbound(); + } + + @Override + public String getPeerHost() { + return engine.getPeerHost(); + } + + @Override + public int getPeerPort() { + return engine.getPeerPort(); + } + + @Override + public SSLEngineResult wrap(ByteBuffer byteBuffer, ByteBuffer byteBuffer2) throws SSLException { + return engine.wrap(byteBuffer, byteBuffer2); + } + + @Override + public SSLEngineResult wrap(ByteBuffer[] byteBuffers, ByteBuffer byteBuffer) throws SSLException { + return engine.wrap(byteBuffers, byteBuffer); + } + + @Override + public SSLEngineResult wrap(ByteBuffer[] byteBuffers, int i, int i2, ByteBuffer byteBuffer) throws SSLException { + return engine.wrap(byteBuffers, i, i2, byteBuffer); + } + + @Override + public SSLEngineResult unwrap(ByteBuffer byteBuffer, ByteBuffer byteBuffer2) throws SSLException { + return engine.unwrap(byteBuffer, byteBuffer2); + } + + @Override + public SSLEngineResult unwrap(ByteBuffer byteBuffer, ByteBuffer[] byteBuffers) throws SSLException { + return engine.unwrap(byteBuffer, byteBuffers); + } + + @Override + public SSLEngineResult unwrap(ByteBuffer byteBuffer, ByteBuffer[] byteBuffers, int i, int i2) throws SSLException { + return engine.unwrap(byteBuffer, byteBuffers, i, i2); + } + + @Override + public Runnable getDelegatedTask() { + return engine.getDelegatedTask(); + } + + @Override + public boolean isInboundDone() { + return engine.isInboundDone(); + } + + @Override + public boolean isOutboundDone() { + return engine.isOutboundDone(); + } + + @Override + public String[] getSupportedCipherSuites() { + return engine.getSupportedCipherSuites(); + } + + @Override + public String[] getEnabledCipherSuites() { + return engine.getEnabledCipherSuites(); + } + + @Override + public void setEnabledCipherSuites(String[] strings) { + engine.setEnabledCipherSuites(strings); + } + + @Override + public String[] getSupportedProtocols() { + return engine.getSupportedProtocols(); + } + + @Override + public String[] getEnabledProtocols() { + return engine.getEnabledProtocols(); + } + + @Override + public void setEnabledProtocols(String[] strings) { + engine.setEnabledProtocols(strings); + } + + @Override + public SSLSession getHandshakeSession() { + return engine.getHandshakeSession(); + } + + @Override + public void beginHandshake() throws SSLException { + engine.beginHandshake(); + } + + @Override + public HandshakeStatus getHandshakeStatus() { + return engine.getHandshakeStatus(); + } + + @Override + public void setUseClientMode(boolean b) { + engine.setUseClientMode(b); + } + + @Override + public boolean getUseClientMode() { + return engine.getUseClientMode(); + } + + @Override + public void setNeedClientAuth(boolean b) { + engine.setNeedClientAuth(b); + } + + @Override + public boolean getNeedClientAuth() { + return engine.getNeedClientAuth(); + } + + @Override + public void setWantClientAuth(boolean b) { + engine.setWantClientAuth(b); + } + + @Override + public boolean getWantClientAuth() { + return engine.getWantClientAuth(); + } + + @Override + public void setEnableSessionCreation(boolean b) { + engine.setEnableSessionCreation(b); + } + + @Override + public boolean getEnableSessionCreation() { + return engine.getEnableSessionCreation(); + } + + @Override + public SSLParameters getSSLParameters() { + return engine.getSSLParameters(); + } + + @Override + public void setSSLParameters(SSLParameters sslParameters) { + engine.setSSLParameters(sslParameters); + } +} diff --git a/handler/src/main/java/io/netty/handler/ssl/JettyNpnSslSession.java b/handler/src/main/java/io/netty/handler/ssl/JettySslSession.java similarity index 97% rename from handler/src/main/java/io/netty/handler/ssl/JettyNpnSslSession.java rename to handler/src/main/java/io/netty/handler/ssl/JettySslSession.java index f4da3da1d5..845b1be6a0 100644 --- a/handler/src/main/java/io/netty/handler/ssl/JettyNpnSslSession.java +++ b/handler/src/main/java/io/netty/handler/ssl/JettySslSession.java @@ -24,12 +24,11 @@ import javax.security.cert.X509Certificate; import java.security.Principal; import java.security.cert.Certificate; -final class JettyNpnSslSession implements SSLSession { - +final class JettySslSession implements SSLSession { private final SSLEngine engine; private volatile String applicationProtocol; - JettyNpnSslSession(SSLEngine engine) { + JettySslSession(SSLEngine engine) { this.engine = engine; } diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java index a62c6d7b2d..f99e192395 100644 --- a/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java @@ -65,7 +65,7 @@ public final class OpenSslServerContext extends SslContext { private final List unmodifiableCiphers = Collections.unmodifiableList(ciphers); private final long sessionCacheSize; private final long sessionTimeout; - private final List nextProtocols; + private final List protocols; /** The OpenSSL SSL_CTX object */ private final long ctx; @@ -135,9 +135,8 @@ public final class OpenSslServerContext extends SslContext { if (keyPassword == null) { keyPassword = ""; } - if (nextProtocols == null) { - nextProtocols = Collections.emptyList(); - } + + protocols = translateProtocols(nextProtocols); for (String c: ciphers) { if (c == null) { @@ -146,15 +145,6 @@ public final class OpenSslServerContext extends SslContext { this.ciphers.add(c); } - List nextProtoList = new ArrayList(); - for (String p: nextProtocols) { - if (p == null) { - break; - } - nextProtoList.add(p); - } - this.nextProtocols = Collections.unmodifiableList(nextProtoList); - // Allocate a new APR pool. aprPool = Pool.create(0); @@ -218,11 +208,11 @@ public final class OpenSslServerContext extends SslContext { } /* Set next protocols for next protocol negotiation extension, if specified */ - if (!nextProtoList.isEmpty()) { + if (!protocols.isEmpty()) { // Convert the protocol list into a comma-separated string. StringBuilder nextProtocolBuf = new StringBuilder(); - for (String p: nextProtoList) { - nextProtocolBuf.append(p); + for (int i = 0; i < protocols.size(); ++i) { + nextProtocolBuf.append(protocols.get(i)); nextProtocolBuf.append(','); } nextProtocolBuf.setLength(nextProtocolBuf.length() - 1); @@ -284,7 +274,7 @@ public final class OpenSslServerContext extends SslContext { @Override public List nextProtocols() { - return nextProtocols; + return protocols; } /** @@ -306,10 +296,10 @@ public final class OpenSslServerContext extends SslContext { */ @Override public SSLEngine newEngine(ByteBufAllocator alloc) { - if (nextProtocols.isEmpty()) { + if (protocols.isEmpty()) { return new OpenSslEngine(ctx, alloc, null); } else { - return new OpenSslEngine(ctx, alloc, nextProtocols.get(nextProtocols.size() - 1)); + return new OpenSslEngine(ctx, alloc, protocols.get(protocols.size() - 1)); } } diff --git a/handler/src/main/java/io/netty/handler/ssl/SslContext.java b/handler/src/main/java/io/netty/handler/ssl/SslContext.java index fde09e8d8d..7b04827fd7 100644 --- a/handler/src/main/java/io/netty/handler/ssl/SslContext.java +++ b/handler/src/main/java/io/netty/handler/ssl/SslContext.java @@ -25,7 +25,11 @@ import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; + import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; import java.util.List; /** @@ -51,7 +55,6 @@ import java.util.List; * */ public abstract class SslContext { - /** * Returns the default server-side implementation provider currently in use. * @@ -74,6 +77,28 @@ public abstract class SslContext { return SslProvider.JDK; } + /** + * Translate an {@link Iterable} of protocols to an unmodifiable {@link List} + * @param protocols Protocols to translate + * @return An unmodifiable {@link List} of protocols + */ + public static List translateProtocols(Iterable protocols) { + Iterator itr = protocols == null ? null : protocols.iterator(); + if (itr != null) { + List nextProtoList = new ArrayList(4); + while (itr.hasNext()) { + String p = itr.next(); + if (p == null) { + break; + } + nextProtoList.add(p); + } + return Collections.unmodifiableList(nextProtoList); + } else { + return Collections.emptyList(); + } + } + /** * Creates a new server-side {@link SslContext}. * @@ -82,7 +107,7 @@ public abstract class SslContext { * @return a new server-side {@link SslContext} */ public static SslContext newServerContext(File certChainFile, File keyFile) throws SSLException { - return newServerContext(null, certChainFile, keyFile, null, null, null, 0, 0); + return newServerContext(certChainFile, keyFile, null); } /** @@ -96,7 +121,7 @@ public abstract class SslContext { */ public static SslContext newServerContext( File certChainFile, File keyFile, String keyPassword) throws SSLException { - return newServerContext(null, certChainFile, keyFile, keyPassword, null, null, 0, 0); + return newServerContext(null, certChainFile, keyFile, keyPassword); } /** @@ -110,6 +135,9 @@ public abstract class SslContext { * {@code null} to use the default cipher suites. * @param nextProtocols the application layer protocols to accept, in the order of preference. * {@code null} to disable TLS NPN/ALPN extension. + * @param wrapperFactory a factory used to wrap the underlying {@link SSLEngine}. + * This is required if {@code nextProtocols} is not {@code null} or empty + * and if OpenSSL is available * @param sessionCacheSize the size of the cache used for storing SSL session objects. * {@code 0} to use the default value. * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. @@ -119,10 +147,11 @@ public abstract class SslContext { public static SslContext newServerContext( File certChainFile, File keyFile, String keyPassword, Iterable ciphers, Iterable nextProtocols, + SslEngineWrapperFactory wrapperFactory, long sessionCacheSize, long sessionTimeout) throws SSLException { return newServerContext( null, certChainFile, keyFile, keyPassword, - ciphers, nextProtocols, sessionCacheSize, sessionTimeout); + ciphers, nextProtocols, wrapperFactory, sessionCacheSize, sessionTimeout); } /** @@ -136,7 +165,7 @@ public abstract class SslContext { */ public static SslContext newServerContext( SslProvider provider, File certChainFile, File keyFile) throws SSLException { - return newServerContext(provider, certChainFile, keyFile, null, null, null, 0, 0); + return newServerContext(provider, certChainFile, keyFile, null); } /** @@ -152,7 +181,8 @@ public abstract class SslContext { */ public static SslContext newServerContext( SslProvider provider, File certChainFile, File keyFile, String keyPassword) throws SSLException { - return newServerContext(provider, certChainFile, keyFile, keyPassword, null, null, 0, 0); + return newServerContext(provider, certChainFile, keyFile, keyPassword, null, null, + DefaultSslWrapperFactory.instance(), 0, 0); } /** @@ -168,6 +198,9 @@ public abstract class SslContext { * {@code null} to use the default cipher suites. * @param nextProtocols the application layer protocols to accept, in the order of preference. * {@code null} to disable TLS NPN/ALPN extension. + * @param wrapperFactory a factory used to wrap the underlying {@link SSLEngine}. + * This is required if {@code nextProtocols} is not {@code null} or empty + * and if the {@code provider} is {@link SslProvider#JDK} * @param sessionCacheSize the size of the cache used for storing SSL session objects. * {@code 0} to use the default value. * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. @@ -178,6 +211,7 @@ public abstract class SslContext { SslProvider provider, File certChainFile, File keyFile, String keyPassword, Iterable ciphers, Iterable nextProtocols, + SslEngineWrapperFactory wrapperFactory, long sessionCacheSize, long sessionTimeout) throws SSLException { if (provider == null) { @@ -188,7 +222,7 @@ public abstract class SslContext { case JDK: return new JdkSslServerContext( certChainFile, keyFile, keyPassword, - ciphers, nextProtocols, sessionCacheSize, sessionTimeout); + ciphers, nextProtocols, wrapperFactory, sessionCacheSize, sessionTimeout); case OPENSSL: return new OpenSslServerContext( certChainFile, keyFile, keyPassword, @@ -204,7 +238,7 @@ public abstract class SslContext { * @return a new client-side {@link SslContext} */ public static SslContext newClientContext() throws SSLException { - return newClientContext(null, null, null, null, null, 0, 0); + return newClientContext(null, null, null); } /** @@ -215,7 +249,7 @@ public abstract class SslContext { * @return a new client-side {@link SslContext} */ public static SslContext newClientContext(File certChainFile) throws SSLException { - return newClientContext(null, certChainFile, null, null, null, 0, 0); + return newClientContext(null, certChainFile); } /** @@ -228,7 +262,7 @@ public abstract class SslContext { * @return a new client-side {@link SslContext} */ public static SslContext newClientContext(TrustManagerFactory trustManagerFactory) throws SSLException { - return newClientContext(null, null, trustManagerFactory, null, null, 0, 0); + return newClientContext(null, null, trustManagerFactory); } /** @@ -244,7 +278,7 @@ public abstract class SslContext { */ public static SslContext newClientContext( File certChainFile, TrustManagerFactory trustManagerFactory) throws SSLException { - return newClientContext(null, certChainFile, trustManagerFactory, null, null, 0, 0); + return newClientContext(null, certChainFile, trustManagerFactory); } /** @@ -259,6 +293,9 @@ public abstract class SslContext { * {@code null} to use the default cipher suites. * @param nextProtocols the application layer protocols to accept, in the order of preference. * {@code null} to disable TLS NPN/ALPN extension. + * @param wrapperFactory a factory used to wrap the underlying {@link SSLEngine}. + * This is required if {@code nextProtocols} is not {@code null} or empty + * and if OpenSSL is available * @param sessionCacheSize the size of the cache used for storing SSL session objects. * {@code 0} to use the default value. * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. @@ -269,10 +306,11 @@ public abstract class SslContext { public static SslContext newClientContext( File certChainFile, TrustManagerFactory trustManagerFactory, Iterable ciphers, Iterable nextProtocols, + SslEngineWrapperFactory wrapperFactory, long sessionCacheSize, long sessionTimeout) throws SSLException { return newClientContext( null, certChainFile, trustManagerFactory, - ciphers, nextProtocols, sessionCacheSize, sessionTimeout); + ciphers, nextProtocols, wrapperFactory, sessionCacheSize, sessionTimeout); } /** @@ -284,7 +322,7 @@ public abstract class SslContext { * @return a new client-side {@link SslContext} */ public static SslContext newClientContext(SslProvider provider) throws SSLException { - return newClientContext(provider, null, null, null, null, 0, 0); + return newClientContext(provider, null, null); } /** @@ -298,7 +336,7 @@ public abstract class SslContext { * @return a new client-side {@link SslContext} */ public static SslContext newClientContext(SslProvider provider, File certChainFile) throws SSLException { - return newClientContext(provider, certChainFile, null, null, null, 0, 0); + return newClientContext(provider, certChainFile, null); } /** @@ -314,7 +352,7 @@ public abstract class SslContext { */ public static SslContext newClientContext( SslProvider provider, TrustManagerFactory trustManagerFactory) throws SSLException { - return newClientContext(provider, null, trustManagerFactory, null, null, 0, 0); + return newClientContext(provider, null, trustManagerFactory); } /** @@ -332,7 +370,8 @@ public abstract class SslContext { */ public static SslContext newClientContext( SslProvider provider, File certChainFile, TrustManagerFactory trustManagerFactory) throws SSLException { - return newClientContext(provider, certChainFile, trustManagerFactory, null, null, 0, 0); + return newClientContext(provider, certChainFile, trustManagerFactory, null, null, + DefaultSslWrapperFactory.instance(), 0, 0); } /** @@ -349,6 +388,9 @@ public abstract class SslContext { * {@code null} to use the default cipher suites. * @param nextProtocols the application layer protocols to accept, in the order of preference. * {@code null} to disable TLS NPN/ALPN extension. + * @param wrapperFactory a factory used to wrap the underlying {@link SSLEngine}. + * This is required if {@code nextProtocols} is not {@code null} or empty + * and if the {@code provider} is {@link SslProvider#JDK} * @param sessionCacheSize the size of the cache used for storing SSL session objects. * {@code 0} to use the default value. * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. @@ -360,6 +402,7 @@ public abstract class SslContext { SslProvider provider, File certChainFile, TrustManagerFactory trustManagerFactory, Iterable ciphers, Iterable nextProtocols, + SslEngineWrapperFactory wrapperFactory, long sessionCacheSize, long sessionTimeout) throws SSLException { if (provider != null && provider != SslProvider.JDK) { @@ -368,7 +411,7 @@ public abstract class SslContext { return new JdkSslClientContext( certChainFile, trustManagerFactory, - ciphers, nextProtocols, sessionCacheSize, sessionTimeout); + ciphers, nextProtocols, wrapperFactory, sessionCacheSize, sessionTimeout); } SslContext() { } diff --git a/handler/src/main/java/io/netty/handler/ssl/SslEngineWrapperFactory.java b/handler/src/main/java/io/netty/handler/ssl/SslEngineWrapperFactory.java new file mode 100644 index 0000000000..8c413693fc --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ssl/SslEngineWrapperFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you 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.netty.handler.ssl; + +import java.util.List; + +import javax.net.ssl.SSLEngine; + +/** + * Abstract factory pattern for wrapping an {@link SSLEngine} object. + * This is useful for NPN/APLN support. + */ +public interface SslEngineWrapperFactory { + /** + * Abstract factory pattern for wrapping an {@link SSLEngine} object. + * This is useful for NPN/APLN support. + * + * @param engine The engine to wrap + * @param protocols The application level protocols that are supported + * @param isServer + *
    + *
  • {@code true} if the engine is for server side of connections
  • + *
  • {@code false} if the engine is for client side of connections
  • + *
+ * @return The resulting wrapped engine. This may just be {@code engine} + */ + SSLEngine wrapSslEngine(SSLEngine engine, List protocols, boolean isServer); +} diff --git a/handler/src/test/java/io/netty/handler/ssl/JettySslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/JettySslEngineTest.java new file mode 100644 index 0000000000..149ec676a0 --- /dev/null +++ b/handler/src/test/java/io/netty/handler/ssl/JettySslEngineTest.java @@ -0,0 +1,211 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you 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.netty.handler.ssl; + +import static org.junit.Assume.assumeNoException; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verify; +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.util.NetUtil; + +import java.net.InetSocketAddress; +import java.security.cert.CertificateException; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLException; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class JettySslEngineTest { + private static final String APPLICATION_LEVEL_PROTOCOL = "my-protocol"; + + @Mock + private MessageReciever serverReceiver; + @Mock + private MessageReciever clientReceiver; + + private SslContext serverSslCtx; + private SslContext clientSslCtx; + private ServerBootstrap sb; + private Bootstrap cb; + private Channel serverChannel; + private Channel serverConnectedChannel; + private Channel clientChannel; + private CountDownLatch serverLatch; + private CountDownLatch clientLatch; + + private interface MessageReciever { + void messageReceived(ByteBuf msg); + } + + private final class MessageDelegatorChannelHandler extends SimpleChannelInboundHandler { + private final MessageReciever receiver; + + public MessageDelegatorChannelHandler(MessageReciever receiver) { + super(false); + this.receiver = receiver; + } + + @Override + protected void messageReceived(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { + receiver.messageReceived(msg); + } + } + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + serverLatch = new CountDownLatch(1); + clientLatch = new CountDownLatch(1); + } + + @After + public void tearDown() throws InterruptedException { + if (serverChannel != null) { + serverChannel.close().sync(); + sb.group().shutdownGracefully(); + cb.group().shutdownGracefully(); + } + clientChannel = null; + serverChannel = null; + serverConnectedChannel = null; + } + + @Test + public void testNpn() throws Exception { + SslEngineWrapperFactory wrapper = null; + try { + wrapper = JettyNpnSslEngineWrapper.instance(); + } catch (SSLException e) { + // NPN availability is dependent on the java version. If NPN is not available because of + // java version incompatibility don't fail the test, but instead just skip the test + assumeNoException(e); + } + mySetup(wrapper); + runTest(); + } + + @Test + public void testAlpn() throws Exception { + SslEngineWrapperFactory wrapper = null; + try { + wrapper = JettyAlpnSslEngineWrapper.instance(); + } catch (SSLException e) { + // ALPN availability is dependent on the java version. If NPN is not available because of + // java version incompatibility don't fail the test, but instead just skip the test + assumeNoException(e); + } + mySetup(wrapper); + runTest(); + } + + private void mySetup(SslEngineWrapperFactory wrapper) throws InterruptedException, SSLException, + CertificateException { + SelfSignedCertificate ssc = new SelfSignedCertificate(); + serverSslCtx = SslContext.newServerContext(SslProvider.JDK, ssc.certificate(), ssc.privateKey(), null, null, + Arrays.asList(APPLICATION_LEVEL_PROTOCOL), wrapper, 0, 0); + clientSslCtx = SslContext.newClientContext(SslProvider.JDK, null, InsecureTrustManagerFactory.INSTANCE, null, + Arrays.asList(APPLICATION_LEVEL_PROTOCOL), wrapper, 0, 0); + + serverConnectedChannel = null; + sb = new ServerBootstrap(); + cb = new Bootstrap(); + + sb.group(new NioEventLoopGroup(), new NioEventLoopGroup()); + sb.channel(NioServerSocketChannel.class); + sb.childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast(serverSslCtx.newHandler(ch.alloc())); + p.addLast(new MessageDelegatorChannelHandler(serverReceiver)); + serverConnectedChannel = ch; + } + }); + + cb.group(new NioEventLoopGroup()); + cb.channel(NioSocketChannel.class); + cb.handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast(clientSslCtx.newHandler(ch.alloc())); + p.addLast(new MessageDelegatorChannelHandler(clientReceiver)); + } + }); + + serverChannel = sb.bind(new InetSocketAddress(0)).sync().channel(); + int port = ((InetSocketAddress) serverChannel.localAddress()).getPort(); + + ChannelFuture ccf = cb.connect(new InetSocketAddress(NetUtil.LOCALHOST, port)); + assertTrue(ccf.awaitUninterruptibly().isSuccess()); + clientChannel = ccf.channel(); + } + + private void runTest() throws Exception { + ByteBuf clientMessage = Unpooled.copiedBuffer("I am a client".getBytes()); + ByteBuf serverMessage = Unpooled.copiedBuffer("I am a server".getBytes()); + try { + writeAndVerifyReceived(clientMessage.retain(), clientChannel, serverLatch, serverReceiver); + writeAndVerifyReceived(serverMessage.retain(), serverConnectedChannel, clientLatch, clientReceiver); + verifyApplicationLevelProtocol(clientChannel); + verifyApplicationLevelProtocol(serverConnectedChannel); + } finally { + clientMessage.release(); + serverMessage.release(); + } + } + + private void verifyApplicationLevelProtocol(Channel channel) { + SslHandler handler = channel.pipeline().get(SslHandler.class); + assertNotNull(handler); + String[] protocol = handler.engine().getSession().getProtocol().split(":"); + assertNotNull(protocol); + assertTrue("protocol.length must be greater than 1 but is " + protocol.length, protocol.length > 1); + assertEquals(APPLICATION_LEVEL_PROTOCOL, protocol[1]); + } + + private static void writeAndVerifyReceived(ByteBuf message, Channel sendChannel, CountDownLatch receiverLatch, + MessageReciever receiver) throws Exception { + sendChannel.writeAndFlush(message); + receiverLatch.await(2, TimeUnit.SECONDS); + message.resetReaderIndex(); + verify(receiver).messageReceived(eq(message)); + } +} diff --git a/pom.xml b/pom.xml index f297c8fee5..fbded08ed1 100644 --- a/pom.xml +++ b/pom.xml @@ -109,7 +109,8 @@ false - -D_ + 8.0.0.v20140317 + -Xbootclasspath/p:${jetty.alpn.path} @@ -172,8 +173,9 @@ npn-7u9 @@ -272,7 +274,7 @@ - npn-7u40 + npn-alpn-7u40 java.version @@ -281,10 +283,12 @@ 1.1.6.v20130911 + 7.0.0.v20140317 + - npn-7u45 + npn-alpn-7u45 java.version @@ -293,10 +297,12 @@ 1.1.6.v20130911 + 7.0.0.v20140317 + - npn-7u51 + npn-alpn-7u51 java.version @@ -305,6 +311,81 @@ 1.1.6.v20130911 + 7.0.0.v20140317 + + + + + npn-alpn-7u55 + + + java.version + 1.7.0_55 + + + + 1.1.7.v20140316 + 7.0.0.v20140317 + + + + + npn-alpn-7u60 + + + java.version + 1.7.0_60 + + + + 1.1.7.v20140316 + 7.0.0.v20140317 + + + + + npn-alpn-7u65 + + + java.version + 1.7.0_65 + + + + 1.1.7.v20140316 + 7.0.0.v20140317 + + + + + npn-alpn-7u67 + + + java.version + 1.7.0_67 + + + + 1.1.7.v20140316 + 7.0.0.v20140317 + + + + + + forcenpn + + + forcenpn + true + + + + -Xbootclasspath/p:${jetty.npn.path} @@ -315,6 +396,8 @@ 1.3.18.GA 1.1.7.v20140316 ${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${jetty.npn.version}/npn-boot-${jetty.npn.version}.jar + 8.0.0.v20140317 + ${settings.localRepository}/org/mortbay/jetty/alpn/alpn-boot/${jetty.alpn.version}/alpn-boot-${jetty.alpn.version}.jar -server -dsa -da -ea:io.netty... @@ -325,7 +408,8 @@ -XX:+OptimizeStringConcat -XX:+HeapDumpOnOutOfMemoryError - -Xbootclasspath/p:${jetty.npn.path} + + -Xbootclasspath/p:${jetty.alpn.path} -verbose:gc -D_ @@ -385,6 +469,16 @@ npn-boot ${jetty.npn.version} + + org.eclipse.jetty.alpn + alpn-api + 1.0.0 + + + org.mortbay.jetty.alpn + alpn-boot + ${jetty.alpn.version} + @@ -665,6 +759,14 @@ org.codehaus.mojo animal-sniffer-maven-plugin 1.9 + + + + org.ow2.asm + asm-all + 5.0.3 + + org.codehaus.mojo.signature @@ -754,6 +856,18 @@ ${jetty.npn.version} + + get-alpn-boot + validate + + get + + + org.mortbay.jetty.alpn + alpn-boot + ${jetty.alpn.version} + + @@ -791,7 +905,7 @@ ${project.groupId}.* - sun.misc.*;resolution:=optional,sun.nio.ch;resolution:=optional,sun.security.*;resolution:=optional,* + sun.misc.*;resolution:=optional,sun.nio.ch;resolution:=optional,sun.security.*;resolution:=optional,org.eclipse.jetty.alpn;version="[1,)";resolution=optional,* !* diff --git a/run-example.sh b/run-example.sh index 3027bab000..a0cda4780c 100755 --- a/run-example.sh +++ b/run-example.sh @@ -40,9 +40,15 @@ EXAMPLE_MAP=( 'localecho:io.netty.example.localecho.LocalEcho' ) +NEEDS_NPN_MAP=( + 'spdy-client' + 'spdy-server' +) + EXAMPLE='' EXAMPLE_CLASS='' EXAMPLE_ARGS='-D_' +FORCE_NPN='' I=0 while [[ $# -gt 0 ]]; do @@ -92,7 +98,13 @@ if [[ -z "$EXAMPLE" ]] || [[ -z "$EXAMPLE_CLASS" ]] || [[ $# -ne 0 ]]; then exit 1 fi +for E in "${NEEDS_NPN_MAP[@]}"; do + if [[ "$EXAMPLE" = "$E" ]]; then + FORCE_NPN='true' + break + fi +done + cd "`dirname "$0"`"/example echo "[INFO] Running: $EXAMPLE ($EXAMPLE_CLASS $EXAMPLE_ARGS)" -exec mvn -q -nsu compile exec:exec -Dcheckstyle.skip=true -DargLine.example="$EXAMPLE_ARGS" -DexampleClass="$EXAMPLE_CLASS" - +exec mvn -q -nsu compile exec:exec -Dcheckstyle.skip=true -Dforcenpn="$FORCE_NPN" -DargLine.example="$EXAMPLE_ARGS" -DexampleClass="$EXAMPLE_CLASS"