diff --git a/src/java.base/share/lib/security/default.policy b/src/java.base/share/lib/security/default.policy
index b22f26947af..20f53b1cd4c 100644
--- a/src/java.base/share/lib/security/default.policy
+++ b/src/java.base/share/lib/security/default.policy
@@ -21,6 +21,8 @@ grant codeBase "jrt:/java.net.http" {
permission java.lang.RuntimePermission "accessClassInPackage.jdk.internal.misc";
permission java.lang.RuntimePermission "modifyThread";
permission java.net.SocketPermission "*","connect,resolve";
+ // required if the HTTPClient is configured to use a local bind address
+ permission java.net.SocketPermission "localhost:*","listen,resolve";
permission java.net.URLPermission "http:*","*:*";
permission java.net.URLPermission "https:*","*:*";
permission java.net.URLPermission "ws:*","*:*";
diff --git a/src/java.net.http/share/classes/java/net/http/HttpClient.java b/src/java.net.http/share/classes/java/net/http/HttpClient.java
index 7038f885d3b..2cb2a5d21de 100644
--- a/src/java.net.http/share/classes/java/net/http/HttpClient.java
+++ b/src/java.net.http/share/classes/java/net/http/HttpClient.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2022, 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
@@ -27,6 +27,7 @@ package java.net.http;
import java.io.IOException;
import java.io.UncheckedIOException;
+import java.net.InetAddress;
import java.nio.channels.Selector;
import java.net.Authenticator;
import java.net.CookieHandler;
@@ -354,16 +355,59 @@ public abstract class HttpClient {
*/
public Builder authenticator(Authenticator authenticator);
+ /**
+ * Binds the socket to this local address when creating
+ * connections for sending requests.
+ *
+ *
If no local address is set or {@code null} is passed
+ * to this method then sockets created by the
+ * HTTP client will be bound to an automatically
+ * assigned socket address.
+ *
+ *
Common usages of the {@code HttpClient} do not require
+ * this method to be called. Setting a local address, through this
+ * method, is only for advanced usages where users of the {@code HttpClient}
+ * require specific control on which network interface gets used
+ * for the HTTP communication. Callers of this method are expected to
+ * be aware of the networking configurations of the system where the
+ * {@code HttpClient} will be used and care should be taken to ensure the
+ * correct {@code localAddr} is passed. Failure to do so can result in
+ * requests sent through the {@code HttpClient} to fail.
+ *
+ * @implSpec The default implementation of this method throws
+ * {@code UnsupportedOperationException}. {@code Builder}s obtained
+ * through {@link HttpClient#newBuilder()} provide an implementation
+ * of this method that allows setting the local address.
+ *
+ * @param localAddr The local address of the socket. Can be null.
+ * @return this builder
+ * @throws UnsupportedOperationException if this builder doesn't support
+ * configuring a local address or if the passed {@code localAddr}
+ * is not supported by this {@code HttpClient} implementation.
+ * @since 19
+ */
+ default Builder localAddress(InetAddress localAddr) {
+ throw new UnsupportedOperationException();
+ }
+
/**
* Returns a new {@link HttpClient} built from the current state of this
* builder.
*
+ * @implSpec If the {@link #localAddress(InetAddress) local address} is a non-null
+ * address and a security manager is installed, then
+ * this method calls {@link SecurityManager#checkListen checkListen} to check that
+ * the caller has necessary permission to bind to that local address.
+ *
* @return a new {@code HttpClient}
*
* @throws UncheckedIOException may be thrown if underlying IO resources required
* by the implementation cannot be allocated. For instance,
* if the implementation requires a {@link Selector}, and opening
* one fails due to {@linkplain Selector#open() lack of necessary resources}.
+ * @throws SecurityException If a security manager has been installed and the
+ * security manager's {@link SecurityManager#checkListen checkListen}
+ * method disallows binding to the given address.
*/
public HttpClient build();
}
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientBuilderImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientBuilderImpl.java
index 23741c0948b..c4157b3c74c 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientBuilderImpl.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientBuilderImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2022, 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
@@ -27,6 +27,7 @@ package jdk.internal.net.http;
import java.net.Authenticator;
import java.net.CookieHandler;
+import java.net.InetAddress;
import java.net.ProxySelector;
import java.time.Duration;
import java.util.concurrent.Executor;
@@ -49,6 +50,7 @@ public class HttpClientBuilderImpl implements HttpClient.Builder {
SSLContext sslContext;
SSLParameters sslParams;
int priority = -1;
+ InetAddress localAddr;
@Override
public HttpClientBuilderImpl cookieHandler(CookieHandler cookieHandler) {
@@ -130,6 +132,12 @@ public class HttpClientBuilderImpl implements HttpClient.Builder {
return this;
}
+ @Override
+ public HttpClient.Builder localAddress(final InetAddress localAddr) {
+ this.localAddr = localAddr;
+ return this;
+ }
+
@Override
public HttpClient build() {
return HttpClientImpl.create(this);
diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java
index 0bdce43173e..63bfed989f5 100644
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java
@@ -36,6 +36,7 @@ import java.lang.ref.WeakReference;
import java.net.Authenticator;
import java.net.ConnectException;
import java.net.CookieHandler;
+import java.net.InetAddress;
import java.net.ProxySelector;
import java.net.http.HttpConnectTimeoutException;
import java.net.http.HttpTimeoutException;
@@ -335,6 +336,7 @@ final class HttpClientImpl extends HttpClient implements Trackable {
private final Http2ClientImpl client2;
private final long id;
private final String dbgTag;
+ private final InetAddress localAddr;
// The SSL DirectBuffer Supplier provides the ability to recycle
// buffers used between the socket reader and the SSLEngine, or
@@ -431,6 +433,17 @@ final class HttpClientImpl extends HttpClient implements Trackable {
SingleFacadeFactory facadeFactory) {
id = CLIENT_IDS.incrementAndGet();
dbgTag = "HttpClientImpl(" + id +")";
+ @SuppressWarnings("removal")
+ var sm = System.getSecurityManager();
+ if (sm != null && builder.localAddr != null) {
+ // when a specific local address is configured, it will eventually
+ // lead to the SocketChannel.bind(...) call with an InetSocketAddress
+ // whose InetAddress is the local address and the port is 0. That ultimately
+ // leads to a SecurityManager.checkListen permission check for that port.
+ // so we do that security manager check here with port 0.
+ sm.checkListen(0);
+ }
+ localAddr = builder.localAddr;
if (builder.sslContext == null) {
try {
sslContext = SSLContext.getDefault();
@@ -1528,6 +1541,10 @@ final class HttpClientImpl extends HttpClient implements Trackable {
return version;
}
+ InetAddress localAddress() {
+ return localAddr;
+ }
+
String dbgString() {
return dbgTag;
}
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 6224ab62ba2..049e2d6e754 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
@@ -183,6 +183,27 @@ class PlainHttpConnection extends HttpConnection {
}
}
+ var localAddr = client().localAddress();
+ if (localAddr != null) {
+ if (debug.on()) {
+ debug.log("binding to configured local address " + localAddr);
+ }
+ var sockAddr = new InetSocketAddress(localAddr, 0);
+ PrivilegedExceptionAction pa = () -> chan.bind(sockAddr);
+ try {
+ AccessController.doPrivileged(pa);
+ if (debug.on()) {
+ debug.log("bind completed " + localAddr);
+ }
+ } catch (PrivilegedActionException e) {
+ var cause = e.getCause();
+ if (debug.on()) {
+ debug.log("bind to " + localAddr + " failed: " + cause.getMessage());
+ }
+ throw cause;
+ }
+ }
+
PrivilegedExceptionAction pa =
() -> chan.connect(Utils.resolveAddress(address));
try {
diff --git a/test/jdk/java/net/httpclient/HttpClientBuilderTest.java b/test/jdk/java/net/httpclient/HttpClientBuilderTest.java
index 121793014db..e21ad1687bc 100644
--- a/test/jdk/java/net/httpclient/HttpClientBuilderTest.java
+++ b/test/jdk/java/net/httpclient/HttpClientBuilderTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2022, 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
@@ -26,12 +26,12 @@ import java.lang.reflect.Method;
import java.net.Authenticator;
import java.net.CookieHandler;
import java.net.CookieManager;
+import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
-import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpResponse.BodyHandlers;
@@ -40,7 +40,6 @@ import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
-import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import javax.net.ssl.SSLContext;
@@ -55,6 +54,7 @@ import static org.testng.Assert.*;
/*
* @test
+ * @bug 8209137
* @summary HttpClient[.Builder] API and behaviour checks
* @library /test/lib
* @build jdk.test.lib.net.SimpleSSLContext
@@ -65,6 +65,7 @@ public class HttpClientBuilderTest {
static final Class NPE = NullPointerException.class;
static final Class IAE = IllegalArgumentException.class;
+ static final Class UOE = UnsupportedOperationException.class;
@Test
public void testDefaults() throws Exception {
@@ -262,6 +263,89 @@ public class HttpClientBuilderTest {
builder.build();
}
+ /**
+ * Tests the {@link java.net.http.HttpClient.Builder#localAddress(InetAddress)} method
+ * behaviour when that method is called on a builder returned by {@link HttpClient#newBuilder()}
+ */
+ @Test
+ public void testLocalAddress() throws Exception {
+ HttpClient.Builder builder = HttpClient.newBuilder();
+ // setting null should work fine
+ builder.localAddress(null);
+ builder.localAddress(InetAddress.getLoopbackAddress());
+ // resetting back to null should work fine
+ builder.localAddress(null);
+ }
+
+ /**
+ * Tests that the default method implementation of
+ * {@link java.net.http.HttpClient.Builder#localAddress(InetAddress)} throws
+ * an {@link UnsupportedOperationException}
+ */
+ @Test
+ public void testDefaultMethodImplForLocalAddress() throws Exception {
+ HttpClient.Builder noOpBuilder = new HttpClient.Builder() {
+ @Override
+ public HttpClient.Builder cookieHandler(CookieHandler cookieHandler) {
+ return null;
+ }
+
+ @Override
+ public HttpClient.Builder connectTimeout(Duration duration) {
+ return null;
+ }
+
+ @Override
+ public HttpClient.Builder sslContext(SSLContext sslContext) {
+ return null;
+ }
+
+ @Override
+ public HttpClient.Builder sslParameters(SSLParameters sslParameters) {
+ return null;
+ }
+
+ @Override
+ public HttpClient.Builder executor(Executor executor) {
+ return null;
+ }
+
+ @Override
+ public HttpClient.Builder followRedirects(Redirect policy) {
+ return null;
+ }
+
+ @Override
+ public HttpClient.Builder version(Version version) {
+ return null;
+ }
+
+ @Override
+ public HttpClient.Builder priority(int priority) {
+ return null;
+ }
+
+ @Override
+ public HttpClient.Builder proxy(ProxySelector proxySelector) {
+ return null;
+ }
+
+ @Override
+ public HttpClient.Builder authenticator(Authenticator authenticator) {
+ return null;
+ }
+
+ @Override
+ public HttpClient build() {
+ return null;
+ }
+ };
+ // expected to throw a UnsupportedOperationException
+ assertThrows(UOE, () -> noOpBuilder.localAddress(null));
+ // a non-null address should also throw a UnsupportedOperationException
+ assertThrows(UOE, () -> noOpBuilder.localAddress(InetAddress.getLoopbackAddress()));
+ }
+
// ---
static final URI uri = URI.create("http://foo.com/");
@@ -303,9 +387,6 @@ public class HttpClientBuilderTest {
// ---
- static final Class UOE =
- UnsupportedOperationException.class;
-
@Test
static void testUnsupportedWebSocket() throws Exception {
// @implSpec The default implementation of this method throws
diff --git a/test/jdk/java/net/httpclient/HttpClientLocalAddrTest.java b/test/jdk/java/net/httpclient/HttpClientLocalAddrTest.java
new file mode 100644
index 00000000000..f51b392ea00
--- /dev/null
+++ b/test/jdk/java/net/httpclient/HttpClientLocalAddrTest.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright (c) 2022, 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 com.sun.net.httpserver.HttpServer;
+import com.sun.net.httpserver.HttpsConfigurator;
+import com.sun.net.httpserver.HttpsServer;
+import jdk.test.lib.net.IPSupport;
+import jdk.test.lib.net.SimpleSSLContext;
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import javax.net.ssl.SSLContext;
+import java.net.InetAddress;
+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.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.function.Predicate;
+
+/*
+ * @test
+ * @summary Tests HttpClient usage when configured with a local address to bind
+ * to, when sending requests
+ * @bug 8209137
+ * @modules jdk.httpserver
+ * java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.base/sun.net.www.http
+ * java.net.http/jdk.internal.net.http.hpack
+ *
+ * @library /test/lib http2/server
+ *
+ * @build jdk.test.lib.net.SimpleSSLContext jdk.test.lib.net.IPSupport HttpServerAdapters
+ *
+ * @run testng/othervm
+ * -Djdk.httpclient.HttpClient.log=frames,ssl,requests,responses,errors
+ * -Djdk.internal.httpclient.debug=true
+ * HttpClientLocalAddrTest
+ *
+ * @run testng/othervm/java.security.policy=httpclient-localaddr-security.policy
+ * -Djdk.httpclient.HttpClient.log=frames,ssl,requests,responses,errors
+ * -Djdk.internal.httpclient.debug=true
+ * HttpClientLocalAddrTest
+ *
+ */
+public class HttpClientLocalAddrTest implements HttpServerAdapters {
+
+ private static SSLContext sslContext;
+ private static HttpServerAdapters.HttpTestServer http1_1_Server;
+ private static URI httpURI;
+ private static HttpServerAdapters.HttpTestServer https_1_1_Server;
+ private static URI httpsURI;
+ private static HttpServerAdapters.HttpTestServer http2_Server;
+ private static URI http2URI;
+ private static HttpServerAdapters.HttpTestServer https2_Server;
+ private static URI https2URI;
+
+ // start various HTTP/HTTPS servers that will be invoked against in the tests
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ sslContext = new SimpleSSLContext().get();
+ Assert.assertNotNull(sslContext, "Unexpected null sslContext");
+
+ HttpServerAdapters.HttpTestHandler handler = (exchange) -> {
+ // the handler receives a request and sends back a 200 response with the
+ // response body containing the raw IP address (in byte[] form) of the client from whom
+ // the request was received
+ var clientAddr = exchange.getRemoteAddress();
+ System.out.println("Received a request from client address " + clientAddr);
+ var responseContent = clientAddr.getAddress().getAddress();
+ exchange.sendResponseHeaders(200, responseContent.length);
+ try (var os = exchange.getResponseBody()) {
+ // write out the client address as a response
+ os.write(responseContent);
+ }
+ exchange.close();
+ };
+
+ // HTTP/1.1 - create servers with http and https
+ final var sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
+ final int backlog = 0;
+ http1_1_Server = HttpServerAdapters.HttpTestServer.of(HttpServer.create(sa, backlog));
+ http1_1_Server.addHandler(handler, "/");
+ http1_1_Server.start();
+ System.out.println("Started HTTP v1.1 server at " + http1_1_Server.serverAuthority());
+ httpURI = new URI("http://" + http1_1_Server.serverAuthority() + "/");
+
+ final HttpsServer httpsServer = HttpsServer.create(sa, 0);
+ httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));
+ https_1_1_Server = HttpServerAdapters.HttpTestServer.of(httpsServer);
+ https_1_1_Server.addHandler(handler, "/");
+ https_1_1_Server.start();
+ System.out.println("Started HTTPS v1.1 server at " + https_1_1_Server.serverAuthority());
+ httpsURI = new URI("https://" + https_1_1_Server.serverAuthority() + "/");
+
+ // HTTP/2 - create servers with http and https
+ http2_Server = HttpServerAdapters.HttpTestServer.of(new Http2TestServer(sa.getHostString(), false, null));
+ http2_Server.addHandler(handler, "/");
+ http2_Server.start();
+ System.out.println("Started HTTP v2 server at " + http2_Server.serverAuthority());
+ http2URI = new URI("http://" + http2_Server.serverAuthority() + "/");
+
+ https2_Server = HttpServerAdapters.HttpTestServer.of(new Http2TestServer(sa.getHostString(), true, sslContext));
+ https2_Server.addHandler(handler, "/");
+ https2_Server.start();
+ System.out.println("Started HTTPS v2 server at " + https2_Server.serverAuthority());
+ https2URI = new URI("https://" + https2_Server.serverAuthority() + "/");
+ }
+
+ // stop each of the started servers
+ @AfterClass
+ public static void afterClass() throws Exception {
+ // stop each of the server and accumulate any exception
+ // that might happen during stop and finally throw
+ // the accumulated exception(s)
+ var e = safeStop(http1_1_Server, null);
+ e = safeStop(https_1_1_Server, e);
+ e = safeStop(http2_Server, e);
+ e = safeStop(https2_Server, e);
+ // throw any exception that happened during stop
+ if (e != null) {
+ throw e;
+ }
+ }
+
+ /**
+ * Stops the server and returns (instead of throwing) any exception that might
+ * have occurred during stop. If {@code prevException} is not null then any
+ * exception during stop of the {@code server} will be added as a suppressed
+ * exception to the {@code prevException} and the {@code prevException} will be
+ * returned.
+ */
+ private static Exception safeStop(HttpServerAdapters.HttpTestServer server, Exception prevException) {
+ if (server == null) {
+ return null;
+ }
+ var serverAuthority = server.serverAuthority();
+ try {
+ server.stop();
+ } catch (Exception e) {
+ System.err.println("Failed to stop server " + serverAuthority);
+ if (prevException == null) {
+ return e;
+ }
+ prevException.addSuppressed(e);
+ return prevException;
+ }
+ return prevException;
+ }
+
+ @DataProvider(name = "params")
+ private Object[][] paramsProvider() throws Exception {
+ final List