/* * 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 *
* Once the servers are started, this test method does the following: *
* - 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
* Once the servers are started, this test method does the following:
*
* - 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
* Once the servers are started, this test method does the following:
*
* - 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
* Once the servers are started, this test method does the following:
*
* - 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