From a89d6685267746e247c1c5f722b30d9f70ce150e Mon Sep 17 00:00:00 2001 From: jansupol Date: Tue, 24 Jan 2023 23:15:45 +0100 Subject: [PATCH] Add SNI Support based on Host header Signed-off-by: jansupol --- .../apache/connector/ApacheConnector.java | 72 +- .../apache5/connector/Apache5Connector.java | 108 ++- .../internal/ConnectorConfiguration.java | 13 +- .../connector/internal/HttpConnection.java | 5 +- .../internal/HttpConnectionPool.java | 14 +- .../jdk/connector/internal/SslFilter.java | 14 +- .../jdk/connector/internal/SslFilterTest.java | 11 +- .../netty/connector/NettyConnector.java | 27 +- .../innate/http/SSLParamConfigurator.java | 190 ++++++ .../client/innate/http/SniConfigurator.java | 110 +++ .../client/innate/http/package-info.java | 21 + .../client/internal/HttpUrlConnector.java | 117 +++- docs/src/main/docbook/client.xml | 16 +- etc/config/copyright-exclude | 1 + pom.xml | 2 +- tests/e2e-tls/pom.xml | 140 ++++ .../tests/e2e/tls/ClientHelloTestServer.java | 147 ++++ .../jersey/tests/e2e/tls/SniTest.java | 151 +++++ .../e2e/tls/explorer/SSLCapabilities.java | 79 +++ .../tests/e2e/tls/explorer/SSLExplorer.java | 627 ++++++++++++++++++ tests/e2e-tls/src/test/resources/hosts | 4 + tests/pom.xml | 3 +- 22 files changed, 1823 insertions(+), 49 deletions(-) create mode 100644 core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java create mode 100644 core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java create mode 100644 core-client/src/main/java/org/glassfish/jersey/client/innate/http/package-info.java create mode 100644 tests/e2e-tls/pom.xml create mode 100644 tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/ClientHelloTestServer.java create mode 100644 tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SniTest.java create mode 100644 tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/explorer/SSLCapabilities.java create mode 100644 tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/explorer/SSLExplorer.java create mode 100644 tests/e2e-tls/src/test/resources/hosts diff --git a/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheConnector.java b/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheConnector.java index 60c5e833999..37183d7e1f6 100644 --- a/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheConnector.java +++ b/connectors/apache-connector/src/main/java/org/glassfish/jersey/apache/connector/ApacheConnector.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.Socket; import java.net.URI; import java.util.ArrayList; import java.util.LinkedList; @@ -37,6 +38,7 @@ import java.util.logging.Logger; import java.util.stream.Collectors; +import javax.net.ssl.SSLSocket; import javax.ws.rs.ProcessingException; import javax.ws.rs.client.Client; import javax.ws.rs.core.Configuration; @@ -53,6 +55,7 @@ import org.glassfish.jersey.client.ClientResponse; import org.glassfish.jersey.client.RequestEntityProcessing; import org.glassfish.jersey.client.innate.ClientProxy; +import org.glassfish.jersey.client.innate.http.SSLParamConfigurator; import org.glassfish.jersey.client.spi.AsyncConnectorCallback; import org.glassfish.jersey.client.spi.Connector; import org.glassfish.jersey.internal.util.PropertiesHelper; @@ -103,6 +106,7 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.impl.io.ChunkedOutputStream; import org.apache.http.io.SessionOutputBuffer; +import org.apache.http.protocol.HttpContext; import org.apache.http.util.TextUtils; import org.apache.http.util.VersionInfo; @@ -180,6 +184,7 @@ class ApacheConnector implements Connector { private static final Logger LOGGER = Logger.getLogger(ApacheConnector.class.getName()); + private static final String JERSEY_REQUEST_ATTR_NAME = "JerseyRequestAttribute"; private static final VersionInfo vi; private static final String release; @@ -381,15 +386,15 @@ private HttpClientConnectionManager createConnectionManager( final LayeredConnectionSocketFactory sslSocketFactory; if (sslContext != null) { - sslSocketFactory = new SSLConnectionSocketFactory( + sslSocketFactory = new SniSSLConnectionSocketFactory( sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier); } else { if (useSystemProperties) { - sslSocketFactory = new SSLConnectionSocketFactory( + sslSocketFactory = new SniSSLConnectionSocketFactory( (SSLSocketFactory) SSLSocketFactory.getDefault(), supportedProtocols, supportedCipherSuites, hostnameVerifier); } else { - sslSocketFactory = new SSLConnectionSocketFactory( + sslSocketFactory = new SniSSLConnectionSocketFactory( SSLContexts.createDefault(), hostnameVerifier); } @@ -450,14 +455,16 @@ public CookieStore getCookieStore() { public ClientResponse apply(final ClientRequest clientRequest) throws ProcessingException { final HttpUriRequest request = getUriHttpRequest(clientRequest); final Map clientHeadersSnapshot = writeOutBoundHeaders(clientRequest, request); + final HttpHost httpHost = getHost(request); try { final CloseableHttpResponse response; final HttpClientContext context = HttpClientContext.create(); + if (preemptiveBasicAuth) { final AuthCache authCache = new BasicAuthCache(); final BasicScheme basicScheme = new BasicScheme(); - authCache.put(getHost(request), basicScheme); + authCache.put(httpHost, basicScheme); context.setAuthCache(authCache); } @@ -468,7 +475,8 @@ public ClientResponse apply(final ClientRequest clientRequest) throws Processing context.setCredentialsProvider(credentialsProvider); } - response = client.execute(getHost(request), request, context); + context.setAttribute(JERSEY_REQUEST_ATTR_NAME, clientRequest); + response = client.execute(httpHost, request, context); HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, clientRequest.getHeaders(), this.getClass().getName(), clientRequest.getConfiguration()); @@ -821,4 +829,56 @@ protected OutputStream createOutputStream(final long len, final SessionOutputBuf return super.createOutputStream(len, outbuffer); } } + + private static final class SniSSLConnectionSocketFactory extends SSLConnectionSocketFactory { + + private final ThreadLocal httpContexts = new ThreadLocal<>(); + + public SniSSLConnectionSocketFactory(final SSLContext sslContext, + final String[] supportedProtocols, + final String[] supportedCipherSuites, + final HostnameVerifier hostnameVerifier) { + super(sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier); + } + + public SniSSLConnectionSocketFactory(final javax.net.ssl.SSLSocketFactory socketFactory, + final String[] supportedProtocols, + final String[] supportedCipherSuites, + final HostnameVerifier hostnameVerifier) { + super(socketFactory, supportedProtocols, supportedCipherSuites, hostnameVerifier); + } + + public SniSSLConnectionSocketFactory( + final SSLContext sslContext, final HostnameVerifier hostnameVerifier) { + super(sslContext, hostnameVerifier); + } + + @Override + public Socket createLayeredSocket( + final Socket socket, + final String target, + final int port, + final HttpContext context) throws IOException { + httpContexts.set(context); + try { + return super.createLayeredSocket(socket, target, port, context); + } finally { + httpContexts.remove(); + } + } + + @Override + protected void prepareSocket(SSLSocket socket) throws IOException { + HttpContext context = httpContexts.get(); + + if (context != null) { + Object objectRequest = context.getAttribute(JERSEY_REQUEST_ATTR_NAME); + if (objectRequest != null) { + ClientRequest clientRequest = (ClientRequest) objectRequest; + SSLParamConfigurator sniConfig = SSLParamConfigurator.builder().request(clientRequest).build(); + sniConfig.setSNIServerName(socket); + } + } + } + } } diff --git a/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5Connector.java b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5Connector.java index da7ccdd5ceb..eb36bdfee25 100644 --- a/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5Connector.java +++ b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5Connector.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; @@ -37,6 +38,7 @@ import java.util.logging.Logger; import java.util.stream.Collectors; +import javax.net.ssl.SSLSocket; import javax.ws.rs.ProcessingException; import javax.ws.rs.client.Client; import javax.ws.rs.core.Configuration; @@ -48,6 +50,7 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; +import org.apache.hc.client5.http.AuthenticationStrategy; import org.apache.hc.client5.http.ConnectionKeepAliveStrategy; import org.apache.hc.client5.http.HttpRequestRetryStrategy; import org.apache.hc.client5.http.auth.AuthCache; @@ -65,6 +68,7 @@ import org.apache.hc.client5.http.impl.auth.BasicAuthCache; import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; import org.apache.hc.client5.http.impl.auth.BasicScheme; +import org.apache.hc.client5.http.impl.classic.BasicHttpClientResponseHandler; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; @@ -86,6 +90,7 @@ import org.apache.hc.core5.http.impl.DefaultContentLengthStrategy; import org.apache.hc.core5.http.io.entity.AbstractHttpEntity; import org.apache.hc.core5.http.io.entity.BufferedHttpEntity; +import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.ssl.SSLContexts; import org.apache.hc.core5.util.TextUtils; import org.apache.hc.core5.util.Timeout; @@ -95,6 +100,7 @@ import org.glassfish.jersey.client.ClientResponse; import org.glassfish.jersey.client.RequestEntityProcessing; import org.glassfish.jersey.client.innate.ClientProxy; +import org.glassfish.jersey.client.innate.http.SSLParamConfigurator; import org.glassfish.jersey.client.spi.AsyncConnectorCallback; import org.glassfish.jersey.client.spi.Connector; import org.glassfish.jersey.internal.util.PropertiesHelper; @@ -176,6 +182,7 @@ class Apache5Connector implements Connector { private static final Logger LOGGER = Logger.getLogger(Apache5Connector.class.getName()); + private static final String JERSEY_REQUEST_ATTR_NAME = "JerseyRequestAttribute"; private static final VersionInfo vi; private static final String release; @@ -385,15 +392,15 @@ private HttpClientConnectionManager createConnectionManager( final LayeredConnectionSocketFactory sslSocketFactory; if (sslContext != null) { - sslSocketFactory = new SSLConnectionSocketFactory( + sslSocketFactory = new SniSSLConnectionSocketFactory( sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier); } else { if (useSystemProperties) { - sslSocketFactory = new SSLConnectionSocketFactory( + sslSocketFactory = new SniSSLConnectionSocketFactory( (SSLSocketFactory) SSLSocketFactory.getDefault(), supportedProtocols, supportedCipherSuites, hostnameVerifier); } else { - sslSocketFactory = new SSLConnectionSocketFactory( + sslSocketFactory = new SniSSLConnectionSocketFactory( SSLContexts.createDefault(), hostnameVerifier); } @@ -458,12 +465,7 @@ public ClientResponse apply(final ClientRequest clientRequest) throws Processing try { final CloseableHttpResponse response; final HttpClientContext context = HttpClientContext.create(); - if (preemptiveBasicAuth) { - final AuthCache authCache = new BasicAuthCache(); - final BasicScheme basicScheme = new BasicScheme(); - authCache.put(getHost(request), basicScheme); - context.setAuthCache(authCache); - } + final HttpHost httpHost = getHost(request); // If a request-specific CredentialsProvider exists, use it instead of the default one CredentialsProvider credentialsProvider = @@ -472,7 +474,18 @@ public ClientResponse apply(final ClientRequest clientRequest) throws Processing context.setCredentialsProvider(credentialsProvider); } - response = client.execute(getHost(request), request, context); + if (preemptiveBasicAuth) { + final AuthCache authCache = new BasicAuthCache(); + final BasicScheme basicScheme = new BasicScheme(); + final AuthScope authScope = new AuthScope(httpHost); + basicScheme.initPreemptive(credentialsProvider.getCredentials(authScope, context)); + context.resetAuthExchange(httpHost, basicScheme); + authCache.put(httpHost, basicScheme); // must be after initPreemptive + context.setAuthCache(authCache); + } + + context.setAttribute(JERSEY_REQUEST_ATTR_NAME, clientRequest); + response = client.execute(httpHost, request, context); HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, clientRequest.getHeaders(), this.getClass().getName(), clientRequest.getConfiguration()); @@ -798,4 +811,77 @@ private ConnectionFactory(final int chunkSize) { ); } } + + private static final class SniSSLConnectionSocketFactory extends SSLConnectionSocketFactory { + + private final ThreadLocal httpContexts = new ThreadLocal<>(); + + public SniSSLConnectionSocketFactory(final SSLContext sslContext, + final String[] supportedProtocols, + final String[] supportedCipherSuites, + final HostnameVerifier hostnameVerifier) { + super(sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier); + } + + public SniSSLConnectionSocketFactory(final javax.net.ssl.SSLSocketFactory socketFactory, + final String[] supportedProtocols, + final String[] supportedCipherSuites, + final HostnameVerifier hostnameVerifier) { + super(socketFactory, supportedProtocols, supportedCipherSuites, hostnameVerifier); + } + + public SniSSLConnectionSocketFactory( + final SSLContext sslContext, final HostnameVerifier hostnameVerifier) { + super(sslContext, hostnameVerifier); + } + + /* Pre 5.2 */ + @Override + public Socket createLayeredSocket( + final Socket socket, + final String target, + final int port, + final HttpContext context) throws IOException { + httpContexts.set(context); + try { + return super.createLayeredSocket(socket, target, port, context); + } finally { + httpContexts.remove(); + } + } + + /* Post 5.2 */ + public Socket createLayeredSocket( + final Socket socket, + final String target, + final int port, + final Object attachment, + final HttpContext context) throws IOException { + httpContexts.set(context); + try { + return super.createLayeredSocket(socket, target, port, attachment, context); + } finally { + httpContexts.remove(); + } + } + + @Override + protected void prepareSocket(SSLSocket socket) throws IOException { + HttpContext context = httpContexts.get(); + + if (context != null) { + Object objectRequest = context.getAttribute(JERSEY_REQUEST_ATTR_NAME); + if (objectRequest != null) { + ClientRequest clientRequest = (ClientRequest) objectRequest; + SSLParamConfigurator sniConfig = SSLParamConfigurator.builder().request(clientRequest).build(); + sniConfig.setSNIServerName(socket); + + final int socketTimeout = ((ClientRequest) objectRequest).resolveProperty(ClientProperties.READ_TIMEOUT, -1); + if (socketTimeout >= 0) { + socket.setSoTimeout(socketTimeout); + } + } + } + } + } } diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ConnectorConfiguration.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ConnectorConfiguration.java index 612d5406fbb..afb0c6cb3e5 100644 --- a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ConnectorConfiguration.java +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/ConnectorConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -19,6 +19,7 @@ import java.net.CookiePolicy; import java.net.URI; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; @@ -30,6 +31,7 @@ import org.glassfish.jersey.SslConfigurator; import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.innate.http.SSLParamConfigurator; import org.glassfish.jersey.jdk.connector.JdkConnectorProperties; /** @@ -57,6 +59,7 @@ class ConnectorConfiguration { private final int responseTimeout; private final int connectTimeout; private final ProxyConfiguration proxyConfiguration; + private final AtomicReference sniConfigs = new AtomicReference<>(null); ConnectorConfiguration(Client client, Configuration config) { final Map properties = config.getProperties(); @@ -170,6 +173,14 @@ public ProxyConfiguration getProxyConfiguration() { return proxyConfiguration; } + void setSniConfig(SSLParamConfigurator sniConfig) { + this.sniConfigs.compareAndSet(null, sniConfig); + } + + SSLParamConfigurator getSniConfig() { + return sniConfigs.get(); + } + @Override public String toString() { return "ConnectorConfiguration{" diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnection.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnection.java index 2330dd492e2..d9ea76ef498 100644 --- a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnection.java +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -199,7 +199,8 @@ protected Filter createFil } - socket = new SslFilter(transportFilter, sslContext, uri.getHost(), configuration.getHostnameVerifier()); + socket = new SslFilter(transportFilter, sslContext, uri.getHost(), + configuration.getHostnameVerifier(), configuration.getSniConfig()); } else { socket = new TransportFilter(INPUT_BUFFER_SIZE, configuration.getThreadPoolConfig(), configuration.getContainerIdleTimeout()); diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnectionPool.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnectionPool.java index 28c750033b5..b20f00cad2d 100644 --- a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnectionPool.java +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/HttpConnectionPool.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -16,7 +16,11 @@ package org.glassfish.jersey.jdk.connector.internal; +import org.glassfish.jersey.client.innate.http.SSLParamConfigurator; + import java.net.CookieManager; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; @@ -45,10 +49,16 @@ class HttpConnectionPool { } void send(HttpRequest httpRequest, CompletionHandler completionHandler) { + final Map> headers = new HashMap<>(); + httpRequest.getHeaders().forEach((k, v) -> headers.put(k, (List) v)); + final SSLParamConfigurator sniConfig = SSLParamConfigurator.builder().uri(httpRequest.getUri()).headers(headers).build(); + connectorConfiguration.setSniConfig(sniConfig); + final DestinationConnectionPool.DestinationKey destinationKey = new DestinationConnectionPool.DestinationKey( - httpRequest.getUri()); + sniConfig.getSNIUri()); DestinationConnectionPool destinationConnectionPool = destinationPools.get(destinationKey); + if (destinationConnectionPool == null) { synchronized (this) { // check again while holding the lock diff --git a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/SslFilter.java b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/SslFilter.java index 85f4541f5c0..979a9ce831d 100644 --- a/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/SslFilter.java +++ b/connectors/jdk-connector/src/main/java/org/glassfish/jersey/jdk/connector/internal/SslFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -16,6 +16,8 @@ package org.glassfish.jersey.jdk.connector.internal; +import org.glassfish.jersey.client.innate.http.SSLParamConfigurator; + import java.nio.ByteBuffer; import java.nio.Buffer; import java.util.LinkedList; @@ -26,7 +28,6 @@ import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLException; -import javax.net.ssl.SSLParameters; /** @@ -87,7 +88,8 @@ class SslFilter extends Filter { SslFilter(Filter downstreamFilter, SSLContext sslContext, String serverHost, - HostnameVerifier customHostnameVerifier) { + HostnameVerifier customHostnameVerifier, + SSLParamConfigurator sniConfig) { super(downstreamFilter); this.serverHost = serverHost; sslEngine = sslContext.createSSLEngine(serverHost, -1); @@ -100,11 +102,11 @@ class SslFilter extends Filter { * when {@link SslEngineConfigurator} supports Java 7. */ if (customHostnameVerifier == null) { - SSLParameters sslParameters = sslEngine.getSSLParameters(); - sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); - sslEngine.setSSLParameters(sslParameters); + sniConfig.setEndpointIdentificationAlgorithm(sslEngine); } + sniConfig.setSNIServerName(sslEngine); + applicationInputBuffer = ByteBuffer.allocate(sslEngine.getSession().getApplicationBufferSize()); networkOutputBuffer = ByteBuffer.allocate(sslEngine.getSession().getPacketBufferSize()); } diff --git a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/SslFilterTest.java b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/SslFilterTest.java index 5d205ce3b6d..ef13997809d 100644 --- a/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/SslFilterTest.java +++ b/connectors/jdk-connector/src/test/java/org/glassfish/jersey/jdk/connector/internal/SslFilterTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -23,10 +23,12 @@ import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.SocketException; +import java.net.URI; import java.nio.ByteBuffer; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -41,7 +43,9 @@ import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; + import org.glassfish.jersey.SslConfigurator; +import org.glassfish.jersey.client.innate.http.SSLParamConfigurator; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ConditionEvaluationResult; import org.junit.jupiter.api.extension.ExecutionCondition; @@ -391,7 +395,10 @@ private Filter openClientSocket( .keyStorePassword("asdfgh"); TransportFilter transportFilter = new TransportFilter(17_000, ThreadPoolConfig.defaultConfig(), 100_000); - final SslFilter sslFilter = new SslFilter(transportFilter, sslConfig.createSSLContext(), host, customHostnameVerifier); + final SSLParamConfigurator sslParamConfigurator = SSLParamConfigurator.builder() + .uri(URI.create("Https://" + host)).headers(Collections.emptyMap()).build(); + final SslFilter sslFilter = new SslFilter( + transportFilter, sslConfig.createSSLContext(), host, customHostnameVerifier, sslParamConfigurator); // exceptions errors that occur before SSL handshake has finished are thrown from this method final AtomicReference exception = new AtomicReference<>(); diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java index 8bb117a9668..9406b51f159 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -36,9 +36,6 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLParameters; - import javax.ws.rs.ProcessingException; import javax.ws.rs.client.Client; import javax.ws.rs.core.Configuration; @@ -83,6 +80,7 @@ import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.client.ClientResponse; import org.glassfish.jersey.client.innate.ClientProxy; +import org.glassfish.jersey.client.innate.http.SSLParamConfigurator; import org.glassfish.jersey.client.spi.AsyncConnectorCallback; import org.glassfish.jersey.client.spi.Connector; import org.glassfish.jersey.message.internal.OutboundMessageContext; @@ -278,17 +276,20 @@ protected void initChannel(SocketChannel ch) throws Exception { (String[]) null, /* enable default protocols */ false /* true if the first write request shouldn't be encrypted */ ); - int port = requestUri.getPort(); - SslHandler sslHandler = jdkSslContext.newHandler(ch.alloc(), requestUri.getHost(), - port <= 0 ? 443 : port, executorService); + + final int port = requestUri.getPort(); + final SSLParamConfigurator sslConfig = SSLParamConfigurator.builder() + .request(jerseyRequest).setSNIAlways(true).build(); + final SslHandler sslHandler = jdkSslContext.newHandler( + ch.alloc(), sslConfig.getSNIHostName(), port <= 0 ? 443 : port, executorService + ); if (ClientProperties.getValue(config.getProperties(), NettyClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION, true)) { - SSLEngine sslEngine = sslHandler.engine(); - SSLParameters sslParameters = sslEngine.getSSLParameters(); - sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); - sslEngine.setSSLParameters(sslParameters); + sslConfig.setEndpointIdentificationAlgorithm(sslHandler.engine()); } + sslConfig.setSNIServerName(sslHandler.engine()); + p.addLast(sslHandler); } @@ -374,7 +375,9 @@ protected void initChannel(SocketChannel ch) throws Exception { setHeaders(jerseyRequest, nettyRequest.headers()); // host header - http 1.1 - nettyRequest.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost()); + if (!nettyRequest.headers().contains(HttpHeaderNames.HOST)) { + nettyRequest.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost()); + } if (jerseyRequest.hasEntity()) { // guard against prematurely closed channel diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java new file mode 100644 index 00000000000..edfe29bb898 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.innate.http; + +import org.glassfish.jersey.client.ClientRequest; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocket; +import javax.ws.rs.core.UriBuilder; +import java.net.InetAddress; +import java.net.URI; +import java.net.UnknownHostException; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * A unified routines to configure {@link SSLParameters}. + * To be reused in connectors. + */ +public final class SSLParamConfigurator { + private final URI uri; + private final Map> httpHeaders; + private final Optional sniConfigurator; + + /** + * Builder of the {@link SSLParamConfigurator} instance. + */ + public static final class Builder { + private ClientRequest clientRequest; + private URI uri; + private Map> httpHeaders; + private boolean setAlways = false; + + /** + * Sets the {@link ClientRequest} instance. + * @param clientRequest the {@link ClientRequest} + * @return the builder instance + */ + public Builder request(ClientRequest clientRequest) { + this.clientRequest = clientRequest; + this.httpHeaders = null; + this.uri = null; + return this; + } + + /** + * Sets the HTTP request {@link URI} instance. + * @param uri The request uri + * @return the builder instance + */ + public Builder uri(URI uri) { + this.clientRequest = null; + this.uri = uri; + return this; + } + + /** + * Sets the HTTP request headers + * @param httpHeaders the http request headers + * @return the builder instance + */ + public Builder headers(Map> httpHeaders) { + this.clientRequest = null; + this.httpHeaders = httpHeaders; + return this; + } + + /** + * Sets SNI only when {@link javax.ws.rs.core.HttpHeaders#HOST} differs from the request host name if set to + * {@code false}. Default is {@code false}. + * @param setAlways set SNI always (default) + * @return the builder instance + */ + public Builder setSNIAlways(boolean setAlways) { + this.setAlways = setAlways; + return this; + } + + /** + * Builds the {@link SSLParamConfigurator} instance. + * @return the configured {@link SSLParamConfigurator} instance. + */ + public SSLParamConfigurator build() { + return new SSLParamConfigurator(this); + } + } + + private SSLParamConfigurator(SSLParamConfigurator.Builder builder) { + this.uri = builder.clientRequest != null ? builder.clientRequest.getUri() : builder.uri; + this.httpHeaders = builder.clientRequest != null ? builder.clientRequest.getHeaders() : builder.httpHeaders; + sniConfigurator = SniConfigurator.createWhenHostHeader(uri, httpHeaders, builder.setAlways); + } + + /** + * Create a new instance of TlsSupport class + **/ + public static SSLParamConfigurator.Builder builder() { + return new SSLParamConfigurator.Builder(); + } + + /** + * Get the host name either set by the request URI or by + * {@link javax.ws.rs.core.HttpHeaders#HOST} header if it differs from HTTP request host name. + * @return the hostName the {@link SSLEngine} is to use. + */ + public String getSNIHostName() { + return sniConfigurator.isPresent() ? sniConfigurator.get().getHostName() : uri.getHost(); + } + + /** + * Replaces hostname within the {@link ClientRequest} uri with a resolved IP address. Should the hostname be not known, + * the original request URI is returned. The purpose of this method is to replace the host with the IP so that + * {code HttpUrlConnection} does not replace user defined {@link javax.net.ssl.SNIHostName} with the host from the request + * uri. + * @return the request uri with ip address of the resolved host. + */ + public URI toIPRequestUri() { + String host = uri.getHost(); + try { + InetAddress ip = InetAddress.getByName(host); + return UriBuilder.fromUri(uri).host(ip.getHostAddress()).build(); + } catch (UnknownHostException e) { + return uri; + } + } + + /** + * Return true iff SNI is to be set, i.e. + * {@link javax.ws.rs.core.HttpHeaders#HOST} header if it differs from HTTP request host name. + * @return Return {@code true} when {@link javax.net.ssl.SNIHostName} is to be set. + */ + public boolean isSNIRequired() { + return sniConfigurator.isPresent(); + } + + /** + * Get the request URI or altered by {@link javax.ws.rs.core.HttpHeaders#HOST} header. + * @return The possibly altered request URI. + * @see #getSNIHostName() + */ + public URI getSNIUri() { + return sniConfigurator.isPresent() ? UriBuilder.fromUri(uri).host(getSNIHostName()).build() : uri; + } + + /** + * Set {@link javax.net.ssl.SNIServerName} for the {@link SSLParameters} when SNI should be used + * (i.e. {@link javax.ws.rs.core.HttpHeaders#HOST} differs from HTTP request host name) + * @param sslEngine the {@link SSLEngine} the {@link SSLParameters} are set for. + */ + public void setSNIServerName(SSLEngine sslEngine) { + sniConfigurator.ifPresent(sni -> sni.setServerNames(sslEngine)); + } + + + /** + * Set {@link javax.net.ssl.SNIServerName} for the {@link SSLParameters} when SNI should be used + * (i.e. {@link javax.ws.rs.core.HttpHeaders#HOST} differs from HTTP request host name) + * @param sslSocket the {@link SSLSocket} the {@link SSLParameters} are set for. + */ + public void setSNIServerName(SSLSocket sslSocket) { + sniConfigurator.ifPresent(sni -> sni.setServerNames(sslSocket)); + } + + /** + * Set setEndpointIdentificationAlgorithm to HTTPS. This is to prevent man-in-the-middle attacks. + * @param sslEngine the {@link SSLEngine} the algorithm is set for. + * @see SSLParameters#setEndpointIdentificationAlgorithm(String) + */ + public void setEndpointIdentificationAlgorithm(SSLEngine sslEngine) { + SSLParameters sslParameters = sslEngine.getSSLParameters(); + sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); + sslEngine.setSSLParameters(sslParameters); + } +} diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java new file mode 100644 index 00000000000..fc5d05732cf --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.innate.http; + +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocket; +import javax.ws.rs.core.HttpHeaders; +import java.net.URI; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * A unified routines to set {@link SNIHostName} for the {@link javax.net.ssl.SSLContext}. + * To be reused in connectors. + */ +final class SniConfigurator { + private final String hostName; + private SniConfigurator(String hostName) { + this.hostName = hostName; + } + + /** + * Get the hostName from the {@link HttpHeaders#HOST} header. + * @return + */ + String getHostName() { + return hostName; + } + + /** + * Create ClientSNI when {@link HttpHeaders#HOST} is set different from the request URI host (or {@code whenDiffer}.is false). + * @param hostUri the Uri of the HTTP request + * @param headers the HttpHeaders + * @param whenDiffer create {@SniConfigurator only when different from the request URI host} + * @return ClientSNI or empty when {@link HttpHeaders#HOST} + */ + static Optional createWhenHostHeader(URI hostUri, Map> headers, boolean whenDiffer) { + List hostHeaders = headers.get(HttpHeaders.HOST); + if (hostHeaders == null || hostHeaders.get(0) == null) { + return Optional.empty(); + } + + final String hostHeader = hostHeaders.get(0).toString(); + final String trimmedHeader; + if (hostHeader != null) { + int index = hostHeader.indexOf(':'); // RFC 7230 Host = uri-host [ ":" port ] ; + final String trimmedHeader0 = index != -1 ? hostHeader.substring(0, index).trim() : hostHeader.trim(); + trimmedHeader = trimmedHeader0.isEmpty() ? hostHeader : trimmedHeader0; + } else { + return Optional.empty(); + } + + final String hostUriString = hostUri.getHost(); + if (!whenDiffer && hostUriString.equals(trimmedHeader)) { + return Optional.empty(); + } + + return Optional.of(new SniConfigurator(trimmedHeader)); + } + + /** + * Set {@link SNIServerName} for the given {@link SSLEngine} SSLParameters. + * @param sslEngine + */ + void setServerNames(SSLEngine sslEngine) { + SSLParameters sslParameters = sslEngine.getSSLParameters(); + updateSSLParameters(sslParameters); + sslEngine.setSSLParameters(sslParameters); + } + + /** + * Set {@link SNIServerName} for the given {@link SSLSocket} SSLParameters. + * @param sslSocket + */ + void setServerNames(SSLSocket sslSocket) { + SSLParameters sslParameters = sslSocket.getSSLParameters(); + updateSSLParameters(sslParameters); + sslSocket.setSSLParameters(sslParameters); + } + + private SSLParameters updateSSLParameters(SSLParameters sslParameters) { + SNIHostName serverName = new SNIHostName(hostName); + List serverNames = new LinkedList<>(); + serverNames.add(serverName); + + sslParameters.setServerNames(serverNames); + + return sslParameters; + } + +} diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/package-info.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/package-info.java new file mode 100644 index 00000000000..bbedce040c0 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/** + * Jersey client MOST INTERNAL http related classes/interfaces. + * Shall not be used outside of Jersey. The module shall not be exported to outside of Jersey. + */ +package org.glassfish.jersey.client.innate.http; \ No newline at end of file diff --git a/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java index dc8af256b11..212dac0f737 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2023 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -21,10 +21,13 @@ import java.io.InputStream; import java.lang.reflect.Field; import java.net.HttpURLConnection; +import java.net.InetAddress; import java.net.ProtocolException; +import java.net.Socket; import java.net.SocketTimeoutException; import java.net.URI; import java.net.URISyntaxException; +import java.net.UnknownHostException; import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; @@ -42,6 +45,7 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.ws.rs.ProcessingException; import javax.ws.rs.client.Client; @@ -55,6 +59,7 @@ import org.glassfish.jersey.client.JerseyClient; import org.glassfish.jersey.client.RequestEntityProcessing; import org.glassfish.jersey.client.innate.ClientProxy; +import org.glassfish.jersey.client.innate.http.SSLParamConfigurator; import org.glassfish.jersey.client.spi.AsyncConnectorCallback; import org.glassfish.jersey.client.spi.Connector; import org.glassfish.jersey.internal.util.PropertiesHelper; @@ -315,11 +320,35 @@ protected void secureConnection(final JerseyClient client, final HttpURLConnecti } } + /** + * Secure connection if necessary. + *

+ * Provided implementation sets {@link HostnameVerifier} and {@link SSLSocketFactory} to give connection, if that + * is an instance of {@link HttpsURLConnection}. + * + * @param clientRequest the actual client request. + * @param uc http connection to be secured. + */ + private void secureConnection( + final ClientRequest clientRequest, final HttpURLConnection uc, final SSLParamConfigurator sniConfig) { + secureConnection(clientRequest.getClient(), uc); // keep this for compatibility + + if (sniConfig.isSNIRequired() && uc instanceof HttpsURLConnection) { // set SNI + HttpsURLConnection suc = (HttpsURLConnection) uc; + SniSSLSocketFactory socketFactory = new SniSSLSocketFactory(suc.getSSLSocketFactory()); + socketFactory.setSniConfig(sniConfig); + suc.setSSLSocketFactory(socketFactory); + } + } + private ClientResponse _apply(final ClientRequest request) throws IOException { final HttpURLConnection uc; - Optional proxy = ClientProxy.proxyFromRequest(request); + final Optional proxy = ClientProxy.proxyFromRequest(request); + final SSLParamConfigurator sniConfig = SSLParamConfigurator.builder().request(request).build(); + final URI sniUri = sniConfig.isSNIRequired() ? sniConfig.toIPRequestUri() : request.getUri(); + proxy.ifPresent(clientProxy -> ClientProxy.setBasicAuthorizationHeader(request.getHeaders(), proxy.get())); - uc = this.connectionFactory.getConnection(request.getUri().toURL(), proxy.isPresent() ? proxy.get().proxy() : null); + uc = this.connectionFactory.getConnection(sniUri.toURL(), proxy.isPresent() ? proxy.get().proxy() : null); uc.setDoInput(true); final String httpMethod = request.getMethod(); @@ -335,7 +364,7 @@ private ClientResponse _apply(final ClientRequest request) throws IOException { uc.setReadTimeout(request.resolveProperty(ClientProperties.READ_TIMEOUT, uc.getReadTimeout())); - secureConnection(request.getClient(), uc); + secureConnection(request, uc, sniConfig); final Object entity = request.getEntity(); Exception storedException = null; @@ -559,4 +588,84 @@ private IOException handleException(ClientRequest request, IOException ex, HttpU public String getName() { return "HttpUrlConnection " + AccessController.doPrivileged(PropertiesHelper.getSystemProperty("java.version")); } + + private static class SniSSLSocketFactory extends SSLSocketFactory { + private final SSLSocketFactory socketFactory; + private ThreadLocal sniConfigs = new ThreadLocal<>(); + + public void setSniConfig(SSLParamConfigurator sniConfigs) { + this.sniConfigs.set(sniConfigs); + } + + private SniSSLSocketFactory(SSLSocketFactory socketFactory) { + this.socketFactory = socketFactory; + } + + @Override + public String[] getDefaultCipherSuites() { + return socketFactory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return socketFactory.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(Socket socket, String s, int i, boolean b) throws IOException { + Socket superSocket = socketFactory.createSocket(socket, s, i, b); + setSNIServerName(superSocket); + return superSocket; + } + + @Override + public Socket createSocket(String s, int i) throws IOException, UnknownHostException { + Socket superSocket = socketFactory.createSocket(s, i); + setSNIServerName(superSocket); + return superSocket; + } + + @Override + public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) throws IOException, UnknownHostException { + Socket superSocket = socketFactory.createSocket(s, i, inetAddress, i1); + setSNIServerName(superSocket); + return superSocket; + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i) throws IOException { + Socket superSocket = socketFactory.createSocket(inetAddress, i); + setSNIServerName(superSocket); + return superSocket; + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int i1) throws IOException { + Socket superSocket = socketFactory.createSocket(inetAddress, i, inetAddress1, i1); + setSNIServerName(superSocket); + return superSocket; + } + + @Override + public Socket createSocket(Socket s, InputStream consumed, boolean autoClose) throws IOException { + Socket superSocket = socketFactory.createSocket(s, consumed, autoClose); + setSNIServerName(superSocket); + return superSocket; + } + + @Override + public Socket createSocket() throws IOException { + Socket superSocket = socketFactory.createSocket(); + setSNIServerName(superSocket); + return superSocket; + } + + private void setSNIServerName(Socket superSocket) { + SSLParamConfigurator sniConfig = this.sniConfigs.get(); + if (null != sniConfig && SSLSocket.class.isInstance(superSocket)) { + sniConfig.setSNIServerName((SSLSocket) superSocket); + } + this.sniConfigs.remove(); + } + } } diff --git a/docs/src/main/docbook/client.xml b/docs/src/main/docbook/client.xml index a4e40d762b5..d9719b67aca 100644 --- a/docs/src/main/docbook/client.xml +++ b/docs/src/main/docbook/client.xml @@ -1088,8 +1088,22 @@ Client client = ClientBuilder.newBuilder().sslContext(sslContext).build(); See javadoc of the &jersey.client.HttpAuthenticationFeature; for more details. - + +

+ <anchor xml:id="SNI"/>Server Name Indication (SNI) Support + + When using SSL/TLS protocols for the connection, SNIHostName is set automatically + based on the host name in the HTTPS request. + + + There might be use-cases where the SNIHostName is required to be set for other host + than the host specified in the HTTPS request. For those cases, when the HTTP header Host + is set, the SNIHostName is set for the host specified in the Host header. + Note that only Apache Connector, JDK Connector, Netty connector, and the default + HttpUrlConnector do support this feature. + +
diff --git a/etc/config/copyright-exclude b/etc/config/copyright-exclude index 987175b5999..1fb7a0b7144 100644 --- a/etc/config/copyright-exclude +++ b/etc/config/copyright-exclude @@ -69,6 +69,7 @@ build.readme /tests/e2e/server/mvc/MvcEncodingTest/MustacheResource.mustache /test/resources/org/glassfish/jersey/server/config/jaxrs-components /tests/e2e-entity/src/test/resources/org/glassfish/jersey/tests/e2e/entity/xxe.txt +/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/ee/tls/explorer /core-server/src/main/java/com/sun/research/ws/wadl /core-common/src/main/java/org/glassfish/jersey/internal/jsr166 /core-common/src/main/jsr166/org/glassfish/jersey/internal/jsr166 diff --git a/pom.xml b/pom.xml index 8a1f1120cf6..12ddc331b8f 100644 --- a/pom.xml +++ b/pom.xml @@ -2181,7 +2181,7 @@ 1.4.9 2.9.0 4.5.13 - 5.1.3 + 5.2.1 2.14.1 1.9.13 3.29.0-GA diff --git a/tests/e2e-tls/pom.xml b/tests/e2e-tls/pom.xml new file mode 100644 index 00000000000..897df6f8857 --- /dev/null +++ b/tests/e2e-tls/pom.xml @@ -0,0 +1,140 @@ + + + + + 4.0.0 + + + org.glassfish.jersey.tests + project + 2.39-SNAPSHOT + + + e2e-tls + jersey-tests-e2e-tls + jar + + Jersey E2E SSL/TLS tests + + + + + org.apache.maven.plugins + maven-surefire-plugin + + 1 + false + false + ${skip.e2e} + + true + + ssl.debug + true + + + + + + + + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-bundle + pom + test + + + org.glassfish.jersey.test-framework + jersey-test-framework-util + test + + + org.hamcrest + hamcrest + test + + + org.junit.platform + junit-platform-suite + ${junit-platform-suite.version} + test + + + io.specto + hoverfly-java-junit5 + 0.14.0 + test + + + + org.glassfish.jersey.connectors + jersey-apache-connector + test + + + org.glassfish.jersey.connectors + jersey-apache5-connector + test + + + org.glassfish.jersey.connectors + jersey-grizzly-connector + test + + + org.glassfish.jersey.connectors + jersey-jetty-connector + test + + + org.glassfish.jersey.connectors + jersey-jdk-connector + test + + + org.glassfish.jersey.security + oauth1-signature + ${project.version} + test + + + + + + jdk11+ + + [11,) + + + + com.sun.xml.bind + jaxb-osgi + test + + + + + -Djdk.tls.server.protocols=TLSv1.2 + + + + + diff --git a/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/ClientHelloTestServer.java b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/ClientHelloTestServer.java new file mode 100644 index 00000000000..703d1c83f2f --- /dev/null +++ b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/ClientHelloTestServer.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.tests.e2e.tls; + +import org.glassfish.jersey.tests.e2e.tls.explorer.SSLCapabilities; +import org.glassfish.jersey.tests.e2e.tls.explorer.SSLExplorer; + +import javax.net.ServerSocketFactory; +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIMatcher; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +public class ClientHelloTestServer { + private ServerSocket serverSocket; + private Thread serverThread; + private volatile State state = State.NONE; + + private enum State { + NONE, + INIT, + STARTED, + STOPPED + } + + protected ServerSocketFactory getServerSocketFactory() { + return ServerSocketFactory.getDefault(); + } + + protected void afterHandshake(Socket socket, SSLCapabilities capabilities) { + + } + + public void init(int port) { + ServerSocketFactory factory = getServerSocketFactory(); + try { + serverSocket = factory.createServerSocket(port); + + state = State.INIT; + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void start() { + if (state != State.INIT) { + System.out.println("Server has not been initialized"); + } + Thread thread = new Thread(() -> { + while (state == State.INIT) { + Socket socket = null; + try { + socket = serverSocket.accept(); + + inspect(socket); + } catch (SocketException e) { + if (!e.getMessage().equals("Interrupted function call: accept failed")) { + e.printStackTrace(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + serverThread = thread; + thread.start(); + } + + public void stop() { + try { + state = State.STOPPED; + serverSocket.close(); + serverThread.join(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void inspect(Socket socket) throws IOException { + InputStream ins = socket.getInputStream(); + + byte[] buffer = new byte[0xFF]; + int position = 0; + SSLCapabilities capabilities = null; + +// Read the header of TLS record + while (position < SSLExplorer.RECORD_HEADER_SIZE) { + int count = SSLExplorer.RECORD_HEADER_SIZE - position; + int n = ins.read(buffer, position, count); + if (n < 0) { + throw new IOException("unexpected end of stream!"); + } + position += n; + } + +// Get the required size to explore the SSL capabilities + int recordLength = SSLExplorer.getRequiredSize(buffer, 0, position); + if (buffer.length < recordLength) { + buffer = Arrays.copyOf(buffer, recordLength); + } + + while (position < recordLength) { + int count = recordLength - position; + int n = ins.read(buffer, position, count); + if (n < 0) { + throw new IOException("unexpected end of stream!"); + } + position += n; + } + +// Explore + capabilities = SSLExplorer.explore(buffer, 0, recordLength); + if (capabilities != null) { + System.out.println("Record version: " + capabilities.getRecordVersion()); + System.out.println("Hello version: " + capabilities.getHelloVersion()); + } + + afterHandshake(socket, capabilities); + } +} diff --git a/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SniTest.java b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SniTest.java new file mode 100644 index 00000000000..1504d442ea0 --- /dev/null +++ b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SniTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.tests.e2e.tls; + +import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; +import org.glassfish.jersey.apache5.connector.Apache5ConnectorProvider; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.HttpUrlConnectorProvider; +import org.glassfish.jersey.client.spi.ConnectorProvider; +import org.glassfish.jersey.jdk.connector.JdkConnectorProvider; +import org.glassfish.jersey.netty.connector.NettyConnectorProvider; +import org.glassfish.jersey.tests.e2e.tls.explorer.SSLCapabilities; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import javax.net.ssl.SNIServerName; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class SniTest { + private static final int PORT = 8443; + private static final String LOCALHOST = "127.0.0.1"; + + + static { + // JDK specific settings + System.setProperty("jdk.net.hosts.file", SniTest.class.getResource("/hosts").getPath()); + } + + public static ConnectorProvider[] getConnectors() { + return new ConnectorProvider[] { + new NettyConnectorProvider(), + new ApacheConnectorProvider(), + new Apache5ConnectorProvider(), + new JdkConnectorProvider(), + new HttpUrlConnectorProvider() + }; + } + + @ParameterizedTest + @MethodSource("getConnectors") + public void server1Test(ConnectorProvider provider) { + ClientConfig clientConfig = new ClientConfig(); + clientConfig.connectorProvider(provider); + serverTest(clientConfig, "www.host1.com"); + } + + public void serverTest(ClientConfig clientConfig, String hostName) { + String newHostName = replaceWhenHostNotKnown(hostName); + final List serverNames = new LinkedList<>(); + final String[] requestHostName = new String[1]; + ClientHelloTestServer server = new ClientHelloTestServer() { + @Override + protected void afterHandshake(Socket socket, SSLCapabilities capabilities) { + serverNames.addAll(capabilities.getServerNames()); + } + }; + server.init(PORT); + server.start(); + + clientConfig.property(ClientProperties.READ_TIMEOUT, 2000); + clientConfig.property(ClientProperties.CONNECT_TIMEOUT, 2000); + try (Response r = ClientBuilder.newClient(clientConfig) + .register(new ClientRequestFilter() { + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestHostName[0] = requestContext.getUri().getHost(); + } + }) + .target("https://" + (newHostName.equals(LOCALHOST) ? LOCALHOST : "www.host0.com") + ":" + PORT) + .path("host") + .request() + .header(HttpHeaders.HOST, hostName + ":8080") + .get()) { + // empty + } catch (Exception e) { + Throwable cause = e; + while (cause != null + && !SocketTimeoutException.class.isInstance(cause) + && TimeoutException.class.isInstance(cause)) { + cause = cause.getCause(); + } + if (cause == null && /*IOE*/ !e.getMessage().contains("Stream closed")) { + throw e; + } + } + + server.stop(); + + if (serverNames.isEmpty()) { + throw new IllegalStateException("ServerNames are empty"); + } + + String clientSniName = new String(serverNames.get(0).getEncoded()); + if (!hostName.equals(clientSniName)) { + throw new IllegalStateException("Unexpected client SNI name " + clientSniName); + } + + if (!LOCALHOST.equals(newHostName) && requestHostName[0].equals(hostName)) { + throw new IllegalStateException("The HTTP Request is made with the same"); + } + + System.out.append("Found expected Client SNI ").println(serverNames.get(0)); + } + + /* + * The method checks whether the JDK-dependent property "jdk.net.hosts.file" works. + * If it does, the request is made with the hostname, so that the 3rd party client has + * the request with the hostname. If a real address is returned or UnknownHostException + * is thrown, the property did not work and the request needs to use 127.0.0.1. + */ + private static String replaceWhenHostNotKnown(String hostName) { + try { + InetAddress inetAddress = InetAddress.getByName(hostName); + return LOCALHOST.equals(inetAddress.getHostAddress()) ? hostName : LOCALHOST; + } catch (UnknownHostException e) { + return LOCALHOST; + } + } +} diff --git a/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/explorer/SSLCapabilities.java b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/explorer/SSLCapabilities.java new file mode 100644 index 00000000000..e21b86ab0ce --- /dev/null +++ b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/explorer/SSLCapabilities.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2013, 2023 Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of Oracle or the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.glassfish.jersey.tests.e2e.tls.explorer; + +import java.nio.ByteBuffer; +import java.util.List; +import javax.net.ssl.SNIServerName; + +/** + * Encapsulates the security capabilities of an SSL/TLS connection. + *

+ * The security capabilities are the list of ciphersuites to be accepted in + * an SSL/TLS handshake, the record version, the hello version, and server + * name indication, etc., of an SSL/TLS connection. + *

+ * SSLCapabilities can be retrieved by exploring the network + * data of an SSL/TLS connection via {@link SSLExplorer#explore(ByteBuffer)} + * or {@link SSLExplorer#explore(byte[], int, int)}. + * + * @see SSLExplorer + */ +public abstract class SSLCapabilities { + + /** + * Returns the record version of an SSL/TLS connection + * + * @return a non-null record version + */ + public abstract String getRecordVersion(); + + /** + * Returns the hello version of an SSL/TLS connection + * + * @return a non-null hello version + */ + public abstract String getHelloVersion(); + + /** + * Returns a List containing all {@link SNIServerName}s + * of the server name indication. + * + * @return a non-null immutable list of {@link SNIServerName}s + * of the server name indication parameter, may be empty + * if no server name indication. + * + * @see SNIServerName + */ + public abstract List getServerNames(); +} + diff --git a/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/explorer/SSLExplorer.java b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/explorer/SSLExplorer.java new file mode 100644 index 00000000000..71c1f4e48ca --- /dev/null +++ b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/explorer/SSLExplorer.java @@ -0,0 +1,627 @@ +package org.glassfish.jersey.tests.e2e.tls.explorer; + +/* + * Copyright (c) 2013, 2023 Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of Oracle or the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLProtocolException; +import javax.net.ssl.StandardConstants; +import java.nio.ByteBuffer; +import java.nio.BufferUnderflowException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Instances of this class acts as an explorer of the network data of an + * SSL/TLS connection. + */ +public final class SSLExplorer { + + // Private constructor prevents construction outside this class. + private SSLExplorer() { + } + + /** + * The header size of TLS/SSL records. + *

+ * The value of this constant is {@value}. + */ + public static final int RECORD_HEADER_SIZE = 0x05; + + /** + * Returns the required number of bytes in the {@code source} + * {@link ByteBuffer} necessary to explore SSL/TLS connection. + *

+ * This method tries to parse as few bytes as possible from + * {@code source} byte buffer to get the length of an + * SSL/TLS record. + *

+ * This method accesses the {@code source} parameter in read-only + * mode, and does not update the buffer's properties such as capacity, + * limit, position, and mark values. + * + * @param source + * a {@link ByteBuffer} containing + * inbound or outbound network data for an SSL/TLS connection. + * @throws BufferUnderflowException if less than {@code RECORD_HEADER_SIZE} + * bytes remaining in {@code source} + * @return the required size in byte to explore an SSL/TLS connection + */ + public static int getRequiredSize(ByteBuffer source) { + + ByteBuffer input = source.duplicate(); + + // Do we have a complete header? + if (input.remaining() < RECORD_HEADER_SIZE) { + throw new BufferUnderflowException(); + } + + // Is it a handshake message? + byte firstByte = input.get(); + byte secondByte = input.get(); + byte thirdByte = input.get(); + if ((firstByte & 0x80) != 0 && thirdByte == 0x01) { + // looks like a V2ClientHello + // return (((firstByte & 0x7F) << 8) | (secondByte & 0xFF)) + 2; + return RECORD_HEADER_SIZE; // Only need the header fields + } else { + return (((input.get() & 0xFF) << 8) | (input.get() & 0xFF)) + 5; + } + } + + /** + * Returns the required number of bytes in the {@code source} byte array + * necessary to explore SSL/TLS connection. + *

+ * This method tries to parse as few bytes as possible from + * {@code source} byte array to get the length of an + * SSL/TLS record. + * + * @param source + * a byte array containing inbound or outbound network data for + * an SSL/TLS connection. + * @param offset + * the start offset in array {@code source} at which the + * network data is read from. + * @param length + * the maximum number of bytes to read. + * + * @throws BufferUnderflowException if less than {@code RECORD_HEADER_SIZE} + * bytes remaining in {@code source} + * @return the required size in byte to explore an SSL/TLS connection + */ + public static int getRequiredSize(byte[] source, + int offset, int length) throws IOException { + + ByteBuffer byteBuffer = + ByteBuffer.wrap(source, offset, length).asReadOnlyBuffer(); + return getRequiredSize(byteBuffer); + } + + /** + * Launch and explore the security capabilities from byte buffer. + *

+ * This method tries to parse as few records as possible from + * {@code source} byte buffer to get the {@link SSLCapabilities} + * of an SSL/TLS connection. + *

+ * Please NOTE that this method must be called before any handshaking + * occurs. The behavior of this method is not defined in this release + * if the handshake has begun, or has completed. + *

+ * This method accesses the {@code source} parameter in read-only + * mode, and does not update the buffer's properties such as capacity, + * limit, position, and mark values. + * + * @param source + * a {@link ByteBuffer} containing + * inbound or outbound network data for an SSL/TLS connection. + * + * @throws IOException on network data error + * @throws BufferUnderflowException if not enough source bytes available + * to make a complete exploration. + * + * @return the explored {@link SSLCapabilities} of the SSL/TLS + * connection + */ + public static SSLCapabilities explore(ByteBuffer source) + throws IOException { + + ByteBuffer input = source.duplicate(); + + // Do we have a complete header? + if (input.remaining() < RECORD_HEADER_SIZE) { + throw new BufferUnderflowException(); + } + + // Is it a handshake message? + byte firstByte = input.get(); + byte secondByte = input.get(); + byte thirdByte = input.get(); + if ((firstByte & 0x80) != 0 && thirdByte == 0x01) { + // looks like a V2ClientHello + return exploreV2HelloRecord(input, + firstByte, secondByte, thirdByte); + } else if (firstByte == 22) { // 22: handshake record + return exploreTLSRecord(input, + firstByte, secondByte, thirdByte); + } else { + throw new SSLException("Not handshake record"); + } + } + + /** + * Launch and explore the security capabilities from byte array. + *

+ * Please NOTE that this method must be called before any handshaking + * occurs. The behavior of this method is not defined in this release + * if the handshake has begun, or has completed. Once handshake has + * begun, or has completed, the security capabilities can not and + * should not be launched with this method. + * + * @param source + * a byte array containing inbound or outbound network data for + * an SSL/TLS connection. + * @param offset + * the start offset in array {@code source} at which the + * network data is read from. + * @param length + * the maximum number of bytes to read. + * + * @throws IOException on network data error + * @throws BufferUnderflowException if not enough source bytes available + * to make a complete exploration. + * @return the explored {@link SSLCapabilities} of the SSL/TLS + * connection + * + * @see #explore(ByteBuffer) + */ + public static SSLCapabilities explore(byte[] source, + int offset, int length) throws IOException { + ByteBuffer byteBuffer = + ByteBuffer.wrap(source, offset, length).asReadOnlyBuffer(); + return explore(byteBuffer); + } + + /* + * uint8 V2CipherSpec[3]; + * struct { + * uint16 msg_length; // The highest bit MUST be 1; + * // the remaining bits contain the length + * // of the following data in bytes. + * uint8 msg_type; // MUST be 1 + * Version version; + * uint16 cipher_spec_length; // It cannot be zero and MUST be a + * // multiple of the V2CipherSpec length. + * uint16 session_id_length; // This field MUST be empty. + * uint16 challenge_length; // SHOULD use a 32-byte challenge + * V2CipherSpec cipher_specs[V2ClientHello.cipher_spec_length]; + * opaque session_id[V2ClientHello.session_id_length]; + * opaque challenge[V2ClientHello.challenge_length; + * } V2ClientHello; + */ + private static SSLCapabilities exploreV2HelloRecord( + ByteBuffer input, byte firstByte, byte secondByte, + byte thirdByte) throws IOException { + + // We only need the header. We have already had enough source bytes. + // int recordLength = (firstByte & 0x7F) << 8) | (secondByte & 0xFF); + try { + // Is it a V2ClientHello? + if (thirdByte != 0x01) { + throw new SSLException( + "Unsupported or Unrecognized SSL record"); + } + + // What's the hello version? + byte helloVersionMajor = input.get(); + byte helloVersionMinor = input.get(); + + // 0x00: major version of SSLv20 + // 0x02: minor version of SSLv20 + // + // SNIServerName is an extension, SSLv20 doesn't support extension. + return new SSLCapabilitiesImpl((byte) 0x00, (byte) 0x02, + helloVersionMajor, helloVersionMinor, + Collections.emptyList()); + } catch (BufferUnderflowException bufe) { + throw new SSLProtocolException( + "Invalid handshake record"); + } + } + + /* + * struct { + * uint8 major; + * uint8 minor; + * } ProtocolVersion; + * + * enum { + * change_cipher_spec(20), alert(21), handshake(22), + * application_data(23), (255) + * } ContentType; + * + * struct { + * ContentType type; + * ProtocolVersion version; + * uint16 length; + * opaque fragment[TLSPlaintext.length]; + * } TLSPlaintext; + */ + private static SSLCapabilities exploreTLSRecord( + ByteBuffer input, byte firstByte, byte secondByte, + byte thirdByte) throws IOException { + + // Is it a handshake message? + if (firstByte != 22) { // 22: handshake record + throw new SSLException("Not handshake record"); + } + + // We need the record version to construct SSLCapabilities. + byte recordMajorVersion = secondByte; + byte recordMinorVersion = thirdByte; + + // Is there enough data for a full record? + int recordLength = getInt16(input); + if (recordLength > input.remaining()) { + throw new BufferUnderflowException(); + } + + // We have already had enough source bytes. + try { + return exploreHandshake(input, + recordMajorVersion, recordMinorVersion, recordLength); + } catch (BufferUnderflowException bufe) { + throw new SSLProtocolException( + "Invalid handshake record"); + } + } + + /* + * enum { + * hello_request(0), client_hello(1), server_hello(2), + * certificate(11), server_key_exchange (12), + * certificate_request(13), server_hello_done(14), + * certificate_verify(15), client_key_exchange(16), + * finished(20) + * (255) + * } HandshakeType; + * + * struct { + * HandshakeType msg_type; + * uint24 length; + * select (HandshakeType) { + * case hello_request: HelloRequest; + * case client_hello: ClientHello; + * case server_hello: ServerHello; + * case certificate: Certificate; + * case server_key_exchange: ServerKeyExchange; + * case certificate_request: CertificateRequest; + * case server_hello_done: ServerHelloDone; + * case certificate_verify: CertificateVerify; + * case client_key_exchange: ClientKeyExchange; + * case finished: Finished; + * } body; + * } Handshake; + */ + private static SSLCapabilities exploreHandshake( + ByteBuffer input, byte recordMajorVersion, + byte recordMinorVersion, int recordLength) throws IOException { + + // What is the handshake type? + byte handshakeType = input.get(); + if (handshakeType != 0x01) { // 0x01: client_hello message + throw new IllegalStateException("Not initial handshaking"); + } + + // What is the handshake body length? + int handshakeLength = getInt24(input); + + // Theoretically, a single handshake message might span multiple + // records, but in practice this does not occur. + if (handshakeLength > (recordLength - 4)) { // 4: handshake header size + throw new SSLException("Handshake message spans multiple records"); + } + + input = input.duplicate(); + input.limit(handshakeLength + input.position()); + return exploreClientHello(input, + recordMajorVersion, recordMinorVersion); + } + + /* + * struct { + * uint32 gmt_unix_time; + * opaque random_bytes[28]; + * } Random; + * + * opaque SessionID<0..32>; + * + * uint8 CipherSuite[2]; + * + * enum { null(0), (255) } CompressionMethod; + * + * struct { + * ProtocolVersion client_version; + * Random random; + * SessionID session_id; + * CipherSuite cipher_suites<2..2^16-2>; + * CompressionMethod compression_methods<1..2^8-1>; + * select (extensions_present) { + * case false: + * struct {}; + * case true: + * Extension extensions<0..2^16-1>; + * }; + * } ClientHello; + */ + private static SSLCapabilities exploreClientHello( + ByteBuffer input, + byte recordMajorVersion, + byte recordMinorVersion) throws IOException { + + List snList = Collections.emptyList(); + + // client version + byte helloMajorVersion = input.get(); + byte helloMinorVersion = input.get(); + + // ignore random + int position = input.position(); + input.position(position + 32); // 32: the length of Random + + // ignore session id + ignoreByteVector8(input); + + // ignore cipher_suites + ignoreByteVector16(input); + + // ignore compression methods + ignoreByteVector8(input); + + if (input.remaining() > 0) { + snList = exploreExtensions(input); + } + + return new SSLCapabilitiesImpl( + recordMajorVersion, recordMinorVersion, + helloMajorVersion, helloMinorVersion, snList); + } + + /* + * struct { + * ExtensionType extension_type; + * opaque extension_data<0..2^16-1>; + * } Extension; + * + * enum { + * server_name(0), max_fragment_length(1), + * client_certificate_url(2), trusted_ca_keys(3), + * truncated_hmac(4), status_request(5), (65535) + * } ExtensionType; + */ + private static List exploreExtensions(ByteBuffer input) + throws IOException { + + int length = getInt16(input); // length of extensions + while (length > 0) { + int extType = getInt16(input); // extenson type + int extLen = getInt16(input); // length of extension data + + if (extType == 0x00) { // 0x00: type of server name indication + return exploreSNIExt(input, extLen); + } else { // ignore other extensions + ignoreByteVector(input, extLen); + } + + length -= extLen + 4; + } + + return Collections.emptyList(); + } + + /* + * struct { + * NameType name_type; + * select (name_type) { + * case host_name: HostName; + * } name; + * } ServerName; + * + * enum { + * host_name(0), (255) + * } NameType; + * + * opaque HostName<1..2^16-1>; + * + * struct { + * ServerName server_name_list<1..2^16-1> + * } ServerNameList; + */ + private static List exploreSNIExt(ByteBuffer input, + int extLen) throws IOException { + + Map sniMap = new LinkedHashMap<>(); + + int remains = extLen; + if (extLen >= 2) { // "server_name" extension in ClientHello + int listLen = getInt16(input); // length of server_name_list + if (listLen == 0 || listLen + 2 != extLen) { + throw new SSLProtocolException( + "Invalid server name indication extension"); + } + + remains -= 2; // 0x02: the length field of server_name_list + while (remains > 0) { + int code = getInt8(input); // name_type + int snLen = getInt16(input); // length field of server name + if (snLen > remains) { + throw new SSLProtocolException( + "Not enough data to fill declared vector size"); + } + byte[] encoded = new byte[snLen]; + input.get(encoded); + + SNIServerName serverName; + switch (code) { + case StandardConstants.SNI_HOST_NAME: + if (encoded.length == 0) { + throw new SSLProtocolException( + "Empty HostName in server name indication"); + } + serverName = new SNIHostName(encoded); + break; + default: + serverName = new UnknownServerName(code, encoded); + } + // check for duplicated server name type + if (sniMap.put(serverName.getType(), serverName) != null) { + throw new SSLProtocolException("Duplicated server name of type " + serverName.getType()); + } + + remains -= encoded.length + 3; // NameType: 1 byte + // HostName length: 2 bytes + } + } else if (extLen == 0) { // "server_name" extension in ServerHello + throw new SSLProtocolException( + "Not server name indication extension in client"); + } + + if (remains != 0) { + throw new SSLProtocolException( + "Invalid server name indication extension"); + } + + return Collections.unmodifiableList( + new ArrayList<>(sniMap.values())); + } + + private static int getInt8(ByteBuffer input) { + return input.get(); + } + + private static int getInt16(ByteBuffer input) { + return ((input.get() & 0xFF) << 8) | (input.get() & 0xFF); + } + + private static int getInt24(ByteBuffer input) { + return ((input.get() & 0xFF) << 16) | ((input.get() & 0xFF) << 8) | (input.get() & 0xFF); + } + + private static void ignoreByteVector8(ByteBuffer input) { + ignoreByteVector(input, getInt8(input)); + } + + private static void ignoreByteVector16(ByteBuffer input) { + ignoreByteVector(input, getInt16(input)); + } + + private static void ignoreByteVector24(ByteBuffer input) { + ignoreByteVector(input, getInt24(input)); + } + + private static void ignoreByteVector(ByteBuffer input, int length) { + if (length != 0) { + int position = input.position(); + input.position(position + length); + } + } + + private static class UnknownServerName extends SNIServerName { + UnknownServerName(int code, byte[] encoded) { + super(code, encoded); + } + } + + private static final class SSLCapabilitiesImpl extends SSLCapabilities { + private static final Map versionMap = new HashMap<>(5); + + private final String recordVersion; + private final String helloVersion; + List sniNames; + + static { + versionMap.put(0x0002, "SSLv2Hello"); + versionMap.put(0x0300, "SSLv3"); + versionMap.put(0x0301, "TLSv1"); + versionMap.put(0x0302, "TLSv1.1"); + versionMap.put(0x0303, "TLSv1.2"); + } + + SSLCapabilitiesImpl(byte recordMajorVersion, byte recordMinorVersion, + byte helloMajorVersion, byte helloMinorVersion, + List sniNames) { + + int version = (recordMajorVersion << 8) | recordMinorVersion; + this.recordVersion = versionMap.get(version) != null + ? versionMap.get(version) + : unknownVersion(recordMajorVersion, recordMinorVersion); + + version = (helloMajorVersion << 8) | helloMinorVersion; + this.helloVersion = versionMap.get(version) != null + ? versionMap.get(version) + : unknownVersion(helloMajorVersion, helloMinorVersion); + + this.sniNames = sniNames; + } + + @Override + public String getRecordVersion() { + return recordVersion; + } + + @Override + public String getHelloVersion() { + return helloVersion; + } + + @Override + public List getServerNames() { + if (!sniNames.isEmpty()) { + return Collections.unmodifiableList(sniNames); + } + + return sniNames; + } + + private static String unknownVersion(byte major, byte minor) { + return "Unknown-" + ((int) major) + "." + ((int) minor); + } + } +} + diff --git a/tests/e2e-tls/src/test/resources/hosts b/tests/e2e-tls/src/test/resources/hosts new file mode 100644 index 00000000000..dc663ff45fe --- /dev/null +++ b/tests/e2e-tls/src/test/resources/hosts @@ -0,0 +1,4 @@ +127.0.0.1 www.host0.com +127.0.0.1 www.host1.com +127.0.0.1 www.host2.com +127.0.0.1 www.host3.com \ No newline at end of file diff --git a/tests/pom.xml b/tests/pom.xml index 3966d7d5fe7..a140de6db28 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -1,7 +1,7 @@