Skip to content

Commit

Permalink
Merge pull request #16724 from iterate-ch/bugfix/GH-16723
Browse files Browse the repository at this point in the history
Always sort result set from DNS resolver
  • Loading branch information
dkocher authored Dec 29, 2024
2 parents 61744a7 + 7963efd commit af43640
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 17 deletions.
34 changes: 29 additions & 5 deletions core/dylib/src/test/java/ch/cyberduck/core/ResolverTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import org.junit.Test;
import org.junit.experimental.categories.Category;

import java.net.Inet4Address;
import java.net.Inet6Address;

import static org.junit.Assert.*;

@Category(IntegrationTest.class)
Expand Down Expand Up @@ -61,16 +64,37 @@ public void testResolveIPv6Localhost() throws Exception {
@Ignore
public void testResolveLinkLocalZoneIndexInterfaceName() throws Exception {
assertEquals("andaman.local", new Resolver().resolve("andaman.local", new DisabledCancelCallback())[0].getHostName());
assertEquals("fe80:0:0:0:c62c:3ff:fe0b:8670%en0", new Resolver().resolve("fe80::c62c:3ff:fe0b:8670%en0", new DisabledCancelCallback())[0].getHostAddress());
assertEquals("fe80:0:0:0:c62c:3ff:fe0b:8670%en0", new Resolver()
.resolve("fe80::c62c:3ff:fe0b:8670%en0", new DisabledCancelCallback())[0].getHostAddress());
}

@Test
public void testResolvePublicDnsIPv6Only() throws Exception {
assertEquals("2001:470:a085:999:0:0:0:21", new Resolver()
.resolve("ftp6.netbsd.org", new DisabledCancelCallback())[0].getHostAddress());
}

@Test
public void testResolvePublicDNSIPv6Only() throws Exception {
assertEquals("2001:470:a085:999:0:0:0:21", new Resolver().resolve("ftp6.netbsd.org", new DisabledCancelCallback())[0].getHostAddress());
public void testResolvePublicPreferIpv6() throws Exception {
assertTrue("2600:3c02:0:0:f03c:91ff:fe89:e8b1", new Resolver(true)
.resolve("intronetworks.cs.luc.edu", new DisabledCancelCallback())[0] instanceof Inet6Address);
assertTrue("2620:100:6025:14:0:0:a27d:450e", new Resolver(true)
.resolve("content.dropboxapi.com", new DisabledCancelCallback())[0] instanceof Inet6Address);
assertTrue("2a00:1450:400a:803:0:0:0:200a", new Resolver(true)
.resolve("www.googleapis.com", new DisabledCancelCallback())[0] instanceof Inet6Address);
assertTrue("2603:1027:1:28:0:0:0:25", new Resolver(true)
.resolve("login.microsoftonline.com", new DisabledCancelCallback())[0] instanceof Inet6Address);
}

@Test
public void testResolvePublicDNSIPv6ForHybrid() throws Exception {
assertEquals("2600:3c02:0:0:f03c:91ff:fe89:e8b1", new Resolver(true).resolve("intronetworks.cs.luc.edu", new DisabledCancelCallback())[0].getHostAddress());
public void testResolvePublicPreferIpv4() throws Exception {
assertTrue("23.239.19.84", new Resolver(false)
.resolve("intronetworks.cs.luc.edu", new DisabledCancelCallback())[0] instanceof Inet4Address);
assertTrue("162.125.69.14", new Resolver(false)
.resolve("content.dropboxapi.com", new DisabledCancelCallback())[0] instanceof Inet4Address);
assertTrue("216.58.215.234", new Resolver(false)
.resolve("www.googleapis.com", new DisabledCancelCallback())[0] instanceof Inet4Address);
assertTrue("20.190.181.0", new Resolver(false)
.resolve("login.microsoftonline.com", new DisabledCancelCallback())[0] instanceof Inet4Address);
}
}
23 changes: 15 additions & 8 deletions core/src/main/java/ch/cyberduck/core/Resolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.time.Duration;
import java.util.Arrays;
import java.util.Set;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicReference;
Expand Down Expand Up @@ -67,20 +69,25 @@ public Resolver(final boolean preferIPv6) {
*/
public InetAddress[] resolve(final String hostname, final CancelCallback callback) throws ResolveFailedException, ResolveCanceledException {
final CountDownLatch signal = new CountDownLatch(1);
final AtomicReference<Set<InetAddress>> resolved = new AtomicReference<>();
final AtomicReference<List<InetAddress>> resolved = new AtomicReference<>();
final AtomicReference<UnknownHostException> failure = new AtomicReference<>();
final Thread resolver = threadFactory.newThread(new Runnable() {
@Override
public void run() {
try {
final InetAddress[] allByName = InetAddress.getAllByName(hostname);
resolved.set(Arrays.stream(allByName).collect(Collectors.toSet()));
if(preferIPv6) {
final Set<InetAddress> filtered = Arrays.stream(allByName).filter(inetAddress -> inetAddress instanceof Inet6Address).collect(Collectors.toSet());
if(!filtered.isEmpty()) {
resolved.set(filtered);
resolved.set(Arrays.stream(allByName).sorted(new Comparator<InetAddress>() {
@Override
public int compare(final InetAddress o1, final InetAddress o2) {
if(o1 instanceof Inet6Address && o2 instanceof Inet4Address) {
return preferIPv6 ? -1 : 1;
}
if(o2 instanceof Inet6Address && o1 instanceof Inet4Address) {
return preferIPv6 ? 1 : -1;
}
return 0;
}
}
}).collect(Collectors.toList()));
log.info("Resolved {} to {}", hostname, Arrays.toString(resolved.get().toArray()));
}
catch(UnknownHostException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package ch.cyberduck.core.http;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpHost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Lookup;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.DnsResolver;
import org.apache.http.conn.HttpClientConnectionOperator;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.conn.ManagedHttpClientConnection;
import org.apache.http.conn.SchemePortResolver;
import org.apache.http.conn.UnsupportedSchemeException;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
import org.apache.http.impl.conn.DefaultSchemePortResolver;
import org.apache.http.impl.conn.SystemDefaultDnsResolver;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.Args;

import java.io.IOException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;

public class CustomHttpClientConnectionOperator implements HttpClientConnectionOperator {

static final String SOCKET_FACTORY_REGISTRY = "http.socket-factory-registry";

private final Log log = LogFactory.getLog(getClass());

private final Lookup<ConnectionSocketFactory> socketFactoryRegistry;
private final SchemePortResolver schemePortResolver;
private final DnsResolver dnsResolver;

public CustomHttpClientConnectionOperator(
final Lookup<ConnectionSocketFactory> socketFactoryRegistry,
final SchemePortResolver schemePortResolver,
final DnsResolver dnsResolver) {
super();
Args.notNull(socketFactoryRegistry, "Socket factory registry");
this.socketFactoryRegistry = socketFactoryRegistry;
this.schemePortResolver = schemePortResolver != null ? schemePortResolver :
DefaultSchemePortResolver.INSTANCE;
this.dnsResolver = dnsResolver != null ? dnsResolver :
SystemDefaultDnsResolver.INSTANCE;
}

private Lookup<ConnectionSocketFactory> getSocketFactoryRegistry(final HttpContext context) {
Lookup<ConnectionSocketFactory> reg = (Lookup<ConnectionSocketFactory>) context.getAttribute(
SOCKET_FACTORY_REGISTRY);
if(reg == null) {
reg = this.socketFactoryRegistry;
}
return reg;
}

@Override
public void connect(
final ManagedHttpClientConnection conn,
final HttpHost host,
final InetSocketAddress localAddress,
final int connectTimeout,
final SocketConfig socketConfig,
final HttpContext context) throws IOException {
final Lookup<ConnectionSocketFactory> registry = getSocketFactoryRegistry(context);
final ConnectionSocketFactory sf = registry.lookup(host.getSchemeName());
if(sf == null) {
throw new UnsupportedSchemeException(host.getSchemeName() +
" protocol is not supported");
}
final InetAddress[] addresses = host.getAddress() != null ?
new InetAddress[]{host.getAddress()} : this.dnsResolver.resolve(host.getHostName());
final int port = this.schemePortResolver.resolve(host);
for(int i = 0; i < addresses.length; i++) {
final InetAddress address = addresses[i];
final boolean last = i == addresses.length - 1;

Socket sock = sf.createSocket(context);
sock.setSoTimeout(socketConfig.getSoTimeout());
sock.setReuseAddress(socketConfig.isSoReuseAddress());
sock.setTcpNoDelay(socketConfig.isTcpNoDelay());
sock.setKeepAlive(socketConfig.isSoKeepAlive());
if(socketConfig.getRcvBufSize() > 0) {
sock.setReceiveBufferSize(socketConfig.getRcvBufSize());
}
if(socketConfig.getSndBufSize() > 0) {
sock.setSendBufferSize(socketConfig.getSndBufSize());
}

final int linger = socketConfig.getSoLinger();
if(linger >= 0) {
sock.setSoLinger(true, linger);
}
conn.bind(sock);

final InetSocketAddress remoteAddress = new InetSocketAddress(address, port);
if(this.log.isDebugEnabled()) {
this.log.debug("Connecting to " + remoteAddress);
}
try {
sock = sf.connectSocket(
connectTimeout, sock, host, remoteAddress, localAddress, context);
conn.bind(sock);
if(this.log.isDebugEnabled()) {
this.log.debug("Connection established " + conn);
}
return;
}
catch(final SocketTimeoutException ex) {
if(last) {
throw new ConnectTimeoutException(ex, host, addresses);
}
}
catch(final ConnectException ex) {
if(last) {
final String msg = ex.getMessage();
throw "Connection timed out".equals(msg)
? new ConnectTimeoutException(ex, host, addresses)
: new HttpHostConnectException(ex, host, addresses);
}
}
catch(final SocketException ex) {
if(last) {
throw ex;
}
}
if(this.log.isDebugEnabled()) {
this.log.debug("Connect to " + remoteAddress + " timed out. " +
"Connection will be retried using another IP address");
}
}
}

@Override
public void upgrade(
final ManagedHttpClientConnection conn,
final HttpHost host,
final HttpContext context) throws IOException {
final HttpClientContext clientContext = HttpClientContext.adapt(context);
final Lookup<ConnectionSocketFactory> registry = getSocketFactoryRegistry(clientContext);
final ConnectionSocketFactory sf = registry.lookup(host.getSchemeName());
if(sf == null) {
throw new UnsupportedSchemeException(host.getSchemeName() +
" protocol is not supported");
}
if(!(sf instanceof LayeredConnectionSocketFactory)) {
throw new UnsupportedSchemeException(host.getSchemeName() +
" protocol does not support connection upgrade");
}
final LayeredConnectionSocketFactory lsf = (LayeredConnectionSocketFactory) sf;
Socket sock = conn.getSocket();
final int port = this.schemePortResolver.resolve(host);
sock = lsf.createLayeredSocket(sock, host.getHostName(), port, context);
conn.bind(sock);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import org.apache.http.impl.client.WinHttpClients;
import org.apache.http.impl.conn.DefaultRoutePlanner;
import org.apache.http.impl.conn.DefaultSchemePortResolver;
import org.apache.http.impl.conn.ManagedHttpClientConnectionFactory;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.HttpContext;
import org.apache.logging.log4j.LogManager;
Expand All @@ -68,6 +69,7 @@
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;

public class HttpConnectionPoolBuilder {
private static final Logger log = LogManager.getLogger(HttpConnectionPoolBuilder.class);
Expand Down Expand Up @@ -139,9 +141,9 @@ public HttpConnectionPoolBuilder(final Host host,
}

/**
* @param proxyfinder Proxy configuration
* @param listener Log listener
* @param prompt Prompt for proxy credentials
* @param proxyfinder Proxy configuration
* @param listener Log listener
* @param prompt Prompt for proxy credentials
* @return Builder for HTTP client
*/
public HttpClientBuilder build(final ProxyFinder proxyfinder, final TranscriptListener listener, final LoginCallback prompt) {
Expand Down Expand Up @@ -238,7 +240,9 @@ public Registry<ConnectionSocketFactory> createRegistry() {

public PoolingHttpClientConnectionManager createConnectionManager(final Registry<ConnectionSocketFactory> registry) {
log.debug("Setup connection pool with registry {}", registry);
final PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager(registry, new CustomDnsResolver());
final PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager(
new CustomHttpClientConnectionOperator(registry, DefaultSchemePortResolver.INSTANCE, new CustomDnsResolver()),
ManagedHttpClientConnectionFactory.INSTANCE, -1, TimeUnit.MILLISECONDS);
manager.setMaxTotal(new HostPreferences(host).getInteger("http.connections.total"));
manager.setDefaultMaxPerRoute(new HostPreferences(host).getInteger("http.connections.route"));
// Detect connections that have become stale (half-closed) while kept inactive in the pool
Expand Down

0 comments on commit af43640

Please sign in to comment.