Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
688 lines
38 KiB
Java
688 lines
38 KiB
Java
/*
|
|
* Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
import java.io.IOException;
|
|
import java.io.OutputStream;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.URI;
|
|
import java.net.http.HttpClient;
|
|
import java.net.http.HttpRequest;
|
|
import java.net.http.HttpResponse;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.security.KeyPair;
|
|
import java.security.KeyStore;
|
|
import java.security.SecureRandom;
|
|
import java.security.cert.Certificate;
|
|
import java.security.cert.X509Certificate;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
import java.util.Optional;
|
|
|
|
import javax.net.ssl.SSLContext;
|
|
import javax.net.ssl.TrustManagerFactory;
|
|
|
|
import jdk.httpclient.test.lib.common.DynamicKeyStoreUtil;
|
|
import jdk.httpclient.test.lib.common.HttpServerAdapters;
|
|
import jdk.httpclient.test.lib.common.ServerNameMatcher;
|
|
import jdk.httpclient.test.lib.http2.Http2TestServer;
|
|
import jdk.httpclient.test.lib.http3.Http3TestServer;
|
|
import jdk.httpclient.test.lib.quic.QuicServer;
|
|
import jdk.test.lib.net.URIBuilder;
|
|
import org.junit.jupiter.api.Test;
|
|
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
|
import static java.net.http.HttpClient.Version.HTTP_2;
|
|
import static java.net.http.HttpClient.Version.HTTP_3;
|
|
import static java.net.http.HttpOption.Http3DiscoveryMode.ALT_SVC;
|
|
import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY;
|
|
import static java.net.http.HttpOption.H3_DISCOVERY;
|
|
import static jdk.httpclient.test.lib.common.DynamicKeyStoreUtil.generateCert;
|
|
import static jdk.httpclient.test.lib.common.DynamicKeyStoreUtil.generateKeyStore;
|
|
import static jdk.httpclient.test.lib.common.DynamicKeyStoreUtil.generateRSAKeyPair;
|
|
import static jdk.httpclient.test.lib.http3.Http3TestServer.quicServerBuilder;
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
|
/*
|
|
* @test
|
|
* @summary verifies the HttpClient's usage of alternate services
|
|
* @comment The goal of this test class is to run various tests to verify that the HttpClient
|
|
* (and the underlying layers) use an alternate server for HTTP request(s) IF AND ONLY IF such an
|
|
* advertised alternate server satisfies "reasonable assurance" expectations as noted in the
|
|
* alternate service RFC-7838. Reasonable assurance can be summarized as:
|
|
* - The origin server which advertised the alternate service, MUST be running on TLS
|
|
* - The certificate presented by origin server during TLS handshake must be valid (and trusted)
|
|
* for the origin server
|
|
* - The certificate presented by alternate server (when subsequently a connection attempt is
|
|
* made to it) MUST be valid (and trusted) for the ORIGIN server
|
|
* @library /test/lib /test/jdk/java/net/httpclient/lib
|
|
* @build jdk.httpclient.test.lib.common.HttpServerAdapters
|
|
* jdk.httpclient.test.lib.common.DynamicKeyStoreUtil
|
|
* jdk.test.lib.net.URIBuilder
|
|
* @modules java.base/sun.net.www.http
|
|
* java.net.http/jdk.internal.net.http.common
|
|
* java.net.http/jdk.internal.net.http.frame
|
|
* java.net.http/jdk.internal.net.http.hpack
|
|
* java.base/jdk.internal.net.quic
|
|
* java.net.http/jdk.internal.net.http.quic
|
|
* java.net.http/jdk.internal.net.http.quic.packets
|
|
* java.net.http/jdk.internal.net.http.quic.frames
|
|
* java.net.http/jdk.internal.net.http.quic.streams
|
|
* java.net.http/jdk.internal.net.http.http3.streams
|
|
* java.net.http/jdk.internal.net.http.http3.frames
|
|
* java.net.http/jdk.internal.net.http.http3
|
|
* java.net.http/jdk.internal.net.http.qpack
|
|
* java.net.http/jdk.internal.net.http.qpack.readers
|
|
* java.net.http/jdk.internal.net.http.qpack.writers
|
|
* @modules java.base/sun.security.x509
|
|
* java.base/jdk.internal.util
|
|
* @run junit/othervm -Djdk.net.hosts.file=${test.src}/altsvc-dns-hosts.txt
|
|
* -Djdk.internal.httpclient.debug=true -Djavax.net.debug=all
|
|
* -Djdk.httpclient.HttpClient.log=requests,responses,errors
|
|
* AltServiceReasonableAssurance
|
|
*/
|
|
public class AltServiceReasonableAssurance implements HttpServerAdapters {
|
|
|
|
private static final String ORIGIN_SERVER_HOSTNAME = "origin.server";
|
|
private static final String ALT_SERVER_HOSTNAME = "altservice.server";
|
|
|
|
private static final String ALT_SERVER_RESPONSE_MESSAGE = "Hello from an alt server";
|
|
private static final String ORIGIN_SERVER_RESPONSE_MESSAGE = "Hello from an origin server";
|
|
|
|
private record TestInput(HttpTestServer originServer, HttpTestServer altServer,
|
|
URI requestURI, String expectedAltSvcHeader) {
|
|
}
|
|
|
|
/**
|
|
* Creates and starts a origin server and an alternate server. The passed (same) SSLContext
|
|
* is used by both the origin server and the alternate server.
|
|
*/
|
|
private static TestInput startOriginAndAltServer(final SSLContext sslContext)
|
|
throws Exception {
|
|
Objects.requireNonNull(sslContext);
|
|
return startOriginAndAltServer(sslContext, sslContext);
|
|
}
|
|
|
|
/**
|
|
* Creates and starts a origin server and an alternate server. The origin server will use
|
|
* the {@code originSrvSSLCtx} and the alternate server will use the {@code altSrvSSLCtx}
|
|
*/
|
|
private static TestInput startOriginAndAltServer(final SSLContext originSrvSSLCtx,
|
|
final SSLContext altSrvSSLCtx)
|
|
throws Exception {
|
|
Objects.requireNonNull(originSrvSSLCtx);
|
|
Objects.requireNonNull(altSrvSSLCtx);
|
|
final String requestPath = "/hello";
|
|
final QuicServer quicServer = quicServerBuilder()
|
|
.sslContext(altSrvSSLCtx)
|
|
// the client sends a SNI for origin server. this alt server should be capable
|
|
// of matching/accepting that SNI name of the origin
|
|
.sniMatcher(new ServerNameMatcher(ORIGIN_SERVER_HOSTNAME))
|
|
.build();
|
|
// Alt server only supports H3
|
|
final HttpTestServer altServer = HttpTestServer.of(new Http3TestServer(quicServer));
|
|
altServer.addHandler(new Handler(ALT_SERVER_RESPONSE_MESSAGE), requestPath);
|
|
altServer.start();
|
|
System.out.println("Alt server started at " + altServer.getAddress());
|
|
|
|
// H2 server which has a (application level) handler which advertises H3 alt service
|
|
final HttpTestServer originServer = HttpTestServer.of(
|
|
new Http2TestServer(ORIGIN_SERVER_HOSTNAME, true, originSrvSSLCtx));
|
|
final int altServerPort = altServer.getAddress().getPort();
|
|
final String altSvcHeaderVal = "h3=\"" + ALT_SERVER_HOSTNAME + ":" + altServerPort + "\"";
|
|
originServer.addHandler(new Handler(ORIGIN_SERVER_RESPONSE_MESSAGE, altSvcHeaderVal),
|
|
requestPath);
|
|
originServer.start();
|
|
System.out.println("Origin server started at " + originServer.getAddress());
|
|
// request URI should be directed to the origin server
|
|
final URI requestURI = URIBuilder.newBuilder()
|
|
.scheme("https")
|
|
.host(ORIGIN_SERVER_HOSTNAME)
|
|
.port(originServer.getAddress().getPort())
|
|
.path(requestPath)
|
|
.build();
|
|
return new TestInput(originServer, altServer, requestURI, altSvcHeaderVal);
|
|
}
|
|
|
|
private TestInput startHttpOriginHttpsAltServer(final SSLContext altServerSSLCtx)
|
|
throws Exception {
|
|
Objects.requireNonNull(altServerSSLCtx);
|
|
final String requestPath = "/foo";
|
|
// Alt server only supports H3
|
|
final HttpTestServer altServer = HttpTestServer.create(HTTP_3_URI_ONLY, altServerSSLCtx);
|
|
altServer.addHandler(new Handler(ALT_SERVER_RESPONSE_MESSAGE), requestPath);
|
|
altServer.start();
|
|
System.out.println("Alt server (HTTPS) started at " + altServer.getAddress());
|
|
|
|
// supports only HTTP server and uses a (application level) handler which advertises a H3
|
|
// alternate service
|
|
final HttpTestServer originServer = HttpTestServer.create(HTTP_2);
|
|
final int altServerPort = altServer.getAddress().getPort();
|
|
final String altSvcHeaderVal = "h3=\"" + ALT_SERVER_HOSTNAME + ":" + altServerPort + "\"";
|
|
originServer.addHandler(new Handler(ORIGIN_SERVER_RESPONSE_MESSAGE, altSvcHeaderVal),
|
|
requestPath);
|
|
originServer.start();
|
|
System.out.println("Origin server (HTTP) started at " + originServer.getAddress());
|
|
// request URI should be against (HTTP) origin server
|
|
final URI requestURI = URIBuilder.newBuilder()
|
|
.scheme("http")
|
|
.host(ORIGIN_SERVER_HOSTNAME)
|
|
.port(originServer.getAddress().getPort())
|
|
.path(requestPath)
|
|
.build();
|
|
return new TestInput(originServer, altServer, requestURI, altSvcHeaderVal);
|
|
}
|
|
|
|
/**
|
|
* Stop the server (and ignore any exception)
|
|
*/
|
|
private static void safeStop(final HttpTestServer server) {
|
|
if (server == null) {
|
|
return;
|
|
}
|
|
final InetSocketAddress serverAddr = server.getAddress();
|
|
try {
|
|
System.out.println("Stopping server " + serverAddr);
|
|
server.stop();
|
|
} catch (Exception e) {
|
|
System.err.println("Ignoring exception: " + e.getMessage() + " that occurred " +
|
|
"during stop of server: " + serverAddr);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns back a 200 HTTP response with a response body containing a response message
|
|
* that was used to construct the Handler instance. Additionally, if the Handler was constructed
|
|
* with a non-null {@code altSvcHeaderVal} then that value is sent back as a header value. in
|
|
* the response, for the {@code alt-svc} header
|
|
*/
|
|
private static final class Handler implements HttpTestHandler {
|
|
private final String responseMessage;
|
|
private final byte[] responseBytes;
|
|
private final String altSvcHeaderVal;
|
|
|
|
private Handler(final String responseMessage) {
|
|
this(responseMessage, null);
|
|
}
|
|
|
|
private Handler(final String responseMessage, final String altSvcHeaderVal) {
|
|
Objects.requireNonNull(responseMessage);
|
|
this.responseMessage = responseMessage;
|
|
this.responseBytes = responseMessage.getBytes(StandardCharsets.UTF_8);
|
|
this.altSvcHeaderVal = altSvcHeaderVal;
|
|
}
|
|
|
|
@Override
|
|
public void handle(final HttpTestExchange exchange) throws IOException {
|
|
System.out.println("Handling request " + exchange.getRequestURI());
|
|
if (this.altSvcHeaderVal != null) {
|
|
System.out.println("Responding with alt-svc header: " + this.altSvcHeaderVal);
|
|
exchange.getResponseHeaders().addHeader("alt-svc", this.altSvcHeaderVal);
|
|
}
|
|
System.out.println("Responding with body: " + this.responseMessage);
|
|
exchange.sendResponseHeaders(200, this.responseBytes.length);
|
|
try (final OutputStream os = exchange.getResponseBody()) {
|
|
os.write(this.responseBytes);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* - Keystore K1 is constructed with a certificate whose subject is origin server hostname and
|
|
* subject alternative name is alternate server hostname
|
|
* - K1 is used to construct a SSLContext and thus the SSLContext uses the keys and trusted
|
|
* certificate from this keystore
|
|
* - The constructed SSLContext instance is used by the HttpClient, the origin server and the
|
|
* alternate server
|
|
* - During TLS handshake with origin server, the origin server is expected to present the
|
|
* certificate from this K1 keystore.
|
|
* - During TLS handshake with alternate server, the alternate server is expected to present
|
|
* this same certificate from K1 keystore.
|
|
* - Since the certificate is valid (and trusted by the client) for both origin server
|
|
* and alternate server (because of the valid subject name and subject alternate name),
|
|
* the TLS handshake between the HttpClient and the origin and alternate server is expected
|
|
* to pass
|
|
* <p>
|
|
* Once the servers are started, this test method does the following:
|
|
* <p>
|
|
* - Client constructs a HTTP_3 request addressed to origin server
|
|
* - Origin server responds with a 200 response and also with alt-svc header pointing to
|
|
* an alternate server
|
|
* - Client verifies the response as well as presence of the alt-svc header value
|
|
* - Client issues the *same* request again
|
|
* - The request is expected to be handled by the alternate server
|
|
*/
|
|
@Test
|
|
public void testOriginAltSameCert() throws Exception {
|
|
// create a keystore which contains a PrivateKey entry and a certificate associated with
|
|
// that key. the certificate's subject will be origin server's hostname and will
|
|
// additionally have the alt server hostname as a subject alternate name. Thus, the
|
|
// certificate is valid for both origin server and alternate server
|
|
final KeyStore keyStore = generateKeyStore(ORIGIN_SERVER_HOSTNAME, ALT_SERVER_HOSTNAME);
|
|
System.out.println("Generated a keystore with certificate: " +
|
|
keyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS));
|
|
// create a SSLContext that will be used by the servers and the HttpClient and will be
|
|
// backed by the keystore we just created. Thus, the HttpClient will trust the certificate
|
|
// belonging to that keystore
|
|
final SSLContext sslContext = DynamicKeyStoreUtil.createSSLContext(keyStore);
|
|
// start the servers
|
|
final TestInput testInput = startOriginAndAltServer(sslContext);
|
|
try {
|
|
final HttpClient client = newClientBuilderForH3()
|
|
.proxy(NO_PROXY)
|
|
.sslContext(sslContext)
|
|
.version(HTTP_3)
|
|
.build();
|
|
// send a HTTP3 request to a server which is expected to respond back
|
|
// with a 200 response and an alt-svc header pointing to another/different H3 server
|
|
final URI requestURI = testInput.requestURI;
|
|
final HttpRequest request = HttpRequest.newBuilder()
|
|
.GET().uri(requestURI)
|
|
.setOption(H3_DISCOVERY, ALT_SVC)
|
|
.build();
|
|
System.out.println("Issuing request " + requestURI);
|
|
final HttpResponse<String> response = client.send(request,
|
|
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
|
assertEquals(200, response.statusCode(), "Unexpected response code");
|
|
// the origin server is expected to respond
|
|
assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, response.body(), "Unexpected response" +
|
|
" body");
|
|
assertEquals(HTTP_2, response.version(), "Unexpected HTTP version in response");
|
|
|
|
// verify the origin server sent back a alt-svc header
|
|
final Optional<String> altSvcHeader = response.headers().firstValue("alt-svc");
|
|
assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response");
|
|
final String actualAltSvcHeader = altSvcHeader.get();
|
|
System.out.println("Received alt-svc header value: " + actualAltSvcHeader);
|
|
assertTrue(actualAltSvcHeader.contains(testInput.expectedAltSvcHeader),
|
|
"Unexpected alt-svc header value: " + actualAltSvcHeader
|
|
+ ", was expected to contain: " + testInput.expectedAltSvcHeader);
|
|
|
|
// now issue the same request again and this time expect it to be handled
|
|
// by the alt-service
|
|
System.out.println("Again issuing request " + requestURI);
|
|
final HttpResponse<String> secondResponse = client.send(request,
|
|
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
|
assertEquals(200, secondResponse.statusCode(), "Unexpected response code");
|
|
// expect the alt service to respond
|
|
assertEquals(ALT_SERVER_RESPONSE_MESSAGE, secondResponse.body(), "Unexpected response" +
|
|
" body");
|
|
assertEquals(HTTP_3, secondResponse.version(), "Unexpected HTTP version in response");
|
|
} finally {
|
|
safeStop(testInput.originServer);
|
|
safeStop(testInput.altServer);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* - Keystore K1 is constructed with a PrivateKey PK1 and certificate whose subject is
|
|
* origin server hostname
|
|
* - Keystore K2 is constructed with the same PrivateKey PK1 and certificate whose subject is
|
|
* alternate server hostname AND has a subject alternate name of origin server hostname
|
|
* - K1 is used to construct a SSLContext S1 and that S1 is used by origin server
|
|
* - K2 is used to construct a SSLContext S2 and that S2 is used by alternate server
|
|
* - SSLContext S3 is constructed with both the certificate of origin server and
|
|
* the certificate of alternate server as trusted certificates. HttpClient uses S3
|
|
* - During TLS handshake with origin server, the origin server is expected to present the
|
|
* certificate from this K1 keystore, with subject as origin server hostname
|
|
* - During TLS handshake with alternate server, the alternate server is expected to present
|
|
* the certificate from K2 keystore, with subject as alternate server hostname AND a subject
|
|
* alternate name of origin server
|
|
* - HttpClient (through S3 SSLContext) trusts both these certs. The cert presented
|
|
* by alt server, is valid (even) for origin server (since its subject alternate name is
|
|
* origin server hostname). Thus, the client must consider the alternate service as valid and
|
|
* use it.
|
|
* <p>
|
|
* Once the servers are started, this test method does the following:
|
|
* <p>
|
|
* - Client constructs a HTTP_3 request addressed to origin server
|
|
* - Origin server responds with a 200 response and also with alt-svc header pointing to
|
|
* an alternate server
|
|
* - Client verifies the response as well as presence of the alt-svc header value
|
|
* - Client issues the *same* request again
|
|
* - The request is expected to be handled by the alternate server
|
|
*/
|
|
@Test
|
|
public void testOriginAltDifferentCert() throws Exception {
|
|
final SecureRandom secureRandom = new SecureRandom();
|
|
final KeyPair keyPair = generateRSAKeyPair(secureRandom);
|
|
|
|
// generate a certificate for origin server, with origin server hostname as the subject
|
|
final X509Certificate originServerCert = generateCert(keyPair, secureRandom,
|
|
ORIGIN_SERVER_HOSTNAME);
|
|
// create a keystore with the private key and the cert. this keystore will then be
|
|
// used by the SSLContext of origin server
|
|
final KeyStore originServerKeyStore = generateKeyStore(keyPair.getPrivate(),
|
|
new Certificate[]{originServerCert});
|
|
System.out.println("Generated a keystore, for origin server, with certificate: " +
|
|
originServerKeyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS));
|
|
// create the SSLContext for the origin server
|
|
final SSLContext originServerSSLCtx = DynamicKeyStoreUtil.createSSLContext(
|
|
originServerKeyStore);
|
|
|
|
// create a cert for the alternate server, with alternate server hostname as the subject
|
|
// AND origin server hostname as a subject alternate name
|
|
final X509Certificate altServerCert = generateCert(keyPair, secureRandom,
|
|
ALT_SERVER_HOSTNAME, ORIGIN_SERVER_HOSTNAME);
|
|
// create keystore with the private key and the alt server's cert. this keystore will then
|
|
// be used by the SSLContext of alternate server
|
|
final KeyStore altServerKeyStore = generateKeyStore(keyPair.getPrivate(),
|
|
new Certificate[]{altServerCert});
|
|
System.out.println("Generated a keystore, for alt server, with certificate: " +
|
|
altServerKeyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS));
|
|
// create SSLContext of alternate server
|
|
final SSLContext altServerSSLCtx = DynamicKeyStoreUtil.createSSLContext(altServerKeyStore);
|
|
|
|
// now create a SSLContext for the HttpClient. This SSLContext will contain no key manager
|
|
// and will have a trust manager which trusts origin server certificate and the alternate
|
|
// server certificate
|
|
final SSLContext clientSSLCtx = sslCtxWithTrustedCerts(List.of(originServerCert,
|
|
altServerCert));
|
|
// start the servers
|
|
final TestInput testInput = startOriginAndAltServer(originServerSSLCtx, altServerSSLCtx);
|
|
try {
|
|
final HttpClient client = newClientBuilderForH3()
|
|
.proxy(NO_PROXY)
|
|
.sslContext(clientSSLCtx)
|
|
.version(HTTP_3)
|
|
.build();
|
|
// send a HTTP3 request to a server which is expected to respond back
|
|
// with a 200 response and an alt-svc header pointing to another/different H3 server
|
|
final URI requestURI = testInput.requestURI;
|
|
final HttpRequest request = HttpRequest.newBuilder()
|
|
.GET().uri(requestURI)
|
|
.setOption(H3_DISCOVERY, ALT_SVC)
|
|
.build();
|
|
System.out.println("Issuing request " + requestURI);
|
|
final HttpResponse<String> response = client.send(request,
|
|
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
|
assertEquals(200, response.statusCode(), "Unexpected response code");
|
|
// the origin server is expected to respond
|
|
assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, response.body(), "Unexpected response" +
|
|
" body");
|
|
assertEquals(HTTP_2, response.version(), "Unexpected HTTP version in response");
|
|
|
|
// verify the origin server sent back a alt-svc header
|
|
final Optional<String> altSvcHeader = response.headers().firstValue("alt-svc");
|
|
assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response");
|
|
final String actualAltSvcHeader = altSvcHeader.get();
|
|
System.out.println("Received alt-svc header value: " + actualAltSvcHeader);
|
|
assertTrue(actualAltSvcHeader.contains(testInput.expectedAltSvcHeader),
|
|
"Unexpected alt-svc header value: " + actualAltSvcHeader
|
|
+ ", was expected to contain: " + testInput.expectedAltSvcHeader);
|
|
|
|
// now issue the same request again and this time expect it to be handled
|
|
// by the alt-service
|
|
System.out.println("Again issuing request " + requestURI);
|
|
final HttpResponse<String> secondResponse = client.send(request,
|
|
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
|
assertEquals(200, secondResponse.statusCode(), "Unexpected response code");
|
|
// expect the alt service to respond
|
|
assertEquals(ALT_SERVER_RESPONSE_MESSAGE, secondResponse.body(), "Unexpected response" +
|
|
" body");
|
|
assertEquals(HTTP_3, secondResponse.version(), "Unexpected HTTP version in response");
|
|
} finally {
|
|
safeStop(testInput.originServer);
|
|
safeStop(testInput.altServer);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* - Keystore K1 is constructed with a PrivateKey PK1 and certificate whose subject is
|
|
* origin server hostname
|
|
* - Keystore K2 is constructed with the same PrivateKey PK1 and certificate whose subject is
|
|
* alternate server hostname
|
|
* - K1 is used to construct a SSLContext S1 and that S1 is used by origin server
|
|
* - K2 is used to construct a SSLContext S2 and that S2 is used by alternate server
|
|
* - SSLContext S3 is constructed with both the certificate of origin server and
|
|
* the certificate of alternate server as trusted certificates. HttpClient uses S3
|
|
* - During TLS handshake with origin server, the origin server is expected to present the
|
|
* certificate from this K1 keystore, with subject as origin server hostname
|
|
* - During TLS handshake with alternate server, the alternate server is expected to present
|
|
* the certificate from K2 keystore, with subject as alternate server hostname
|
|
* - HttpClient (through S3 SSLContext) trusts both these certs, but the cert presented
|
|
* by alt server, although valid for the alt server, CANNOT/MUST NOT be valid for origin
|
|
* server (since it's subject nor subject alternate name is origin server hostname).
|
|
* Reasonable assurance expects that the alt server present a certificate that is valid for
|
|
* origin server host and since it doesn't, the alt server must not be used by the HttpClient.
|
|
* <p>
|
|
* Once the servers are started, this test method does the following:
|
|
* <p>
|
|
* - Client constructs a HTTP_3 request addressed to origin server
|
|
* - Origin server responds with a 200 response and also with alt-svc header pointing to
|
|
* an alternate server
|
|
* - Client verifies the response as well as presence of the alt-svc header value
|
|
* - Client issues the *same* request again
|
|
* - The request is expected to be handled by the origin server again and the advertised
|
|
* alternate service MUST NOT be used (due to reasons noted above)
|
|
*/
|
|
@Test
|
|
public void testAltServerWrongCert() throws Exception {
|
|
final SecureRandom secureRandom = new SecureRandom();
|
|
final KeyPair keyPair = generateRSAKeyPair(secureRandom);
|
|
|
|
// generate a certificate for origin server, with origin server hostname as the subject
|
|
final X509Certificate originServerCert = generateCert(keyPair, secureRandom,
|
|
ORIGIN_SERVER_HOSTNAME);
|
|
// create a keystore with the private key and the cert. this keystore will then be
|
|
// used by the SSLContext of origin server
|
|
final KeyStore originServerKeyStore = generateKeyStore(keyPair.getPrivate(),
|
|
new Certificate[]{originServerCert});
|
|
System.out.println("Generated a keystore, for origin server, with certificate: " +
|
|
originServerKeyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS));
|
|
// create the SSLContext for the origin server
|
|
final SSLContext originServerSSLCtx = DynamicKeyStoreUtil.createSSLContext(
|
|
originServerKeyStore);
|
|
|
|
// create a cert for the alternate server, with alternate server hostname as the subject
|
|
final X509Certificate altServerCert = generateCert(keyPair, secureRandom,
|
|
ALT_SERVER_HOSTNAME);
|
|
// create keystore with the private key and the alt server's cert. this keystore will then
|
|
// be used by the SSLContext of alternate server
|
|
final KeyStore altServerKeyStore = generateKeyStore(keyPair.getPrivate(),
|
|
new Certificate[]{altServerCert});
|
|
System.out.println("Generated a keystore, for alt server, with certificate: " +
|
|
altServerKeyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS));
|
|
// create SSLContext of alternate server
|
|
final SSLContext altServerSSLCtx = DynamicKeyStoreUtil.createSSLContext(altServerKeyStore);
|
|
|
|
// now create a SSLContext for the HttpClient. This SSLContext will contain no key manager
|
|
// and will have a trust manager which trusts origin server certificate and the alternate
|
|
// server certificate
|
|
final SSLContext clientSSLCtx = sslCtxWithTrustedCerts(List.of(originServerCert,
|
|
altServerCert));
|
|
// start the servers
|
|
final TestInput testInput = startOriginAndAltServer(originServerSSLCtx, altServerSSLCtx);
|
|
try {
|
|
final HttpClient client = newClientBuilderForH3()
|
|
.proxy(NO_PROXY)
|
|
.sslContext(clientSSLCtx)
|
|
.version(HTTP_3)
|
|
.build();
|
|
// send a HTTP3 request to a server which is expected to respond back
|
|
// with a 200 response and an alt-svc header pointing to another/different H3 server
|
|
final URI requestURI = testInput.requestURI;
|
|
final HttpRequest request = HttpRequest.newBuilder()
|
|
.GET().uri(requestURI)
|
|
.setOption(H3_DISCOVERY, ALT_SVC)
|
|
.build();
|
|
System.out.println("Issuing request " + requestURI);
|
|
final HttpResponse<String> response = client.send(request,
|
|
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
|
assertEquals(200, response.statusCode(), "Unexpected response code");
|
|
// the origin server is expected to respond
|
|
assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, response.body(), "Unexpected response" +
|
|
" body");
|
|
assertEquals(HTTP_2, response.version(), "Unexpected HTTP version in response");
|
|
|
|
// verify the origin server sent back a alt-svc header
|
|
final Optional<String> altSvcHeader = response.headers().firstValue("alt-svc");
|
|
assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response");
|
|
final String actualAltSvcHeader = altSvcHeader.get();
|
|
System.out.println("Received alt-svc header value: " + actualAltSvcHeader);
|
|
assertTrue(actualAltSvcHeader.contains(testInput.expectedAltSvcHeader),
|
|
"Unexpected alt-svc header value: " + actualAltSvcHeader
|
|
+ ", was expected to contain: " + testInput.expectedAltSvcHeader);
|
|
|
|
// now issue the same request again (a few times). Expect each of these requests too,
|
|
// to be handled by the origin server (since the advertised alt server isn't expected
|
|
// to satisfy the "reasonable assurances" expectations
|
|
for (int i = 1; i <= 3; i++) {
|
|
System.out.println("Again(" + i + ") issuing request " + requestURI);
|
|
final HttpResponse<String> rsp = client.send(request,
|
|
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
|
assertEquals(200, rsp.statusCode(), "Unexpected response code");
|
|
// expect the alt service to respond
|
|
assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, rsp.body(),
|
|
"Unexpected response body");
|
|
assertEquals(HTTP_2, rsp.version(), "Unexpected HTTP version in response");
|
|
}
|
|
} finally {
|
|
safeStop(testInput.originServer);
|
|
safeStop(testInput.altServer);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* - Keystore K1 is constructed with a PrivateKey PK1 and certificate whose subject is
|
|
* alternate server hostname
|
|
* - K1 is used to construct a SSLContext S1 and that S1 is used by alternate server
|
|
* - The same SSLContext S1 is used by the HttpClient (and thus will trust the alternate
|
|
* server's certificate)
|
|
* - Origin server runs only on HTTP
|
|
* - Any alt-svc advertised by origin server MUST NOT be used by the client, because origin
|
|
* server runs on HTTP and as a result the "reasonable assurance" for the origin server
|
|
* cannot be satisfied.
|
|
* <p>
|
|
* Once the servers are started, this test method does the following:
|
|
* <p>
|
|
* - Client constructs a HTTP2 request addressed to origin server
|
|
* - Origin server responds with a 200 response and also with alt-svc header pointing to
|
|
* an alternate server
|
|
* - Client verifies the response as well as presence of the alt-svc header value
|
|
* - Client issues the request again, to the origin server, this time with HTTP3 as the request
|
|
* version
|
|
* - The request is expected to be handled by the origin server again and the advertised
|
|
* alternate service MUST NOT be used (due to reasons noted above)
|
|
*/
|
|
@Test
|
|
public void testAltServiceAdvertisedByHTTPOrigin() throws Exception {
|
|
// create a keystore which contains a PrivateKey entry and a certificate associated with
|
|
// that key. the certificate's subject will be alternate server's hostname. Thus, the
|
|
// certificate is valid for alternate server
|
|
final KeyStore keyStore = generateKeyStore(ALT_SERVER_HOSTNAME);
|
|
System.out.println("Generated a keystore with certificate: " +
|
|
keyStore.getCertificate(DynamicKeyStoreUtil.DEFAULT_ALIAS));
|
|
// create a SSLContext that will be used by the alternate server and the HttpClient and
|
|
// will be backed by the keystore we just created. Thus, the HttpClient will trust the
|
|
// certificate belonging to that keystore
|
|
final SSLContext sslContext = DynamicKeyStoreUtil.createSSLContext(keyStore);
|
|
|
|
// start the servers
|
|
final TestInput testInput = startHttpOriginHttpsAltServer(sslContext);
|
|
try {
|
|
final HttpClient client = newClientBuilderForH3()
|
|
.proxy(NO_PROXY)
|
|
.sslContext(sslContext)
|
|
.version(HTTP_3)
|
|
.build();
|
|
// send a HTTP2 request to a server which is expected to respond back
|
|
// with a 200 response and an alt-svc header pointing to another/different H3 server
|
|
final URI requestURI = testInput.requestURI;
|
|
final HttpRequest request = HttpRequest.newBuilder()
|
|
.version(HTTP_2).GET()
|
|
.uri(requestURI).build();
|
|
System.out.println("Issuing " + request.version() + " request to " + requestURI);
|
|
final HttpResponse<String> response = client.send(request,
|
|
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
|
assertEquals(200, response.statusCode(), "Unexpected response code");
|
|
assertEquals(HTTP_2, response.version(), "Unexpected HTTP version in response");
|
|
// the origin server is expected to respond
|
|
assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, response.body(), "Unexpected response" +
|
|
" body");
|
|
|
|
// verify the origin server sent back a alt-svc header
|
|
final Optional<String> altSvcHeader = response.headers().firstValue("alt-svc");
|
|
assertTrue(altSvcHeader.isPresent(), "alt-svc header is missing in response");
|
|
final String actualAltSvcHeader = altSvcHeader.get();
|
|
System.out.println("Received alt-svc header value: " + actualAltSvcHeader);
|
|
assertTrue(actualAltSvcHeader.contains(testInput.expectedAltSvcHeader),
|
|
"Unexpected alt-svc header value: " + actualAltSvcHeader
|
|
+ ", was expected to contain: " + testInput.expectedAltSvcHeader);
|
|
|
|
// now issue few more requests to the same address, but as a HTTP3 version. Expect each
|
|
// of these requests too, to be handled by the origin server (since the previously
|
|
// advertised alt server isn't expected to satisfy the "reasonable assurances"
|
|
// expectations)
|
|
for (int i = 1; i <= 3; i++) {
|
|
final HttpRequest h3Request = HttpRequest.newBuilder()
|
|
.version(HTTP_3).GET().uri(requestURI)
|
|
.setOption(H3_DISCOVERY, ALT_SVC)
|
|
.build();
|
|
System.out.println("Again(" + i + ") issuing " + h3Request.version()
|
|
+ " request to " + requestURI);
|
|
final HttpResponse<String> rsp = client.send(h3Request,
|
|
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
|
assertEquals(200, rsp.statusCode(), "Unexpected response code");
|
|
// even though the request is HTTP_3 version, the client will fall back to HTTP_2
|
|
// since the origin server does run on TLS and thus cannot use HTTP_3 (which
|
|
// requires TLS)
|
|
assertEquals(HTTP_2, rsp.version(), "Unexpected HTTP version in response");
|
|
// expect the alt service to respond
|
|
assertEquals(ORIGIN_SERVER_RESPONSE_MESSAGE, rsp.body(),
|
|
"Unexpected response body");
|
|
}
|
|
} finally {
|
|
safeStop(testInput.originServer);
|
|
safeStop(testInput.altServer);
|
|
}
|
|
}
|
|
|
|
private static SSLContext sslCtxWithTrustedCerts(final List<Certificate> trustedCerts)
|
|
throws Exception {
|
|
Objects.requireNonNull(trustedCerts);
|
|
// start with a blank keystore
|
|
final KeyStore keyStore = DynamicKeyStoreUtil.generateBlankKeyStore();
|
|
final String aliasPrefix = "trusted-certs-alias-";
|
|
int i = 1;
|
|
for (final Certificate cert : trustedCerts) {
|
|
// add the cert as a trusted certificate to the keystore
|
|
keyStore.setCertificateEntry(aliasPrefix + i, cert);
|
|
i++;
|
|
}
|
|
System.out.println("Generated a keystore with (only) trusted certs: ");
|
|
for (--i; i > 0; i--) {
|
|
System.out.println(keyStore.getCertificate(aliasPrefix + i));
|
|
}
|
|
final TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
|
|
// use the generated keystore for this trust manager
|
|
tmf.init(keyStore);
|
|
|
|
final String protocol = "TLS";
|
|
final SSLContext ctx = SSLContext.getInstance(protocol);
|
|
// initialize the SSLContext with the trust manager which trusts the passed certificates
|
|
ctx.init(null, tmf.getTrustManagers(), null);
|
|
return ctx;
|
|
}
|
|
}
|