ALPN java implementation

Motivation:

Netty only supports a java NPN implementation provided by npn-api and npn-boot.
There is no java implementation for ALPN.
ALPN is needed to be compliant with the HTTP/2 spec.

Modifications:
-SslContext and JdkSslContext to support ALPN
-JettyNpn* class restructure for NPN and ALPN common aspects
-Pull in alpn-api and alpn-boot optional dependencies for ALPN java implementation

Result:

-Netty provides access to a java implementation of APLN
This commit is contained in:
Scott Mitchell 2014-08-30 00:50:01 -04:00
parent 0d549706f1
commit 5f232b2220
25 changed files with 1032 additions and 301 deletions

View File

@ -48,7 +48,6 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@ -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;

View File

@ -76,7 +76,13 @@ public class HttpResponseHandler extends SimpleChannelInboundHandler<FullHttpRes
@Override
protected void messageReceived(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception {
int streamId = Integer.parseInt(msg.headers().get(HttpUtil.ExtensionHeaders.Names.STREAM_ID));
String streamIdText = msg.headers().get(HttpUtil.ExtensionHeaders.Names.STREAM_ID);
if (streamIdText == null) {
System.err.println("HttpResponseHandler unexpected message received: " + msg);
return;
}
int streamId = Integer.parseInt(streamIdText);
ChannelPromise promise = streamidPromiseMap.get(streamId);
if (promise == null) {
System.err.println("Message received for unknown stream id " + streamId);

View File

@ -37,10 +37,13 @@ public class Http2OrHttpHandler extends Http2OrHttpChooser {
@Override
protected SelectedProtocol getProtocol(SSLEngine engine) {
String[] protocol = engine.getSession().getProtocol().split(":");
if (protocol != null && protocol.length > 1) {
SelectedProtocol selectedProtocol = SelectedProtocol.protocol(protocol[1]);
System.err.println("Selected Protocol is " + selectedProtocol);
return selectedProtocol;
}
return SelectedProtocol.UNKNOWN;
}
@Override
protected ChannelHandler createHttp1RequestHandler() {

View File

@ -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;

View File

@ -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;

View File

@ -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();

View File

@ -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.

View File

@ -66,6 +66,22 @@
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.alpn</groupId>
<artifactId>alpn-api</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mortbay.jetty.alpn</groupId>
<artifactId>alpn-boot</artifactId>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -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<String> protocols, boolean isServer) {
return engine;
}
}

View File

@ -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<String> ciphers, Iterable<String> 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<String> nextProtoList = new ArrayList<String>();
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) {

View File

@ -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<String> unmodifiableCipherSuites;
private final SslEngineWrapperFactory wrapperFactory;
JdkSslContext(Iterable<String> ciphers) {
JdkSslContext(Iterable<String> 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<String> ciphers) {

View File

@ -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<String> ciphers, Iterable<String> 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<String> list = new ArrayList<String>();
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";

View File

@ -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<String> 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<String> 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<String> 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();
}
}

View File

@ -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<String> protocols, boolean isServer) {
return new JettyAlpnSslEngine(engine, protocols, isServer);
}
}

View File

@ -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<String> 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<String> 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();
}
}

View File

@ -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<String> protocols, boolean isServer) {
return new JettyNpnSslEngine(engine, protocols, isServer);
}
}

View File

@ -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<String> 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);
}
}

View File

@ -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;
}

View File

@ -65,7 +65,7 @@ public final class OpenSslServerContext extends SslContext {
private final List<String> unmodifiableCiphers = Collections.unmodifiableList(ciphers);
private final long sessionCacheSize;
private final long sessionTimeout;
private final List<String> nextProtocols;
private final List<String> 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<String> nextProtoList = new ArrayList<String>();
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<String> 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));
}
}

View File

@ -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;
* </pre>
*/
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<String> translateProtocols(Iterable<String> protocols) {
Iterator<String> itr = protocols == null ? null : protocols.iterator();
if (itr != null) {
List<String> nextProtoList = new ArrayList<String>(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<String> ciphers, Iterable<String> 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<String> ciphers, Iterable<String> 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<String> ciphers, Iterable<String> 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<String> ciphers, Iterable<String> 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() { }

View File

@ -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
* <ul>
* <li>{@code true} if the engine is for server side of connections</li>
* <li>{@code false} if the engine is for client side of connections</li>
* </ul>
* @return The resulting wrapped engine. This may just be {@code engine}
*/
SSLEngine wrapSslEngine(SSLEngine engine, List<String> protocols, boolean isServer);
}

View File

@ -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<ByteBuf> {
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<Channel>() {
@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<Channel>() {
@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));
}
}

128
pom.xml
View File

@ -109,7 +109,8 @@
<!-- Our Javadoc has poor enough quality to fail the build thanks to JDK8 javadoc which got more strict. -->
<maven.javadoc.failOnError>false</maven.javadoc.failOnError>
<!-- npn-boot does not work with JDK 8 -->
<argLine.bootcp>-D_</argLine.bootcp>
<jetty.alpn.version>8.0.0.v20140317</jetty.alpn.version>
<argLine.bootcp>-Xbootclasspath/p:${jetty.alpn.path}</argLine.bootcp>
</properties>
</profile>
<profile>
@ -172,8 +173,9 @@
</profile>
<!--
Profiles that assigns proper Jetty npn-boot version.
Profiles that assigns proper Jetty npn-boot and alpn-boot version.
See: http://www.eclipse.org/jetty/documentation/current/npn-chapter.html#npn-versions
See: http://www.eclipse.org/jetty/documentation/current/alpn-chapter.html#alpn-versions
-->
<profile>
<id>npn-7u9</id>
@ -272,7 +274,7 @@
</properties>
</profile>
<profile>
<id>npn-7u40</id>
<id>npn-alpn-7u40</id>
<activation>
<property>
<name>java.version</name>
@ -281,10 +283,12 @@
</activation>
<properties>
<jetty.npn.version>1.1.6.v20130911</jetty.npn.version>
<jetty.alpn.version>7.0.0.v20140317</jetty.alpn.version>
<!-- Defer definition of Xbootclasspath to default or forcenpn profile -->
</properties>
</profile>
<profile>
<id>npn-7u45</id>
<id>npn-alpn-7u45</id>
<activation>
<property>
<name>java.version</name>
@ -293,10 +297,12 @@
</activation>
<properties>
<jetty.npn.version>1.1.6.v20130911</jetty.npn.version>
<jetty.alpn.version>7.0.0.v20140317</jetty.alpn.version>
<!-- Defer definition of Xbootclasspath to default or forcenpn profile -->
</properties>
</profile>
<profile>
<id>npn-7u51</id>
<id>npn-alpn-7u51</id>
<activation>
<property>
<name>java.version</name>
@ -305,6 +311,81 @@
</activation>
<properties>
<jetty.npn.version>1.1.6.v20130911</jetty.npn.version>
<jetty.alpn.version>7.0.0.v20140317</jetty.alpn.version>
<!-- Defer definition of Xbootclasspath to default or forcenpn profile -->
</properties>
</profile>
<profile>
<id>npn-alpn-7u55</id>
<activation>
<property>
<name>java.version</name>
<value>1.7.0_55</value>
</property>
</activation>
<properties>
<jetty.npn.version>1.1.7.v20140316</jetty.npn.version>
<jetty.alpn.version>7.0.0.v20140317</jetty.alpn.version>
<!-- Defer definition of Xbootclasspath to default or forcenpn profile -->
</properties>
</profile>
<profile>
<id>npn-alpn-7u60</id>
<activation>
<property>
<name>java.version</name>
<value>1.7.0_60</value>
</property>
</activation>
<properties>
<jetty.npn.version>1.1.7.v20140316</jetty.npn.version>
<jetty.alpn.version>7.0.0.v20140317</jetty.alpn.version>
<!-- Defer definition of Xbootclasspath to default or forcenpn profile -->
</properties>
</profile>
<profile>
<id>npn-alpn-7u65</id>
<activation>
<property>
<name>java.version</name>
<value>1.7.0_65</value>
</property>
</activation>
<properties>
<jetty.npn.version>1.1.7.v20140316</jetty.npn.version>
<jetty.alpn.version>7.0.0.v20140317</jetty.alpn.version>
<!-- Defer definition of Xbootclasspath to default or forcenpn profile -->
</properties>
</profile>
<profile>
<id>npn-alpn-7u67</id>
<activation>
<property>
<name>java.version</name>
<value>1.7.0_67</value>
</property>
</activation>
<properties>
<jetty.npn.version>1.1.7.v20140316</jetty.npn.version>
<jetty.alpn.version>7.0.0.v20140317</jetty.alpn.version>
<!-- Defer definition of Xbootclasspath to default or forcenpn profile -->
</properties>
</profile>
<profile>
<!--
This profile exists because either ALPN or NPN can exits on the class path at once, but not both.
The JDK version is typically used to distinguish which should be used but there is some overlap
where both could be used. ALPN is the default and this profile is enabled with a -Dforcenpn=true arugument
-->
<id>forcenpn</id>
<activation>
<property>
<name>forcenpn</name>
<value>true</value>
</property>
</activation>
<properties>
<argLine.bootcp>-Xbootclasspath/p:${jetty.npn.path}</argLine.bootcp>
</properties>
</profile>
</profiles>
@ -315,6 +396,8 @@
<jboss.marshalling.version>1.3.18.GA</jboss.marshalling.version>
<jetty.npn.version>1.1.7.v20140316</jetty.npn.version>
<jetty.npn.path>${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${jetty.npn.version}/npn-boot-${jetty.npn.version}.jar</jetty.npn.path>
<jetty.alpn.version>8.0.0.v20140317</jetty.alpn.version>
<jetty.alpn.path>${settings.localRepository}/org/mortbay/jetty/alpn/alpn-boot/${jetty.alpn.version}/alpn-boot-${jetty.alpn.version}.jar</jetty.alpn.path>
<argLine.common>
-server
-dsa -da -ea:io.netty...
@ -325,7 +408,8 @@
-XX:+OptimizeStringConcat
-XX:+HeapDumpOnOutOfMemoryError
</argLine.common>
<argLine.bootcp>-Xbootclasspath/p:${jetty.npn.path}</argLine.bootcp>
<!-- Default to ALPN. Then classpath is used to refine selection. See forcenpn profile to force NPN -->
<argLine.bootcp>-Xbootclasspath/p:${jetty.alpn.path}</argLine.bootcp>
<argLine.leak>-verbose:gc</argLine.leak> <!-- Overridden when 'leak' profile is active -->
<argLine.coverage>-D_</argLine.coverage> <!-- Overridden when 'coverage' profile is active -->
</properties>
@ -385,6 +469,16 @@
<artifactId>npn-boot</artifactId>
<version>${jetty.npn.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.alpn</groupId>
<artifactId>alpn-api</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.mortbay.jetty.alpn</groupId>
<artifactId>alpn-boot</artifactId>
<version>${jetty.alpn.version}</version>
</dependency>
<!-- Google Protocol Buffers - completely optional -->
<dependency>
@ -665,6 +759,14 @@
<groupId>org.codehaus.mojo</groupId>
<artifactId>animal-sniffer-maven-plugin</artifactId>
<version>1.9</version>
<dependencies>
<!-- Upgrade ASM and support Java 8 bytecode -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-all</artifactId>
<version>5.0.3</version>
</dependency>
</dependencies>
<configuration>
<signature>
<groupId>org.codehaus.mojo.signature</groupId>
@ -754,6 +856,18 @@
<version>${jetty.npn.version}</version>
</configuration>
</execution>
<execution>
<id>get-alpn-boot</id>
<phase>validate</phase>
<goals>
<goal>get</goal>
</goals>
<configuration>
<groupId>org.mortbay.jetty.alpn</groupId>
<artifactId>alpn-boot</artifactId>
<version>${jetty.alpn.version}</version>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
@ -791,7 +905,7 @@
<instructions>
<Export-Package>${project.groupId}.*</Export-Package>
<!-- enforce JVM vendor package as optional -->
<Import-Package>sun.misc.*;resolution:=optional,sun.nio.ch;resolution:=optional,sun.security.*;resolution:=optional,*</Import-Package>
<Import-Package>sun.misc.*;resolution:=optional,sun.nio.ch;resolution:=optional,sun.security.*;resolution:=optional,org.eclipse.jetty.alpn;version="[1,)";resolution=optional,*</Import-Package>
<!-- override "internal" private package convention -->
<Private-Package>!*</Private-Package>
</instructions>

View File

@ -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"