From ce13e3606f0c193342f54d9b8bcea8a7162cf27a Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Tue, 6 Sep 2011 12:20:26 +0000 Subject: [PATCH] HTTPCLIENT-1123: Support for pluggable DNS resolvers. Contributed by Alin Vasile git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@1165635 13f79535-47bb-0310-9956-ffa450edef68 --- RELEASE_NOTES.txt | 15 ++- .../org/apache/http/conn/DnsResolver.java | 52 ++++++++ .../conn/DefaultClientConnectionOperator.java | 35 +++++- .../http/impl/conn/InMemoryDnsResolver.java | 119 ++++++++++++++++++ .../conn/PoolingClientConnectionManager.java | 24 +++- .../TestDefaultClientConnectOperator.java | 78 ++++++++++++ 6 files changed, 320 insertions(+), 3 deletions(-) create mode 100644 httpclient/src/main/java/org/apache/http/conn/DnsResolver.java create mode 100644 httpclient/src/main/java/org/apache/http/impl/conn/InMemoryDnsResolver.java create mode 100644 httpclient/src/test/java/org/apache/http/impl/conn/TestDefaultClientConnectOperator.java diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt index 6e79c7413..01ac8bab4 100644 --- a/RELEASE_NOTES.txt +++ b/RELEASE_NOTES.txt @@ -1,8 +1,18 @@ -Changes since 4.1.1 +Changes since 4.1.2 + +* [HTTPCLIENT-1123] Support for pluggable DNS resolvers. + Contributed by Alin Vasile * [HTTPCLIENT-1120] DefaultHttpRequestRetryHandler#retryRequest should not retry aborted requests Contributed by Alin Vasile + +Release 4.1.2 +------------------- + +The HttpClient 4.1.2 is a bug fix release that addresses a number of non-critical issues reported +since release 4.1.1. + * [HTTPCLIENT-1100] Missing Content-Length header makes cached entry invalid Contributed by Bart Robeyns @@ -32,6 +42,9 @@ Changes since 4.1.1 do not correctly handle content streaming. Contributed by James Abley +* [HTTPCLIENT-1051] Avoid reverse DNS lookups when opening SSL connections by IP address. + Contributed by Oleg Kalnichevski + Release 4.1.1 ------------------- diff --git a/httpclient/src/main/java/org/apache/http/conn/DnsResolver.java b/httpclient/src/main/java/org/apache/http/conn/DnsResolver.java new file mode 100644 index 000000000..97556b6fd --- /dev/null +++ b/httpclient/src/main/java/org/apache/http/conn/DnsResolver.java @@ -0,0 +1,52 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.http.conn; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * Users may implement this interface to override the normal DNS lookup offered + * by the OS. + */ +public interface DnsResolver { + + /** + * Returns the IP address for the specified host name, or null if the given + * host is not recognized or the associated IP address cannot be used to + * build an InetAddress instance. + * + * @see InetAddress + * + * @param host + * The host name to be resolved by this resolver. + * @return The IP address associated to the given host name, or null if the + * host name is not known by the implementation class. + */ + InetAddress[] resolve(String host) throws UnknownHostException; + +} \ No newline at end of file diff --git a/httpclient/src/main/java/org/apache/http/impl/conn/DefaultClientConnectionOperator.java b/httpclient/src/main/java/org/apache/http/impl/conn/DefaultClientConnectionOperator.java index 2e5f8a4ee..e93d5cce7 100644 --- a/httpclient/src/main/java/org/apache/http/impl/conn/DefaultClientConnectionOperator.java +++ b/httpclient/src/main/java/org/apache/http/impl/conn/DefaultClientConnectionOperator.java @@ -53,6 +53,8 @@ import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.conn.scheme.SchemeSocketFactory; +import org.apache.http.conn.DnsResolver; + /** * Default implementation of a {@link ClientConnectionOperator}. It uses a {@link SchemeRegistry} * to look up {@link SchemeSocketFactory} objects. @@ -90,6 +92,9 @@ public class DefaultClientConnectionOperator implements ClientConnectionOperator /** The scheme registry for looking up socket factories. */ protected final SchemeRegistry schemeRegistry; // @ThreadSafe + /** the custom-configured DNS lookup mechanism. */ + protected final DnsResolver dnsResolver; + /** * Creates a new client connection operator for the given scheme registry. * @@ -100,6 +105,26 @@ public class DefaultClientConnectionOperator implements ClientConnectionOperator throw new IllegalArgumentException("Scheme registry amy not be null"); } this.schemeRegistry = schemes; + this.dnsResolver = null; + } + + /** + * Creates a new client connection operator for the given scheme registry + * and the given custom DNS lookup mechanism. + * + * @param schemes + * the scheme registry + * @param dnsResolver + * the custom DNS lookup mechanism + */ + public DefaultClientConnectionOperator(final SchemeRegistry schemes,final DnsResolver dnsResolver) { + if (schemes == null) { + throw new IllegalArgumentException( + "Scheme registry amy not be null"); + } + + this.schemeRegistry = schemes; + this.dnsResolver = dnsResolver; } public OperatedClientConnection createConnection() { @@ -233,6 +258,10 @@ public class DefaultClientConnectionOperator implements ClientConnectionOperator * Resolves the given host name to an array of corresponding IP addresses, based on the * configured name service on the system. * + * If a custom DNS resolver is provided, the given host will be searched in + * it first. If the host is not configured, the default OS DNS-lookup + * mechanism is used. + * * @param host host name to resolve * @return array of IP addresses * @exception UnknownHostException if no IP address for the host could be determined. @@ -240,7 +269,11 @@ public class DefaultClientConnectionOperator implements ClientConnectionOperator * @since 4.1 */ protected InetAddress[] resolveHostname(final String host) throws UnknownHostException { - return InetAddress.getAllByName(host); + if (dnsResolver != null) { + return dnsResolver.resolve(host); + } else { + return InetAddress.getAllByName(host); + } } } diff --git a/httpclient/src/main/java/org/apache/http/impl/conn/InMemoryDnsResolver.java b/httpclient/src/main/java/org/apache/http/impl/conn/InMemoryDnsResolver.java new file mode 100644 index 000000000..f5de9ebab --- /dev/null +++ b/httpclient/src/main/java/org/apache/http/impl/conn/InMemoryDnsResolver.java @@ -0,0 +1,119 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.http.impl.conn; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.conn.util.InetAddressUtils; +import org.apache.http.conn.DnsResolver; + +/** + * In-memory DNS resolver implementation with entries built using the + * {@link InMemoryDnsResolver#add(String, String) method}. + * + * Currently this class supports only IPv4 addresses. + * + */ +public class InMemoryDnsResolver implements DnsResolver { + + /** Logger associated to this class. */ + private final Log log = LogFactory.getLog(InMemoryDnsResolver.class); + + /** + * In-memory collection that will hold the associations between a host name + * and an array of InetAddress instances. + */ + private Map dnsMap; + + /** + * Builds a DNS resolver that will resolve the host names against a + * collection held in-memory. + */ + public InMemoryDnsResolver() { + dnsMap = new ConcurrentHashMap(); + } + + /** + * Associates the given IP address to the given host in this DNS overrider. + * + * @param host + * The host name to be associated with the given IP. + * @param ip + * IPv4 address to be resolved by this DNS overrider to the given + * host name. + * + * @throws IllegalArgumentException + * if the given IP is not a valid IPv4 address or an InetAddress + * instance cannot be built based on the given IPv4 address. + * + * @see InetAddress#getByAddress + */ + public void add(final String host, final String ip) { + if (!InetAddressUtils.isIPv4Address(ip)) { + throw new IllegalArgumentException(ip + " must be a valid IPv4 address"); + } + + String[] ipParts = ip.split("\\."); + + byte[] byteIpAddress = new byte[4]; + + for (int i = 0; i < 4; i++) { + byteIpAddress[i] = Integer.decode(ipParts[i]).byteValue(); + } + + try { + dnsMap.put(host, new InetAddress[] { InetAddress.getByAddress(byteIpAddress) }); + } catch (UnknownHostException e) { + log.error("Unable to build InetAddress for " + ip, e); + throw new IllegalArgumentException(e); + } + + } + + /** + * {@inheritDoc} + */ + public InetAddress[] resolve(String host) throws UnknownHostException { + InetAddress[] resolvedAddresses = dnsMap.get(host); + if (log.isInfoEnabled()) { + log.info("Resolving " + host + " to " + Arrays.deepToString(resolvedAddresses)); + } + + if(resolvedAddresses == null){ + throw new UnknownHostException(host + " cannot be resolved."); + } + + return resolvedAddresses; + } + +} \ No newline at end of file diff --git a/httpclient/src/main/java/org/apache/http/impl/conn/PoolingClientConnectionManager.java b/httpclient/src/main/java/org/apache/http/impl/conn/PoolingClientConnectionManager.java index 4d65ec8fd..8222bc34a 100644 --- a/httpclient/src/main/java/org/apache/http/impl/conn/PoolingClientConnectionManager.java +++ b/httpclient/src/main/java/org/apache/http/impl/conn/PoolingClientConnectionManager.java @@ -47,6 +47,7 @@ import org.apache.http.pool.ConnPoolControl; import org.apache.http.pool.PoolStats; import org.apache.http.impl.conn.DefaultClientConnectionOperator; import org.apache.http.impl.conn.SchemeRegistryFactory; +import org.apache.http.conn.DnsResolver; /** * Manages a pool of {@link OperatedClientConnection client connections} and @@ -77,10 +78,17 @@ public class PoolingClientConnectionManager implements ClientConnectionManager, private final ClientConnectionOperator operator; + /** the custom-configured DNS lookup mechanism. */ + private final DnsResolver dnsResolver; + public PoolingClientConnectionManager(final SchemeRegistry schreg) { this(schreg, -1, TimeUnit.MILLISECONDS); } + public PoolingClientConnectionManager(final SchemeRegistry schreg,final DnsResolver dnsResolver) { + this(schreg, -1, TimeUnit.MILLISECONDS,dnsResolver); + } + public PoolingClientConnectionManager() { this(SchemeRegistryFactory.createDefault()); } @@ -93,6 +101,20 @@ public class PoolingClientConnectionManager implements ClientConnectionManager, throw new IllegalArgumentException("Scheme registry may not be null"); } this.schemeRegistry = schemeRegistry; + this.dnsResolver = null; + this.operator = createConnectionOperator(schemeRegistry); + this.pool = new HttpConnPool(this.log, 2, 20, timeToLive, tunit); + } + + public PoolingClientConnectionManager(final SchemeRegistry schemeRegistry, + final long timeToLive, final TimeUnit tunit, + final DnsResolver dnsResolver) { + super(); + if (schemeRegistry == null) { + throw new IllegalArgumentException("Scheme registry may not be null"); + } + this.schemeRegistry = schemeRegistry; + this.dnsResolver = dnsResolver; this.operator = createConnectionOperator(schemeRegistry); this.pool = new HttpConnPool(this.log, 2, 20, timeToLive, tunit); } @@ -119,7 +141,7 @@ public class PoolingClientConnectionManager implements ClientConnectionManager, * @return the connection operator to use */ protected ClientConnectionOperator createConnectionOperator(SchemeRegistry schreg) { - return new DefaultClientConnectionOperator(schreg); + return new DefaultClientConnectionOperator(schreg, this.dnsResolver); } public SchemeRegistry getSchemeRegistry() { diff --git a/httpclient/src/test/java/org/apache/http/impl/conn/TestDefaultClientConnectOperator.java b/httpclient/src/test/java/org/apache/http/impl/conn/TestDefaultClientConnectOperator.java new file mode 100644 index 000000000..dc9f94b5b --- /dev/null +++ b/httpclient/src/test/java/org/apache/http/impl/conn/TestDefaultClientConnectOperator.java @@ -0,0 +1,78 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.http.impl.conn; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.apache.http.conn.DnsResolver; + +import org.junit.Assert; +import org.junit.Test; + +public class TestDefaultClientConnectOperator { + + @Test + public void testCustomDnsResolver() throws Exception { + DnsResolver dnsResolver = mock(DnsResolver.class); + InetAddress[] firstAddress = translateIp("192.168.1.1"); + when(dnsResolver.resolve("somehost.example.com")).thenReturn(firstAddress); + + InetAddress[] secondAddress = translateIp("192.168.12.16"); + when(dnsResolver.resolve("otherhost.example.com")).thenReturn(secondAddress); + + DefaultClientConnectionOperator operator = new DefaultClientConnectionOperator( + SchemeRegistryFactory.createDefault(), dnsResolver); + + Assert.assertArrayEquals(firstAddress, operator.resolveHostname("somehost.example.com")); + Assert.assertArrayEquals(secondAddress, operator.resolveHostname("otherhost.example.com")); + } + + @Test(expected=UnknownHostException.class) + public void testDnsResolverUnknownHost() throws Exception { + DnsResolver dnsResolver = mock(DnsResolver.class); + when(dnsResolver.resolve("unknown.example.com")).thenThrow(new UnknownHostException()); + + DefaultClientConnectionOperator operator = new DefaultClientConnectionOperator( + SchemeRegistryFactory.createDefault(), dnsResolver); + operator.resolveHostname("unknown.example.com"); + } + + private InetAddress[] translateIp(String ip) throws UnknownHostException { + String[] ipParts = ip.split("\\."); + + byte[] byteIpAddress = new byte[4]; + for (int i = 0; i < 4; i++) { + byteIpAddress[i] = Integer.decode(ipParts[i]).byteValue(); + } + return new InetAddress[] { InetAddress.getByAddress(byteIpAddress) }; + } + +} \ No newline at end of file