Skip to content

Commit

Permalink
HTTPCLIENT-2350 - Refactored the connect method in DefaultHttpClientC…
Browse files Browse the repository at this point in the history
…onnectionOperator to leverage a customizable Function<HttpHost, InetSocketAddress[]> for address resolution, allowing for more flexible hostname handling and direct support for unresolved addresses.
  • Loading branch information
arturobernalg committed Nov 14, 2024
1 parent 4b2a365 commit a9da185
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.function.Function;

import javax.net.ssl.SSLSocket;

Expand Down Expand Up @@ -158,33 +159,39 @@ public void connect(
Args.notNull(endpointHost, "Host");
Args.notNull(socketConfig, "Socket config");
Args.notNull(context, "Context");
final InetAddress[] remoteAddresses;
if (endpointHost.getAddress() != null) {
remoteAddresses = new InetAddress[] { endpointHost.getAddress() };
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("{} resolving remote address", endpointHost.getHostName());
}

remoteAddresses = this.dnsResolver.resolve(endpointHost.getHostName());

if (LOG.isDebugEnabled()) {
LOG.debug("{} resolved to {}", endpointHost.getHostName(), remoteAddresses == null ? "null" : Arrays.asList(remoteAddresses));
}

if (remoteAddresses == null || remoteAddresses.length == 0) {
throw new UnknownHostException(endpointHost.getHostName());
}
}

final Timeout soTimeout = socketConfig.getSoTimeout();
final SocketAddress socksProxyAddress = socketConfig.getSocksProxyAddress();
final Proxy socksProxy = socksProxyAddress != null ? new Proxy(Proxy.Type.SOCKS, socksProxyAddress) : null;
final int port = this.schemePortResolver.resolve(endpointHost.getSchemeName(), endpointHost);

final Function<HttpHost, InetSocketAddress[]> addressResolver = host -> {
if (host.getAddress() != null) {
return new InetSocketAddress[]{new InetSocketAddress(host.getAddress(), port)};
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("{} resolving remote address", host.getHostName());
}
try {
final InetAddress[] remoteAddresses = this.dnsResolver.resolve(host.getHostName());
if (remoteAddresses == null || remoteAddresses.length == 0) {
throw new UnknownHostException(host.getHostName());
}
return Arrays.stream(remoteAddresses)
.map(address -> new InetSocketAddress(address, port))
.toArray(InetSocketAddress[]::new);
} catch (final UnknownHostException e) {
return new InetSocketAddress[]{InetSocketAddress.createUnresolved(host.getHostName(), port)};
}
}
};

final InetSocketAddress[] remoteAddresses = addressResolver.apply(endpointHost);

for (int i = 0; i < remoteAddresses.length; i++) {
final InetAddress address = remoteAddresses[i];
final InetSocketAddress remoteAddress = remoteAddresses[i];
final boolean last = i == remoteAddresses.length - 1;
final InetSocketAddress remoteAddress = new InetSocketAddress(address, port);

onBeforeSocketConnect(context, endpointHost);
if (LOG.isDebugEnabled()) {
LOG.debug("{} connecting {}->{} ({})", endpointHost, localAddress, remoteAddress, connectTimeout);
Expand Down Expand Up @@ -245,7 +252,7 @@ public void connect(
if (LOG.isDebugEnabled()) {
LOG.debug("{} connection to {} failed ({}); terminating operation", endpointHost, remoteAddress, ex.getClass());
}
throw ConnectExceptionSupport.enhance(ex, endpointHost, remoteAddresses);
throw ConnectExceptionSupport.enhance(ex, endpointHost);
}
if (LOG.isDebugEnabled()) {
LOG.debug("{} connection to {} failed ({}); retrying connection to the next address", endpointHost, remoteAddress, ex.getClass());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,4 +279,69 @@ void testUpgradeNonLayeringScheme() {
connectionOperator.upgrade(conn, host, context));
}

@Test
void testConnectWithDisableDnsResolution() throws Exception {
final HttpClientContext context = HttpClientContext.create();
final HttpHost host = new HttpHost("someonion.onion");
final InetAddress local = InetAddress.getByAddress(new byte[]{127, 0, 0, 0});
final int port = 80;

Mockito.when(schemePortResolver.resolve(host.getSchemeName(), host)).thenReturn(port);
Mockito.when(detachedSocketFactory.create(Mockito.any())).thenReturn(socket);

final SocketConfig socketConfig = SocketConfig.custom()
.setSoKeepAlive(true)
.setSoReuseAddress(true)
.setSoTimeout(5000, TimeUnit.MILLISECONDS)
.setTcpNoDelay(true)
.setSoLinger(50, TimeUnit.MILLISECONDS)
.build();
final InetSocketAddress localAddress = new InetSocketAddress(local, 0);
final InetSocketAddress remoteAddress = InetSocketAddress.createUnresolved(host.getHostName(), port);

connectionOperator.connect(conn, host, null, localAddress, Timeout.ofMilliseconds(123), socketConfig, null, context);

// Verify that the socket was created and attempted to connect without DNS resolution
Mockito.verify(socket).setKeepAlive(true);
Mockito.verify(socket).setReuseAddress(true);
Mockito.verify(socket).setSoTimeout(5000);
Mockito.verify(socket).setSoLinger(true, 50);
Mockito.verify(socket).setTcpNoDelay(true);
Mockito.verify(socket).bind(localAddress);

Mockito.verify(socket).connect(remoteAddress, 123);
Mockito.verify(conn, Mockito.times(2)).bind(socket);
}

@Test
void testConnectWithDnsResolutionAndFallback() throws Exception {
final HttpClientContext context = HttpClientContext.create();
final HttpHost host = new HttpHost("fallbackhost.com");
final InetAddress local = InetAddress.getByAddress(new byte[] {127, 0, 0, 0});
final int port = 8080;
final InetAddress ip1 = InetAddress.getByAddress(new byte[] {10, 0, 0, 1});
final InetAddress ip2 = InetAddress.getByAddress(new byte[] {10, 0, 0, 2});

Mockito.when(dnsResolver.resolve("fallbackhost.com")).thenReturn(new InetAddress[] { ip1, ip2 });
Mockito.when(schemePortResolver.resolve(host.getSchemeName(), host)).thenReturn(port);
Mockito.when(detachedSocketFactory.create(Mockito.any())).thenReturn(socket);

Mockito.doThrow(new ConnectException()).when(socket).connect(Mockito.eq(new InetSocketAddress(ip1, port)), Mockito.anyInt());

final InetSocketAddress localAddress = new InetSocketAddress(local, 0);
final SocketConfig socketConfig = SocketConfig.custom()
.setSoKeepAlive(true)
.setSoReuseAddress(true)
.setSoTimeout(5000, TimeUnit.MILLISECONDS)
.setTcpNoDelay(true)
.setSoLinger(50, TimeUnit.MILLISECONDS)
.build();

connectionOperator.connect(conn, host, null, localAddress, Timeout.ofMilliseconds(123), socketConfig, null, context);

// Verify fallback behavior after connection failure to the first address
Mockito.verify(socket, Mockito.times(2)).bind(localAddress);
Mockito.verify(socket).connect(new InetSocketAddress(ip2, port), 123);
Mockito.verify(conn, Mockito.times(3)).bind(socket);
}
}

0 comments on commit a9da185

Please sign in to comment.