SniTest.java
/*
* Copyright (c) 2023, 2024 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", "www.host1.com");
}
@ParameterizedTest
@MethodSource("getConnectors")
public void sniHostNamePropertyTest(ConnectorProvider provider) {
ClientConfig clientConfig = new ClientConfig();
clientConfig.connectorProvider(provider);
clientConfig.property(ClientProperties.SNI_HOST_NAME, "www.host3.com");
serverTest(clientConfig, "www.host4.com", "www.host3.com");
}
@ParameterizedTest
@MethodSource("getConnectors")
public void turnOffSniTest(ConnectorProvider provider) {
ClientConfig clientConfig = new ClientConfig();
clientConfig.connectorProvider(provider);
clientConfig.property(ClientProperties.SNI_HOST_NAME, LOCALHOST);
serverTest(clientConfig, "www.host4.com", null);
}
public void serverTest(ClientConfig clientConfig, String hostName, String resultHostName) {
String newHostName = replaceWhenHostNotKnown(hostName);
final List<SNIServerName> 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 (resultHostName != null && serverNames.isEmpty()) {
throw new IllegalStateException("ServerNames are empty");
} else if (resultHostName == null && serverNames.isEmpty()) {
return;
}
String clientSniName = new String(serverNames.get(0).getEncoded());
if (!resultHostName.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;
}
}
}