Skip to content

Commit

Permalink
8209137: Add ability to bind to specific local address to HTTP client
Browse files Browse the repository at this point in the history
Reviewed-by: dfuchs, michaelm
  • Loading branch information
jaikiran committed May 16, 2022
1 parent 65da38d commit f4258a5
Show file tree
Hide file tree
Showing 11 changed files with 593 additions and 9 deletions.
2 changes: 2 additions & 0 deletions src/java.base/share/lib/security/default.policy
Original file line number Diff line number Diff line change
Expand Up @@ -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:*","*:*";
Expand Down
46 changes: 45 additions & 1 deletion src/java.net.http/share/classes/java/net/http/HttpClient.java
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -27,6 +27,7 @@

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;
Expand Down Expand Up @@ -354,16 +355,59 @@ public interface Builder {
*/
public Builder authenticator(Authenticator authenticator);

/**
* Binds the socket to this local address when creating
* connections for sending requests.
*
* <p> 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.
*
* <p> 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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -27,6 +27,7 @@

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;
Expand All @@ -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) {
Expand Down Expand Up @@ -130,6 +132,12 @@ public HttpClientBuilderImpl authenticator(Authenticator a) {
return this;
}

@Override
public HttpClient.Builder localAddress(final InetAddress localAddr) {
this.localAddr = localAddr;
return this;
}

@Override
public HttpClient build() {
return HttpClientImpl.create(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
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;
Expand Down Expand Up @@ -335,6 +336,7 @@ static void abortPendingRequests(HttpClientImpl client, Throwable reason) {
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
Expand Down Expand Up @@ -431,6 +433,17 @@ private HttpClientImpl(HttpClientBuilderImpl builder,
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();
Expand Down Expand Up @@ -1528,6 +1541,10 @@ public Version version() {
return version;
}

InetAddress localAddress() {
return localAddr;
}

String dbgString() {
return dbgTag;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,27 @@ public CompletableFuture<Void> connectAsync(Exchange<?> exchange) {
}
}

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<SocketChannel> 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<Boolean> pa =
() -> chan.connect(Utils.resolveAddress(address));
try {
Expand Down
93 changes: 87 additions & 6 deletions test/jdk/java/net/httpclient/HttpClientBuilderTest.java
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -26,12 +26,12 @@
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;
Expand All @@ -40,7 +40,6 @@
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;
Expand All @@ -55,6 +54,7 @@

/*
* @test
* @bug 8209137
* @summary HttpClient[.Builder] API and behaviour checks
* @library /test/lib
* @build jdk.test.lib.net.SimpleSSLContext
Expand All @@ -65,6 +65,7 @@ public class HttpClientBuilderTest {

static final Class<NullPointerException> NPE = NullPointerException.class;
static final Class<IllegalArgumentException> IAE = IllegalArgumentException.class;
static final Class<UnsupportedOperationException> UOE = UnsupportedOperationException.class;

@Test
public void testDefaults() throws Exception {
Expand Down Expand Up @@ -262,6 +263,89 @@ static void testPriority() throws Exception {
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/");
Expand Down Expand Up @@ -303,9 +387,6 @@ static class HttpConnectRequest extends HttpRequest {

// ---

static final Class<UnsupportedOperationException> UOE =
UnsupportedOperationException.class;

@Test
static void testUnsupportedWebSocket() throws Exception {
// @implSpec The default implementation of this method throws
Expand Down
Loading

3 comments on commit f4258a5

@openjdk-notifier
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@varada1110
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/backport jdk17u-dev

@openjdk
Copy link

@openjdk openjdk bot commented on f4258a5 Nov 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@varada1110 Could not automatically backport f4258a50 to openjdk/jdk17u-dev due to conflicts in the following files:

  • test/jdk/java/net/httpclient/http2/server/Http2TestServer.java

Please fetch the appropriate branch/commit and manually resolve these conflicts by using the following commands in your personal fork of openjdk/jdk17u-dev. Note: these commands are just some suggestions and you can use other equivalent commands you know.

# Fetch the up-to-date version of the target branch
$ git fetch --no-tags https://git.openjdk.org/jdk17u-dev.git master:master

# Check out the target branch and create your own branch to backport
$ git checkout master
$ git checkout -b varada1110-backport-f4258a50

# Fetch the commit you want to backport
$ git fetch --no-tags https://git.openjdk.org/jdk.git f4258a50e0f65ab9c375b9ee79f31de98d872550

# Backport the commit
$ git cherry-pick --no-commit f4258a50e0f65ab9c375b9ee79f31de98d872550
# Resolve conflicts now

# Commit the files you have modified
$ git add files/with/resolved/conflicts
$ git commit -m 'Backport f4258a50e0f65ab9c375b9ee79f31de98d872550'

Once you have resolved the conflicts as explained above continue with creating a pull request towards the openjdk/jdk17u-dev with the title Backport f4258a50e0f65ab9c375b9ee79f31de98d872550.

Please sign in to comment.