Index: httpclient/src/main/java/org/apache/http/impl/conn/DefaultHttpRoutePlanner.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- httpclient/src/main/java/org/apache/http/impl/conn/DefaultHttpRoutePlanner.java (revision 1391400) +++ httpclient/src/main/java/org/apache/http/impl/conn/DefaultHttpRoutePlanner.java (revision ) @@ -35,6 +35,7 @@ import org.apache.http.HttpException; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; +import org.apache.http.osgi.ProxyWrapper; import org.apache.http.protocol.HttpContext; import org.apache.http.conn.routing.HttpRoute; @@ -105,8 +106,10 @@ final InetAddress local = ConnRouteParams.getLocalAddress(request.getParams()); - final HttpHost proxy = - ConnRouteParams.getDefaultProxy(request.getParams()); + HttpHost proxy = ConnRouteParams.getDefaultProxy(request.getParams()); + if (null == proxy) { + proxy = ProxyWrapper.getProxy(target.getHostName()); + } final Scheme schm; try { Index: httpclient/src/main/java/org/apache/http/osgi/ProxySelector.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- httpclient/src/main/java/org/apache/http/osgi/ProxySelector.java (revision ) +++ httpclient/src/main/java/org/apache/http/osgi/ProxySelector.java (revision ) @@ -0,0 +1,314 @@ +package org.apache.http.osgi; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.NTCredentials; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.impl.client.AbstractHttpClient; +import org.apache.sling.commons.osgi.PropertiesUtil; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The proxy selector processes a proxy configuration (in #configure()). If no proxy configuration is present it assumes + * a default configuration (proxying enabled with no proxy host). + *

+ * The selector only provides a proxy host for targets that are not listed in the configurable host exclusion list. + */ +final class ProxySelector { + + public static final String PROP_PROXY_ENABLED = "proxy.enabled"; + + public static final boolean DEFAULT_PROXY_ENABLED = false; + + public static final String PROP_PROXY_HOST = "proxy.host"; + + public static final String DEFAULT_PROXY_HOST = null; + + public static final String PROP_PROXY_EXCEPTIONS = "proxy.exceptions"; + + public static final String[] DEFAULT_PROXY_EXCEPTIONS = {"localhost", + "127.0.0.1"}; + + public static final String PROP_PROXY_USERNAME = "proxy.user"; + + public static final String DEFAULT_PROXY_USERNAME = null; + + public static final String PROP_PROXY_PASSWORD = "proxy.password"; + + public static final String DEFAULT_PROXY_PASSWORD = null; + + public static final String PROP_PROXY_NTLM_HOST = "proxy.ntlm.host"; + + public static final String DEFAULT_PROXY_NTLM_HOST = null; + + public static final String PROP_PROXY_NTLM_DOMAIN = "proxy.ntlm.domain"; + + public static final String DEFAULT_PROXY_NTLM_DOMAIN = null; + + static final Pattern IP_MASK_PATTERN = + Pattern.compile("(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})(/(\\d{1,2}))?"); + + private final Log log = LogFactory.getLog(getClass()); + + private boolean proxyEnabled; + + private HttpHost proxyHost; + + private AuthScope proxyAuthScope; + + private Credentials proxyCredentials; + + private HostMatcher[] exceptions; + + ProxySelector() { + this.proxyEnabled = false; + this.proxyHost = null; + this.exceptions = new HostMatcher[0]; + } + + final void configure(final Dictionary properties) { + if (properties != null) { + // updated configuration + boolean pEnabled = PropertiesUtil.toBoolean(properties.get(PROP_PROXY_ENABLED), DEFAULT_PROXY_ENABLED); + setProxyEnabled(pEnabled); + setProxyHost(pEnabled ? PropertiesUtil.toString(properties.get(PROP_PROXY_HOST), + DEFAULT_PROXY_HOST) : DEFAULT_PROXY_HOST); + setProxyExceptions(PropertiesUtil.toStringArray( + properties.get(PROP_PROXY_EXCEPTIONS), DEFAULT_PROXY_EXCEPTIONS)); + setProxyCredentials( + PropertiesUtil.toString(properties.get(PROP_PROXY_USERNAME), DEFAULT_PROXY_USERNAME), + PropertiesUtil.toString(properties.get(PROP_PROXY_PASSWORD), DEFAULT_PROXY_PASSWORD), + PropertiesUtil.toString(properties.get(PROP_PROXY_NTLM_HOST), DEFAULT_PROXY_NTLM_HOST), + PropertiesUtil.toString(properties.get(PROP_PROXY_NTLM_DOMAIN), DEFAULT_PROXY_NTLM_DOMAIN) + ); + } else { + // deleted or no configuration -> revert to default values + setProxyEnabled(DEFAULT_PROXY_ENABLED); + setProxyHost(DEFAULT_PROXY_HOST); + setProxyExceptions(DEFAULT_PROXY_EXCEPTIONS); + setProxyCredentials(DEFAULT_PROXY_USERNAME, DEFAULT_PROXY_PASSWORD, + DEFAULT_PROXY_NTLM_HOST, DEFAULT_PROXY_NTLM_DOMAIN); + } + + log.info("ProxySelector reconfigured: enabled=" + proxyEnabled + ", host:" + proxyHost); + } + + private void setProxyEnabled(boolean proxyEnabled) { + this.proxyEnabled = proxyEnabled; + } + + private void setProxyHost(String proxyHostConfig) { + + // reset the proxy host and authscope + proxyHost = null; + proxyAuthScope = null; + + if (proxyHostConfig != null) { + String[] parts = proxyHostConfig.split(":"); + if (parts.length != 2) { + log.error("ProxyHost: Missing coloing in format; expect host:port, get: " + proxyHostConfig); + } else if (parts[0].length() == 0) { + log.error("ProxyHost: Empty host name; expect host:port, get: " + proxyHostConfig); + } else if (parts[1].length() == 0) { + log.error("ProxyHost: Empty port number; expect host:port, get: " + proxyHostConfig); + } else { + // everything is fine so far + try { + InetAddress.getByName(parts[0]); + final int port = Integer.parseInt(parts[1]); + proxyHost = new HttpHost(parts[0], port); + proxyAuthScope = new AuthScope(parts[0], port); + } catch (NumberFormatException nfe) { + log.error("ProxyHost: Port not a number; expect host:port, get: " + proxyHostConfig); + } catch (UnknownHostException e) { + log.error("ProxyHost: Proxy Host name '" + parts[0] + "' does not resolve", e); + } + } + } + } + + private void setProxyCredentials(String userName, String passWord, String host, String domain) { + if (userName != null && userName.length() > 0) { + if (host != null && host.length() > 0 && domain != null + && domain.length() > 0) { + proxyCredentials = new NTCredentials(userName, passWord, host, domain); + } else { + proxyCredentials = new UsernamePasswordCredentials(userName, passWord); + } + } else { + proxyCredentials = null; + } + } + + private void setProxyExceptions(String[] exceptions) { + ArrayList exceptionList = new ArrayList(); + if (exceptions != null) { + for (final String exception : exceptions) { + exceptionList.add(createMatcher(exception.trim())); + } + } + this.exceptions = exceptionList.toArray(new HostMatcher[exceptionList.size()]); + } + + final HttpHost getProxy(String targetHost) { + HttpHost proxy; + if (proxyEnabled && proxyHost != null) { + proxy = this.proxyHost; + HostMatcher[] exceptions = this.exceptions; + for (final HostMatcher exception : exceptions) { + if (exception.matches(targetHost)) { + proxy = null; + break; + } + } + } else { + proxy = null; + } + + return proxy; + } + + final void setProxyCredentials(AbstractHttpClient client, HttpHost proxy) { + if (proxy != null + && proxy.equals(this.proxyHost) + && proxyEnabled + && proxyAuthScope != null + && proxyCredentials != null) { + + client.getCredentialsProvider() + .setCredentials(new AuthScope(proxy.getHostName(), proxy.getPort()), proxyCredentials); + } + } + + private static HostMatcher createMatcher(final String name) { + final NetworkAddress na = NetworkAddress.parse(name); + if (na != null) { + return new IPAddressMatcher(na); + } + + if (name.startsWith(".")) { + return new DomainNameMatcher(name); + } + + return new HostNameMatcher(name); + } + + static interface HostMatcher { + + boolean matches(String host); + } + + static class HostNameMatcher implements HostMatcher { + + private final String hostName; + + HostNameMatcher(String hostName) { + this.hostName = hostName; + } + + public boolean matches(String host) { + return hostName.equalsIgnoreCase(host); + } + + public String getHostName() { + return hostName; + } + } + + static class DomainNameMatcher implements HostMatcher { + + private final String domainName; + + DomainNameMatcher(String domainName) { + this.domainName = domainName.toLowerCase(); + } + + public boolean matches(String host) { + return host.toLowerCase().endsWith(domainName); + } + + public String getDomainName() { + return domainName; + } + } + + static class IPAddressMatcher implements HostMatcher { + + private final NetworkAddress address; + + IPAddressMatcher(NetworkAddress address) { + this.address = address; + } + + public boolean matches(String host) { + NetworkAddress hostAddress = NetworkAddress.parse(host); + return hostAddress != null && address.address == (hostAddress.address & address.mask); + } + + public int getIpAddress() { + return address.address; + } + + public int getNetMask() { + return address.mask; + } + } + + static class NetworkAddress { + + final int address; + + final int mask; + + static NetworkAddress parse(String adrSpec) { + + if (null != adrSpec) { + final Matcher nameMatcher = IP_MASK_PATTERN.matcher(adrSpec); + if (nameMatcher.matches()) { + try { + int i1 = toInt(nameMatcher.group(1), 255); + int i2 = toInt(nameMatcher.group(2), 255); + int i3 = toInt(nameMatcher.group(3), 255); + int i4 = toInt(nameMatcher.group(4), 255); + int ip = i1 << 24 | i2 << 16 | i3 << 8 | i4; + + int mask = toInt(nameMatcher.group(6), 32); + mask = (mask == 32) ? -1 : -1 - (-1 >>> mask); + + return new NetworkAddress(ip, mask); + } catch (NumberFormatException nfe) { + // not expected after the pattern match ! + } + } + } + + return null; + } + + private static int toInt(String value, int max) { + if (value == null || value.length() == 0) { + return max; + } + + int number = Integer.parseInt(value); + if (number > max) { + number = max; + } + return number; + } + + NetworkAddress(int address, int mask) { + this.address = address; + this.mask = mask; + } + } +} Index: httpclient/src/main/java/org/apache/http/osgi/Activator.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- httpclient/src/main/java/org/apache/http/osgi/Activator.java (revision ) +++ httpclient/src/main/java/org/apache/http/osgi/Activator.java (revision ) @@ -0,0 +1,54 @@ +package org.apache.http.osgi; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.ManagedService; + +import java.util.Dictionary; +import java.util.Hashtable; + +/** + * This is the bundle activator. It registers the activator as a service, so it can receive configuration properties. + * Once a configuration is updated, it sets up proxies according to the configuration properties. + *

+ * The activator also is responsible for unregistering the service upon bundle shutdown. Additionally the shutdown + * ensures that all open connections of all http-clients are terminated, so the bundle properly stops. Finally, upon + * shutdown, the proxy setup is cleared. + */ +public class Activator extends ProxyWrapper implements BundleActivator, ManagedService { + + private ServiceRegistration configurator; + + public void start(BundleContext context) throws Exception { + + // ensure we receive configurations for the proxy selector + Hashtable props = new Hashtable(); + props.put(Constants.SERVICE_PID, "org.apache.http.client"); + props.put(Constants.SERVICE_VENDOR, context.getBundle().getHeaders(Constants.BUNDLE_VENDOR)); + props.put(Constants.SERVICE_DESCRIPTION, "Apache HttpComponents Client Configurator"); + configurator = context.registerService(ManagedService.class.getName(), this, props); + } + + public void stop(BundleContext context) throws Exception { + + // stop receiving configuration + if (configurator != null) { + configurator.unregister(); + } + + // ensure all connections are terminated + // TODO MultiThreadedHttpConnectionManager.shutdownAll(); + + // clear the proxy selector again + setupProxies(null); + } + + // ---------- ManagedService + + @SuppressWarnings("unchecked") + public void updated(@SuppressWarnings("rawtypes") final Dictionary properties) { + setupProxies(properties); + } +} Index: httpclient/src/main/java/org/apache/http/impl/client/AbstractHttpClient.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- httpclient/src/main/java/org/apache/http/impl/client/AbstractHttpClient.java (revision 1391400) +++ httpclient/src/main/java/org/apache/http/impl/client/AbstractHttpClient.java (revision ) @@ -86,6 +86,7 @@ import org.apache.http.impl.cookie.NetscapeDraftSpecFactory; import org.apache.http.impl.cookie.RFC2109SpecFactory; import org.apache.http.impl.cookie.RFC2965SpecFactory; +import org.apache.http.osgi.ProxyWrapper; import org.apache.http.params.HttpParams; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.BasicHttpProcessor; @@ -879,6 +880,8 @@ : (HttpHost) determineParams(request).getParameter( ClientPNames.DEFAULT_HOST); HttpRoute route = routePlanner.determineRoute(targetForRoute, request, execContext); + + ProxyWrapper.setProxyCredentials(this, route.getProxyHost()); HttpResponse out; try { Index: httpclient/src/main/java/org/apache/http/osgi/ProxyWrapper.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- httpclient/src/main/java/org/apache/http/osgi/ProxyWrapper.java (revision ) +++ httpclient/src/main/java/org/apache/http/osgi/ProxyWrapper.java (revision ) @@ -0,0 +1,36 @@ +package org.apache.http.osgi; + +import org.apache.http.HttpHost; +import org.apache.http.impl.client.AbstractHttpClient; + +import java.util.Dictionary; + +/** + * The proxy wrapper holds a proxy selector, with which it can process proxy configurations and retrieve proxy hosts + * from the selector. Also, the wrapper is able to set credentials on a proxy. + */ +public abstract class ProxyWrapper { + + private static ProxySelector proxySelector = new ProxySelector(); + + protected void setupProxies(Dictionary configuration) { + proxySelector.configure(configuration); + } + + public static HttpHost getProxy(String targetHost) { + ProxySelector ps = proxySelector; + if (ps != null) { + return proxySelector.getProxy(targetHost); + } + + return null; + } + + public static void setProxyCredentials(AbstractHttpClient client, final HttpHost proxy) { + ProxySelector ps = proxySelector; + if (proxy != null && ps != null) { + proxySelector.setProxyCredentials(client, proxy); + } + } + +} Index: httpclient/pom.xml IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- httpclient/pom.xml (revision 1391400) +++ httpclient/pom.xml (revision ) @@ -66,6 +66,30 @@ mockito-core test + + org.osgi + org.osgi.core + 4.2.0 + compile + + + org.osgi + org.osgi.compendium + 4.2.0 + compile + + + javax.servlet + servlet-api + 2.4 + compile + + + org.apache.sling + org.apache.sling.commons.osgi + 2.1.0 + compile + Index: httpclient/src/main/java/org/apache/http/osgi/ProxyContextListener.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- httpclient/src/main/java/org/apache/http/osgi/ProxyContextListener.java (revision ) +++ httpclient/src/main/java/org/apache/http/osgi/ProxyContextListener.java (revision ) @@ -0,0 +1,28 @@ +package org.apache.http.osgi; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; + +public class ProxyContextListener extends ProxyWrapper implements ServletContextListener { + + public void contextInitialized(ServletContextEvent servletContextEvent) { + ServletContext context = servletContextEvent.getServletContext(); + @SuppressWarnings("unchecked") + Enumeration e = context.getInitParameterNames(); + Dictionary properties = new Hashtable(); + while (e.hasMoreElements()) { + String paramName = e.nextElement(); + properties.put(paramName, context.getInitParameter(paramName)); + } + setupProxies(properties); + } + + public void contextDestroyed(ServletContextEvent servletContextEvent) { + setupProxies(null); + } + +}