Skip to content

Commit

Permalink
Support SNI for JNH connector
Browse files Browse the repository at this point in the history
Signed-off-by: jansupol <[email protected]>
  • Loading branch information
jansupol committed Apr 20, 2023
1 parent 71aedfc commit bfb3d64
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, 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
Expand Down Expand Up @@ -57,24 +57,49 @@ public class JavaNetHttpClientProperties {
public static final String SSL_PARAMETERS = "jersey.config.jnh.client.sslParameters";

/**
* An instance of the {@link java.net.Authenticator} class that should be used to retrieve
* credentials from a user.
*
* <p>
* An instance of the {@link java.net.Authenticator} class that should be used to retrieve
* credentials from a user.
* </p>
* <p>
* The name of the configuration property is <tt>{@value}</tt>.
* </p>
*/
public static final String PREEMPTIVE_BASIC_AUTHENTICATION =
"jersey.config.jnh.client.preemptiveBasicAuthentication";

/**
* A value of {@code false} indicates the client should handle cookies
* automatically using HttpClient's default cookie policy. A value
* of {@code false} will cause the client to ignore all cookies.
* <p/>
* The value MUST be an instance of {@link java.lang.Boolean}.
* If the property is absent the default value is {@code false}
* <p>
* A value of {@code false} indicates the client should handle cookies
* automatically using HttpClient's default cookie policy. A value
* of {@code false} will cause the client to ignore all cookies.
* </p>
* <p>
* The value MUST be an instance of {@link java.lang.Boolean}.
* If the property is absent the default value is {@code false}
* </p>
* <p>
* The name of the configuration property is <tt>{@value}</tt>.
* </p>
*/
public static final String DISABLE_COOKIES =
"jersey.config.jnh.client.disableCookies";

/**
* <p>
* Java Net Http client sets SslProperties per client rather than per request.
* While most connectors support HOST header to set the SNIHostName, the headers cannot be set per client,
* and SNIHostName is set by this property.
* </p>
* <p>
* The value MUST be an instance of {@link java.lang.Boolean}.
* </p>
* <p>
* The name of the configuration property is <tt>{@value}</tt>.
* </p>
* @since 3.1.2
*/
public static final String SNI_HOST_NAME = "jersey.config.jnh.client.SniHostName";

/**
* Prevent this class from instantiation.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, 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
Expand All @@ -19,12 +19,14 @@
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.core.Configuration;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MultivaluedMap;
import org.glassfish.jersey.client.ClientProperties;
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.Expect100ContinueUsage;
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.Version;
Expand All @@ -49,14 +51,13 @@
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.logging.Logger;
import java.util.zip.GZIPOutputStream;

/**
* Provides a Jersey client {@link Connector}, which internally uses Java's {@link HttpClient}.
Expand Down Expand Up @@ -104,11 +105,14 @@ public JavaNetHttpConnector(final Client client, final Configuration configurati
} else {
httpClientBuilder.followRedirects(HttpClient.Redirect.NORMAL);
}

SSLParameters sslParameters =
getPropertyOrNull(configuration, JavaNetHttpClientProperties.SSL_PARAMETERS, SSLParameters.class);
sslParameters = new SniSslParameters(sslParameters).getSslParameters(client);
if (sslParameters != null) {
httpClientBuilder.sslParameters(sslParameters);
}

final Authenticator preemptiveAuthenticator =
getPropertyOrNull(configuration,
JavaNetHttpClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, Authenticator.class);
Expand Down Expand Up @@ -170,8 +174,20 @@ public OutputStream getOutputStream(int contentLength) throws IOException {
* @return the {@link HttpRequest} instance for the {@link HttpClient} request
*/
private HttpRequest getHttpRequest(ClientRequest request) {
final String sniHostName = sniHostName(request.getConfiguration());
final URI sniUri;
if (sniHostName != null) {
final SSLParamConfigurator sniConfig = SSLParamConfigurator.builder()
.uri(request.getUri())
.headers(Map.of(HttpHeaders.HOST, List.of(sniHostName)))
.build();
sniUri = sniConfig.isSNIRequired() ? sniConfig.toIPRequestUri() : request.getUri();
} else {
sniUri = request.getUri();
}

HttpRequest.Builder builder = HttpRequest.newBuilder();
builder.uri(request.getUri());
builder.uri(sniUri);
HttpRequest.BodyPublisher bodyPublisher = HttpRequest.BodyPublishers.noBody();
if (request.hasEntity()) {
try {
Expand Down Expand Up @@ -296,4 +312,41 @@ public CookieHandler getCookieHandler() {
}
return null;
}

private static class SniSslParameters {
private final SSLParameters sslParameters;

private SniSslParameters(SSLParameters sslParameters) {
this.sslParameters = sslParameters;
}

private SSLParameters getSslParameters(Client client) {
String sniHostName = sniHostName(client.getConfiguration());
SSLParameters sslParameters = this.sslParameters;
if (sniHostName != null) {
if (sslParameters == null) {
sslParameters = new SSLParameters();
}
SSLParamConfigurator sniConfig = SSLParamConfigurator.builder()
.headers(Collections.singletonMap(HttpHeaders.HOST, List.of(sniHostName)))
.build();
sniConfig.setSNIServerName(sslParameters);
}

return sslParameters;
}
}

private static String sniHostName(Configuration configuration) {
final String sniHostname;
if (configuration.hasProperty(JavaNetHttpClientProperties.SNI_HOST_NAME)) {
sniHostname = (String) configuration.getProperty(JavaNetHttpClientProperties.SNI_HOST_NAME);
} else if (configuration.hasProperty(HttpHeaders.HOST)) {
sniHostname = (String) configuration.getProperty(HttpHeaders.HOST);
} else {
sniHostname = null;
}
return sniHostname;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ public void setSNIServerName(SSLSocket sslSocket) {
sniConfigurator.ifPresent(sni -> sni.setServerNames(sslSocket));
}

/**
* Set {@link javax.net.ssl.SNIServerName} for the {@link SSLParameters} when SNI should be used
* (i.e. {@link jakarta.ws.rs.core.HttpHeaders#HOST} differs from HTTP request host name)
* @param parameters the {@link SSLParameters} to be set
*/
public void setSNIServerName(SSLParameters parameters) {
sniConfigurator.ifPresent(sni -> sni.updateSSLParameters(parameters));
}

/**
* Set setEndpointIdentificationAlgorithm to HTTPS. This is to prevent man-in-the-middle attacks.
* @param sslEngine the {@link SSLEngine} the algorithm is set for.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,11 @@ static Optional<SniConfigurator> createWhenHostHeader(URI hostUri, Map<String, L
return Optional.empty();
}

final String hostUriString = hostUri.getHost();
if (!whenDiffer && hostUriString.equals(trimmedHeader)) {
return Optional.empty();
if (hostUri != null) {
final String hostUriString = hostUri.getHost();
if (!whenDiffer && hostUriString.equals(trimmedHeader)) {
return Optional.empty();
}
}

return Optional.of(new SniConfigurator(trimmedHeader));
Expand All @@ -97,7 +99,7 @@ void setServerNames(SSLSocket sslSocket) {
sslSocket.setSSLParameters(sslParameters);
}

private SSLParameters updateSSLParameters(SSLParameters sslParameters) {
SSLParameters updateSSLParameters(SSLParameters sslParameters) {
SNIHostName serverName = new SNIHostName(hostName);
List<SNIServerName> serverNames = new LinkedList<>();
serverNames.add(serverName);
Expand Down
6 changes: 6 additions & 0 deletions tests/e2e-tls/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<skipTests>${skip.e2e}</skipTests>
<systemPropertyVariables>
<sun.net.http.allowRestrictedHeaders>true</sun.net.http.allowRestrictedHeaders>
<jdk.httpclient.allowRestrictedHeaders>Host</jdk.httpclient.allowRestrictedHeaders>
<property>
<name>ssl.debug</name>
<value>true</value>
Expand Down Expand Up @@ -109,6 +110,11 @@
<artifactId>jersey-jdk-connector</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.connectors</groupId>
<artifactId>jersey-jnh-connector</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.security</groupId>
<artifactId>oauth1-signature</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
import org.glassfish.jersey.client.HttpUrlConnectorProvider;
import org.glassfish.jersey.client.spi.ConnectorProvider;
import org.glassfish.jersey.jdk.connector.JdkConnectorProvider;
import org.glassfish.jersey.jnh.connector.JavaNetHttpClientProperties;
import org.glassfish.jersey.jnh.connector.JavaNetHttpConnectorProvider;
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;

Expand All @@ -39,13 +40,9 @@
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 {
Expand All @@ -54,6 +51,9 @@ public class SniTest {


static {
// Debug
// System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
// System.setProperty("jdk.httpclient.allowRestrictedHeaders", "Host");
// JDK specific settings
System.setProperty("jdk.net.hosts.file", SniTest.class.getResource("/hosts").getPath());
}
Expand All @@ -64,7 +64,8 @@ public static ConnectorProvider[] getConnectors() {
new ApacheConnectorProvider(),
new Apache5ConnectorProvider(),
new JdkConnectorProvider(),
new HttpUrlConnectorProvider()
new HttpUrlConnectorProvider(),
new JavaNetHttpConnectorProvider()
};
}

Expand All @@ -73,6 +74,7 @@ public static ConnectorProvider[] getConnectors() {
public void server1Test(ConnectorProvider provider) {
ClientConfig clientConfig = new ClientConfig();
clientConfig.connectorProvider(provider);
clientConfig.property(JavaNetHttpClientProperties.SNI_HOST_NAME, "www.host1.com");
serverTest(clientConfig, "www.host1.com");
}

Expand All @@ -99,6 +101,7 @@ public void filter(ClientRequestContext requestContext) throws IOException {
}
})
.target("https://" + (newHostName.equals(LOCALHOST) ? LOCALHOST : "www.host0.com") + ":" + PORT)
//.target("https://127.0.0.1:" + PORT)
.path("host")
.request()
.header(HttpHeaders.HOST, hostName + ":8080")
Expand All @@ -111,7 +114,7 @@ public void filter(ClientRequestContext requestContext) throws IOException {
&& TimeoutException.class.isInstance(cause)) {
cause = cause.getCause();
}
if (cause == null && /*IOE*/ !e.getMessage().contains("Stream closed")) {
if ((!e.getMessage().contains("Stream closed")) && !e.getMessage().contains("timed out")) {
throw e;
}
}
Expand Down

0 comments on commit bfb3d64

Please sign in to comment.