diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/AbstractAsyncSSLConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/AbstractAsyncSSLConnection.java index 52037ba497f..088e8c66b04 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/AbstractAsyncSSLConnection.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/AbstractAsyncSSLConnection.java @@ -41,7 +41,7 @@ import jdk.internal.net.http.common.Alpns; import jdk.internal.net.http.common.SSLTube; import jdk.internal.net.http.common.Log; import jdk.internal.net.http.common.Utils; -import static jdk.internal.net.http.common.Utils.ServerName; +import sun.net.util.IPAddressUtil; /** * Asynchronous version of SSLConnection. @@ -63,6 +63,10 @@ import static jdk.internal.net.http.common.Utils.ServerName; */ abstract class AbstractAsyncSSLConnection extends HttpConnection { + + private record ServerName(String name, boolean isLiteral) { + } + protected final SSLEngine engine; protected final SSLParameters sslParameters; private final List sniServerNames; @@ -71,17 +75,19 @@ abstract class AbstractAsyncSSLConnection extends HttpConnection private static final boolean disableHostnameVerification = Utils.isHostnameVerificationDisabled(); - AbstractAsyncSSLConnection(InetSocketAddress addr, + AbstractAsyncSSLConnection(Origin originServer, + InetSocketAddress addr, HttpClientImpl client, - ServerName serverName, int port, String[] alpn, String label) { - super(addr, client, label); + super(originServer, addr, client, label); + assert originServer != null : "origin server is null"; + final ServerName serverName = getServerName(originServer); this.sniServerNames = formSNIServerNames(serverName, client); SSLContext context = client.theSSLContext(); sslParameters = createSSLParameters(client, this.sniServerNames, alpn); Log.logParams(sslParameters); - engine = createEngine(context, serverName.name(), port, sslParameters); + engine = createEngine(context, serverName.name(), originServer.port(), sslParameters); } abstract SSLTube getConnectionFlow(); @@ -187,6 +193,23 @@ abstract class AbstractAsyncSSLConnection extends HttpConnection return engine; } + /** + * Analyse the given {@linkplain Origin origin server} and determine + * if the origin server's host is a literal or not, returning the server's + * address in String form. + */ + private static ServerName getServerName(final Origin originServer) { + final String host = originServer.host(); + byte[] literal = IPAddressUtil.textToNumericFormatV4(host); + if (literal == null) { + // not IPv4 literal. Check IPv6 + literal = IPAddressUtil.textToNumericFormatV6(host); + return new ServerName(host, literal != null); + } else { + return new ServerName(host, true); + } + } + @Override final boolean isSecure() { return true; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLConnection.java index 56477e9604e..c02cd145cf8 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLConnection.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLConnection.java @@ -42,12 +42,13 @@ class AsyncSSLConnection extends AbstractAsyncSSLConnection { final PlainHttpPublisher writePublisher; private volatile SSLTube flow; - AsyncSSLConnection(InetSocketAddress addr, + AsyncSSLConnection(Origin originServer, + InetSocketAddress addr, HttpClientImpl client, String[] alpn, String label) { - super(addr, client, Utils.getServerName(addr), addr.getPort(), alpn, label); - plainConnection = new PlainHttpConnection(addr, client, label); + super(originServer, addr, client, alpn, label); + plainConnection = new PlainHttpConnection(originServer, addr, client, label); writePublisher = new PlainHttpPublisher(); } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLTunnelConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLTunnelConnection.java index 1210f4dd62b..a81d8de328e 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLTunnelConnection.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/AsyncSSLTunnelConnection.java @@ -43,15 +43,17 @@ class AsyncSSLTunnelConnection extends AbstractAsyncSSLConnection { final PlainHttpPublisher writePublisher; volatile SSLTube flow; - AsyncSSLTunnelConnection(InetSocketAddress addr, + AsyncSSLTunnelConnection(Origin originServer, + InetSocketAddress addr, HttpClientImpl client, String[] alpn, InetSocketAddress proxy, ProxyHeaders proxyHeaders, String label) { - super(addr, client, Utils.getServerName(addr), addr.getPort(), alpn, label); - this.plainConnection = new PlainTunnelingConnection(addr, proxy, client, proxyHeaders, label); + super(originServer, addr, client, alpn, label); + this.plainConnection = new PlainTunnelingConnection(originServer, addr, proxy, client, + proxyHeaders, label); this.writePublisher = new PlainHttpPublisher(); } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java index dd8f6652290..07cfc4dbdf6 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java @@ -100,7 +100,11 @@ abstract class HttpConnection implements Closeable { */ private final String label; - HttpConnection(InetSocketAddress address, HttpClientImpl client, String label) { + private final Origin originServer; + + HttpConnection(Origin originServer, InetSocketAddress address, HttpClientImpl client, + String label) { + this.originServer = originServer; this.address = address; this.client = client; trailingOperations = new TrailingOperations(); @@ -244,6 +248,14 @@ abstract class HttpConnection implements Closeable { return false; } + /** + * {@return the {@link Origin} server against which this connection communicates. + * Returns {@code null} if the connection is a plain connection to a proxy} + */ + final Origin getOriginServer() { + return this.originServer; + } + interface HttpPublisher extends FlowTube.TubePublisher { void enqueue(List buffers) throws IOException; void enqueueUnordered(List buffers) throws IOException; @@ -334,13 +346,20 @@ abstract class HttpConnection implements Closeable { String[] alpn, HttpRequestImpl request, HttpClientImpl client) { - String label = nextLabel(); + final String label = nextLabel(); + final Origin originServer; + try { + originServer = Origin.from(request.uri()); + } catch (IllegalArgumentException iae) { + // should never happen + throw new AssertionError("failed to determine origin server from request URI", iae); + } if (proxy != null) - return new AsyncSSLTunnelConnection(addr, client, alpn, proxy, + return new AsyncSSLTunnelConnection(originServer, addr, client, alpn, proxy, proxyTunnelHeaders(request), label); else - return new AsyncSSLConnection(addr, client, alpn, label); + return new AsyncSSLConnection(originServer, addr, client, alpn, label); } /** @@ -414,14 +433,21 @@ abstract class HttpConnection implements Closeable { InetSocketAddress proxy, HttpRequestImpl request, HttpClientImpl client) { - String label = nextLabel(); + final String label = nextLabel(); + final Origin originServer; + try { + originServer = Origin.from(request.uri()); + } catch (IllegalArgumentException iae) { + // should never happen + throw new AssertionError("failed to determine origin server from request URI", iae); + } if (request.isWebSocket() && proxy != null) - return new PlainTunnelingConnection(addr, proxy, client, + return new PlainTunnelingConnection(originServer, addr, proxy, client, proxyTunnelHeaders(request), label); if (proxy == null) - return new PlainHttpConnection(addr, client, label); + return new PlainHttpConnection(originServer, addr, client, label); else return new PlainProxyConnection(proxy, client, label); } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Origin.java b/src/java.net.http/share/classes/jdk/internal/net/http/Origin.java new file mode 100644 index 00000000000..adbee565297 --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Origin.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +package jdk.internal.net.http; + +import java.net.URI; +import java.util.Locale; +import java.util.Objects; + +import sun.net.util.IPAddressUtil; + +/** + * Represents an origin server to which a HTTP request is targeted. + * + * @param scheme The scheme of the origin (for example: https). Unlike the application layer + * protocol (which can be a finer grained protocol like h2, h3 etc...), + * this is actually a scheme. Only {@code http} and {@code https} literals are + * supported. Cannot be null. + * @param host The host of the origin, cannot be null. If the host is an IPv6 address, + * then it must not be enclosed in square brackets ({@code '['} and {@code ']'}). + * If the host is a DNS hostname, then it must be passed as a lower case String. + * @param port The port of the origin. Must be greater than 0. + */ +public record Origin(String scheme, String host, int port) { + public Origin { + Objects.requireNonNull(scheme); + Objects.requireNonNull(host); + if (!isValidScheme(scheme)) { + throw new IllegalArgumentException("Unsupported scheme: " + scheme); + } + if (host.startsWith("[") && host.endsWith("]")) { + throw new IllegalArgumentException("Invalid host: " + host); + } + // expect DNS hostname to be passed as lower case + if (isDNSHostName(host) && !host.toLowerCase(Locale.ROOT).equals(host)) { + throw new IllegalArgumentException("non-lowercase hostname: " + host); + } + if (port <= 0) { + throw new IllegalArgumentException("Invalid port: " + port); + } + } + + @Override + public String toString() { + return scheme + "://" + toAuthority(host, port); + } + + /** + * {@return Creates and returns an Origin from an URI} + * + * @param uri The URI of the origin + * @throws IllegalArgumentException if a Origin cannot be constructed from + * the given {@code uri} + */ + public static Origin from(final URI uri) throws IllegalArgumentException { + Objects.requireNonNull(uri); + final String scheme = uri.getScheme(); + if (scheme == null) { + throw new IllegalArgumentException("missing scheme in URI"); + } + final String lcaseScheme = scheme.toLowerCase(Locale.ROOT); + if (!isValidScheme(lcaseScheme)) { + throw new IllegalArgumentException("Unsupported scheme: " + scheme); + } + final String host = uri.getHost(); + if (host == null) { + throw new IllegalArgumentException("missing host in URI"); + } + String effectiveHost; + if (host.startsWith("[") && host.endsWith("]")) { + // strip the square brackets from IPv6 host + effectiveHost = host.substring(1, host.length() - 1); + } else { + effectiveHost = host; + } + assert !effectiveHost.isEmpty() : "unexpected URI host: " + host; + // If the host is a DNS hostname, then convert the host to lower case. + // The DNS hostname is expected to be ASCII characters and is case-insensitive. + // + // Its usage in areas like SNI too match this expectation - RFC-6066, section 3: + // "HostName" contains the fully qualified DNS hostname of the server, + // as understood by the client. The hostname is represented as a byte + // string using ASCII encoding without a trailing dot. ... DNS hostnames + // are case-insensitive. + if (isDNSHostName(effectiveHost)) { + effectiveHost = effectiveHost.toLowerCase(Locale.ROOT); + } + int port = uri.getPort(); + if (port == -1) { + port = switch (lcaseScheme) { + case "http" -> 80; + case "https" -> 443; + // we have already verified that this is a valid scheme, so this + // should never happen + default -> throw new AssertionError("Unsupported scheme: " + scheme); + }; + } + return new Origin(lcaseScheme, effectiveHost, port); + } + + static String toAuthority(final String host, final int port) { + assert port > 0 : "invalid port: " + port; + // borrowed from code in java.net.URI + final boolean needBrackets = host.indexOf(':') >= 0 + && !host.startsWith("[") + && !host.endsWith("]"); + if (needBrackets) { + return "[" + host + "]:" + port; + } + return host + ":" + port; + } + + private static boolean isValidScheme(final String scheme) { + // only "http" and "https" literals allowed + return "http".equals(scheme) || "https".equals(scheme); + } + + private static boolean isDNSHostName(final String host) { + final boolean isLiteral = IPAddressUtil.isIPv4LiteralAddress(host) + || IPAddressUtil.isIPv6LiteralAddress(host); + + return !isLiteral; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java index 190df8a00ba..ff97841d325 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PlainHttpConnection.java @@ -310,8 +310,9 @@ class PlainHttpConnection extends HttpConnection { return tube; } - PlainHttpConnection(InetSocketAddress addr, HttpClientImpl client, String label) { - super(addr, client, label); + PlainHttpConnection(Origin originServer, InetSocketAddress addr, HttpClientImpl client, + String label) { + super(originServer, addr, client, label); try { this.chan = SocketChannel.open(); chan.configureBlocking(false); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/PlainProxyConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/PlainProxyConnection.java index c11fd489177..c4b1b14a4d2 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/PlainProxyConnection.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PlainProxyConnection.java @@ -30,7 +30,9 @@ import java.net.InetSocketAddress; class PlainProxyConnection extends PlainHttpConnection { PlainProxyConnection(InetSocketAddress proxy, HttpClientImpl client, String label) { - super(proxy, client, label); + // we don't track the origin server for a plain proxy connection, since it + // can be used to serve requests against several different origin servers. + super(null, proxy, client, label); } @Override diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/PlainTunnelingConnection.java b/src/java.net.http/share/classes/jdk/internal/net/http/PlainTunnelingConnection.java index 7a8eb9c79c5..147d5938fe5 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/PlainTunnelingConnection.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/PlainTunnelingConnection.java @@ -51,15 +51,16 @@ final class PlainTunnelingConnection extends HttpConnection { final InetSocketAddress proxyAddr; private volatile boolean connected; - protected PlainTunnelingConnection(InetSocketAddress addr, + protected PlainTunnelingConnection(Origin originServer, + InetSocketAddress addr, InetSocketAddress proxy, HttpClientImpl client, ProxyHeaders proxyHeaders, String label) { - super(addr, client, label); + super(originServer, addr, client, label); this.proxyAddr = proxy; this.proxyHeaders = proxyHeaders; - delegate = new PlainHttpConnection(proxy, client, label); + delegate = new PlainHttpConnection(originServer, proxy, client, label); } @Override diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java index d035a8c8da1..8aefa0ee5ba 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 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 @@ -38,7 +38,6 @@ import java.io.UncheckedIOException; import java.lang.System.Logger.Level; import java.net.ConnectException; import java.net.InetSocketAddress; -import java.net.URI; import java.net.http.HttpHeaders; import java.net.http.HttpTimeoutException; import java.nio.ByteBuffer; @@ -73,12 +72,10 @@ import jdk.internal.net.http.common.DebugLogger.LoggerConfig; import jdk.internal.net.http.HttpRequestImpl; import sun.net.NetProperties; -import sun.net.util.IPAddressUtil; import sun.net.www.HeaderParser; import static java.lang.String.format; import static java.nio.charset.StandardCharsets.US_ASCII; -import static java.util.stream.Collectors.joining; import static java.net.Authenticator.RequestorType.PROXY; import static java.net.Authenticator.RequestorType.SERVER; @@ -487,39 +484,6 @@ public final class Utils { return !token.isEmpty(); } - public record ServerName (String name, boolean isLiteral) { - } - - /** - * Analyse the given address and determine if it is literal or not, - * returning the address in String form. - */ - public static ServerName getServerName(InetSocketAddress addr) { - String host = addr.getHostString(); - byte[] literal = IPAddressUtil.textToNumericFormatV4(host); - if (literal == null) { - // not IPv4 literal. Check IPv6 - literal = IPAddressUtil.textToNumericFormatV6(host); - return new ServerName(host, literal != null); - } else { - return new ServerName(host, true); - } - } - - private static boolean isLoopbackLiteral(byte[] bytes) { - if (bytes.length == 4) { - return bytes[0] == 127; - } else if (bytes.length == 16) { - for (int i=0; i<14; i++) - if (bytes[i] != 0) - return false; - if (bytes[15] != 1) - return false; - return true; - } else - throw new InternalError(); - } - /* * Validates an RFC 7230 field-value. * @@ -895,33 +859,6 @@ public final class Utils { return DebugLogger.createHttpLogger(dbgTag, config); } - /** - * Return the host string from a HttpRequestImpl - * - * @param request - * @return - */ - public static String hostString(HttpRequestImpl request) { - URI uri = request.uri(); - int port = uri.getPort(); - String host = uri.getHost(); - - boolean defaultPort; - if (port == -1) { - defaultPort = true; - } else if (uri.getScheme().equalsIgnoreCase("https")) { - defaultPort = port == 443; - } else { - defaultPort = port == 80; - } - - if (defaultPort) { - return host; - } else { - return host + ":" + port; - } - } - /** * Get a logger for debug HPACK traces.The logger should only be used * with levels whose severity is {@code <= DEBUG}. diff --git a/test/jdk/java/net/httpclient/OriginTest.java b/test/jdk/java/net/httpclient/OriginTest.java new file mode 100644 index 00000000000..58310ecd9ad --- /dev/null +++ b/test/jdk/java/net/httpclient/OriginTest.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 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.net.URI; +import java.util.Locale; + +import jdk.internal.net.http.Origin; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/* + * @test + * @summary verify the behaviour of jdk.internal.net.http.Origin + * @modules java.net.http/jdk.internal.net.http + * @run junit OriginTest + */ +class OriginTest { + + @ParameterizedTest + @ValueSource(strings = {"foo", "Bar", "HttPS", "HTTP"}) + void testInvalidScheme(final String scheme) throws Exception { + final String validHost = "127.0.0.1"; + final int validPort = 80; + final IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> { + new Origin(scheme, validHost, validPort); + }); + assertTrue(iae.getMessage().contains("scheme"), + "unexpected exception message: " + iae.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"http", "https"}) + void testValidScheme(final String scheme) throws Exception { + final String validHost = "127.0.0.1"; + final int validPort = 80; + final Origin o1 = new Origin(scheme, validHost, validPort); + assertEquals(validHost, o1.host(), "unexpected host"); + assertEquals(validPort, o1.port(), "unexpected port"); + assertEquals(scheme, o1.scheme(), "unexpected scheme"); + + final URI uri = URI.create(scheme + "://" + validHost + ":" + validPort); + final Origin o2 = Origin.from(uri); + assertNotNull(o2, "null Origin for URI " + uri); + assertEquals(validHost, o2.host(), "unexpected host"); + assertEquals(validPort, o2.port(), "unexpected port"); + assertEquals(scheme, o2.scheme(), "unexpected scheme"); + } + + @ParameterizedTest + @ValueSource(strings = {"JDK.java.net", "[::1]", "[0:0:0:0:0:0:0:1]"}) + void testInvalidHost(final String host) throws Exception { + final String validScheme = "http"; + final int validPort = 8000; + final IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> { + new Origin(validScheme, host, validPort); + }); + assertTrue(iae.getMessage().contains("host"), + "unexpected exception message: " + iae.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"127.0.0.1", "localhost", "jdk.java.net", "::1", "0:0:0:0:0:0:0:1"}) + void testValidHost(final String host) throws Exception { + final String validScheme = "https"; + final int validPort = 42; + final Origin o1 = new Origin(validScheme, host, validPort); + assertEquals(host, o1.host(), "unexpected host"); + assertEquals(validPort, o1.port(), "unexpected port"); + assertEquals(validScheme, o1.scheme(), "unexpected scheme"); + + String uriHost = host; + if (host.contains(":")) { + uriHost = "[" + host + "]"; + } + final URI uri = URI.create(validScheme + "://" + uriHost + ":" + validPort); + final Origin o2 = Origin.from(uri); + assertNotNull(o2, "null Origin for URI " + uri); + assertEquals(host, o2.host(), "unexpected host"); + assertEquals(validPort, o2.port(), "unexpected port"); + assertEquals(validScheme, o2.scheme(), "unexpected scheme"); + } + + @ParameterizedTest + @ValueSource(ints = {-1, 0}) + void testInvalidPort(final int port) throws Exception { + final String validScheme = "http"; + final String validHost = "127.0.0.1"; + final IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, + () -> new Origin(validScheme, validHost, port)); + assertTrue(iae.getMessage().contains("port"), + "unexpected exception message: " + iae.getMessage()); + } + + @ParameterizedTest + @ValueSource(ints = {100, 1024, 80, 8080, 42}) + void testValidPort(final int port) throws Exception { + final String validScheme = "https"; + final String validHost = "localhost"; + final Origin o1 = new Origin(validScheme, validHost, port); + assertEquals(validHost, o1.host(), "unexpected host"); + assertEquals(port, o1.port(), "unexpected port"); + assertEquals(validScheme, o1.scheme(), "unexpected scheme"); + + final URI uri = URI.create(validScheme + "://" + validHost + ":" + port); + final Origin o2 = Origin.from(uri); + assertNotNull(o2, "null Origin for URI " + uri); + assertEquals(validHost, o2.host(), "unexpected host"); + assertEquals(port, o2.port(), "unexpected port"); + assertEquals(validScheme, o2.scheme(), "unexpected scheme"); + } + + @Test + void testInferredPort() throws Exception { + final URI httpURI = URI.create("http://localhost"); + final Origin httpOrigin = Origin.from(httpURI); + assertNotNull(httpOrigin, "null Origin for URI " + httpURI); + assertEquals("localhost", httpOrigin.host(), "unexpected host"); + assertEquals(80, httpOrigin.port(), "unexpected port"); + assertEquals("http", httpOrigin.scheme(), "unexpected scheme"); + + + final URI httpsURI = URI.create("https://[::1]"); + final Origin httpsOrigin = Origin.from(httpsURI); + assertNotNull(httpsOrigin, "null Origin for URI " + httpsURI); + assertEquals("::1", httpsOrigin.host(), "unexpected host"); + assertEquals(443, httpsOrigin.port(), "unexpected port"); + assertEquals("https", httpsOrigin.scheme(), "unexpected scheme"); + } + + @Test + void testFromURI() { + // non-lower case URI scheme is expected to be converted to lowercase in the Origin + // constructed through Origin.from(URI) + for (final String scheme : new String[]{"httPs", "HTTP"}) { + final String expectedScheme = scheme.toLowerCase(Locale.ROOT); + final URI uri = URI.create(scheme + "://localhost:1234"); + final Origin origin = Origin.from(uri); + assertNotNull(origin, "null Origin for URI " + uri); + assertEquals("localhost", origin.host(), "unexpected host"); + assertEquals(1234, origin.port(), "unexpected port"); + assertEquals(expectedScheme, origin.scheme(), "unexpected scheme"); + } + // URI without a port is expected to be defaulted to port 80 or 443 for http and https + // schemes respectively + for (final String scheme : new String[]{"http", "https"}) { + final int expectedPort = switch (scheme) { + case "http" -> 80; + case "https" -> 443; + default -> fail("unexpected scheme: " + scheme); + }; + final URI uri = URI.create(scheme + "://localhost"); + final Origin origin = Origin.from(uri); + assertNotNull(origin, "null Origin for URI " + uri); + assertEquals("localhost", origin.host(), "unexpected host"); + assertEquals(expectedPort, origin.port(), "unexpected port"); + assertEquals(scheme, origin.scheme(), "unexpected scheme"); + } + } +} diff --git a/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/ConnectionPoolTest.java b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/ConnectionPoolTest.java index 729fdb084e1..27d2b98a34d 100644 --- a/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/ConnectionPoolTest.java +++ b/test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/ConnectionPoolTest.java @@ -33,6 +33,7 @@ import java.net.ProxySelector; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketOption; +import java.net.URI; import java.net.http.HttpHeaders; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; @@ -459,7 +460,9 @@ public class ConnectionPoolTest { InetSocketAddress address, InetSocketAddress proxy, boolean secured) { - super(address, impl, "testConn-" + IDS.incrementAndGet()); + final Origin originServer = Origin.from( + URI.create("http://"+ address.getHostString() + ":" + address.getPort())); + super(originServer, address, impl, "testConn-" + IDS.incrementAndGet()); this.key = ConnectionPool.cacheKey(secured, address, proxy); this.address = address; this.proxy = proxy;