From 562dc3ff8ce11fc459f976deb8233626d32c9228 Mon Sep 17 00:00:00 2001 From: k-tamura Date: Thu, 20 Jun 2019 22:19:56 +0900 Subject: [PATCH] KEYCLOAK-10659 Proxy authentication support for proxy-mappings --- .../connections/httpclient/ProxyMappings.java | 76 +++++++++---- .../ProxyMappingsAwareRoutePlanner.java | 22 +++- .../httpclient/ProxyMappingsTest.java | 105 ++++++++++++------ 3 files changed, 141 insertions(+), 62 deletions(-) diff --git a/services/src/main/java/org/keycloak/connections/httpclient/ProxyMappings.java b/services/src/main/java/org/keycloak/connections/httpclient/ProxyMappings.java index c534cb8c47d..3186567a84c 100644 --- a/services/src/main/java/org/keycloak/connections/httpclient/ProxyMappings.java +++ b/services/src/main/java/org/keycloak/connections/httpclient/ProxyMappings.java @@ -17,12 +17,16 @@ package org.keycloak.connections.httpclient; import org.apache.http.HttpHost; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.jboss.logging.Logger; import java.net.URI; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -36,10 +40,14 @@ import java.util.stream.Collectors; */ public class ProxyMappings { + private static final Logger logger = Logger.getLogger(ProxyMappings.class); + private static final ProxyMappings EMPTY_MAPPING = valueOf(Collections.emptyList()); private final List entries; + private static Map hostnameToProxyCache = new ConcurrentHashMap<>(); + /** * Creates a {@link ProxyMappings} from the provided {@link ProxyMapping Entries}. * @@ -92,18 +100,28 @@ public class ProxyMappings { /** * @param hostname - * @return the {@link HttpHost} proxy associated with the first matching hostname {@link Pattern} - * or {@literal null} if none matches. + * @return the {@link ProxyMapping} associated with the first matching hostname {@link Pattern} + * or the {@link ProxyMapping} including {@literal null} as {@link HttpHost} if none matches. */ - public HttpHost getProxyFor(String hostname) { + public ProxyMapping getProxyFor(String hostname) { Objects.requireNonNull(hostname, "hostname"); + if (hostnameToProxyCache.containsKey(hostname)) { + return hostnameToProxyCache.get(hostname); + } + ProxyMapping proxyMapping = entries.stream() // + .filter(e -> e.matches(hostname)) // + .findFirst() // + .orElse(null); + if (proxyMapping == null) { + proxyMapping = new ProxyMapping(null, null, null); + } + hostnameToProxyCache.put(hostname, proxyMapping); + return proxyMapping; + } - return entries.stream() // - .filter(e -> e.matches(hostname)) // - .findFirst() // - .map(ProxyMapping::getProxy) // - .orElse(null); + public static void clearCache() { + hostnameToProxyCache.clear(); } /** @@ -117,19 +135,26 @@ public class ProxyMappings { private final Pattern hostnamePattern; - private final HttpHost proxy; + private final HttpHost proxyHost; - public ProxyMapping(Pattern hostnamePattern, HttpHost proxy) { + private final UsernamePasswordCredentials proxyCredentials; + + public ProxyMapping(Pattern hostnamePattern, HttpHost proxyHost, UsernamePasswordCredentials proxyCredentials) { this.hostnamePattern = hostnamePattern; - this.proxy = proxy; + this.proxyHost = proxyHost; + this.proxyCredentials = proxyCredentials; } public Pattern getHostnamePattern() { return hostnamePattern; } - public HttpHost getProxy() { - return proxy; + public HttpHost getProxyHost() { + return proxyHost; + } + + public UsernamePasswordCredentials getProxyCredentials() { + return proxyCredentials; } public boolean matches(String hostname) { @@ -166,26 +191,31 @@ public class ProxyMappings { String proxyUriString = mappingTokens[1]; Pattern hostPattern = Pattern.compile(hostPatternRegex); - HttpHost proxyHost = toProxyHost(proxyUriString); - - return new ProxyMapping(hostPattern, proxyHost); - } - - private static HttpHost toProxyHost(String proxyUriString) { - if (NO_PROXY.equals(proxyUriString)) { - return null; + return new ProxyMapping(hostPattern, null, null); } URI uri = URI.create(proxyUriString); - return new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()); + String userInfo = uri.getUserInfo(); + UsernamePasswordCredentials proxyCredentials = null; + if (userInfo != null) { + if (userInfo.indexOf(":") > 0) { + String[] credencials = userInfo.split(":", 2); + if (credencials != null && credencials.length == 2) { + proxyCredentials = new UsernamePasswordCredentials(credencials[0], credencials[1]); + } + } else { + logger.warn("Invalid proxy credentials: " + userInfo); + } + } + return new ProxyMapping(hostPattern, new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), proxyCredentials); } @Override public String toString() { return "ProxyMapping{" + "hostnamePattern=" + hostnamePattern + - ", proxy=" + proxy + + ", proxyHost=" + proxyHost + '}'; } } diff --git a/services/src/main/java/org/keycloak/connections/httpclient/ProxyMappingsAwareRoutePlanner.java b/services/src/main/java/org/keycloak/connections/httpclient/ProxyMappingsAwareRoutePlanner.java index 353a5bb0f6a..ec149092fe4 100644 --- a/services/src/main/java/org/keycloak/connections/httpclient/ProxyMappingsAwareRoutePlanner.java +++ b/services/src/main/java/org/keycloak/connections/httpclient/ProxyMappingsAwareRoutePlanner.java @@ -19,11 +19,18 @@ package org.keycloak.connections.httpclient; import org.apache.http.HttpException; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.conn.DefaultRoutePlanner; import org.apache.http.impl.conn.DefaultSchemePortResolver; import org.apache.http.protocol.HttpContext; import org.jboss.logging.Logger; +import static org.keycloak.connections.httpclient.ProxyMappings.ProxyMapping; + /** * A {@link DefaultRoutePlanner} that determines the proxy to use for a given target hostname by consulting * the given {@link ProxyMappings}. @@ -45,9 +52,16 @@ public class ProxyMappingsAwareRoutePlanner extends DefaultRoutePlanner { @Override protected HttpHost determineProxy(HttpHost target, HttpRequest request, HttpContext context) throws HttpException { - HttpHost proxy = proxyMappings.getProxyFor(target.getHostName()); - LOG.debugf("Returning proxy=%s for targetHost=%s", proxy, target.getHostName()); - - return proxy; + String targetHostName = target.getHostName(); + ProxyMapping proxyMapping = proxyMappings.getProxyFor(targetHostName); + LOG.debugf("Returning proxyMapping=%s for targetHost=%s", proxyMapping, targetHostName); + UsernamePasswordCredentials proxyCredentials = proxyMapping.getProxyCredentials(); + HttpHost proxyHost = proxyMapping.getProxyHost(); + if (proxyCredentials != null) { + CredentialsProvider credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials(new AuthScope(proxyHost.getHostName(), proxyHost.getPort()), proxyCredentials); + context.setAttribute(HttpClientContext.CREDS_PROVIDER, credsProvider); + } + return proxyHost; } } diff --git a/services/src/test/java/org/keycloak/connections/httpclient/ProxyMappingsTest.java b/services/src/test/java/org/keycloak/connections/httpclient/ProxyMappingsTest.java index 3841521a4ce..60c095b8e51 100644 --- a/services/src/test/java/org/keycloak/connections/httpclient/ProxyMappingsTest.java +++ b/services/src/test/java/org/keycloak/connections/httpclient/ProxyMappingsTest.java @@ -16,11 +16,11 @@ */ package org.keycloak.connections.httpclient; -import org.apache.http.HttpHost; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.keycloak.connections.httpclient.ProxyMappings.ProxyMapping; import java.util.ArrayList; import java.util.Arrays; @@ -47,6 +47,8 @@ public class ProxyMappingsTest { private static final List MAPPINGS_WITH_FALLBACK_AND_PROXY_EXCEPTION = new ArrayList<>(); + private static final List MAPPINGS_WITH_PROXY_AUTHENTICATION = new ArrayList<>(); + static { MAPPINGS_WITH_FALLBACK.addAll(DEFAULT_MAPPINGS); MAPPINGS_WITH_FALLBACK.add(".*;http://fallback:8080"); @@ -58,6 +60,11 @@ public class ProxyMappingsTest { MAPPINGS_WITH_FALLBACK_AND_PROXY_EXCEPTION.add(".*;http://fallback:8080"); } + static { + MAPPINGS_WITH_PROXY_AUTHENTICATION.add(".*stackexchange\\.com;http://user01:pas2w0rd@proxy3:88"); + MAPPINGS_WITH_PROXY_AUTHENTICATION.addAll(MAPPINGS_WITH_FALLBACK_AND_PROXY_EXCEPTION); + } + @Rule public ExpectedException expectedException = ExpectedException.none(); @@ -65,6 +72,7 @@ public class ProxyMappingsTest { @Before public void setup() { + ProxyMappings.clearCache(); proxyMappings = ProxyMappings.valueOf(DEFAULT_MAPPINGS); } @@ -76,40 +84,40 @@ public class ProxyMappingsTest { @Test public void shouldReturnProxy1ForConfiguredProxyMapping() { - HttpHost proxy = proxyMappings.getProxyFor("account.google.com"); - assertThat(proxy, is(notNullValue())); - assertThat(proxy.getHostName(), is("proxy1")); + ProxyMapping proxy = proxyMappings.getProxyFor("account.google.com"); + assertThat(proxy.getProxyHost(), is(notNullValue())); + assertThat(proxy.getProxyHost().getHostName(), is("proxy1")); } @Test public void shouldReturnProxy1ForConfiguredProxyMappingAlternative() { - HttpHost proxy = proxyMappings.getProxyFor("www.googleapis.com"); - assertThat(proxy, is(notNullValue())); - assertThat(proxy.getHostName(), is("proxy1")); + ProxyMapping proxy = proxyMappings.getProxyFor("www.googleapis.com"); + assertThat(proxy.getProxyHost(), is(notNullValue())); + assertThat(proxy.getProxyHost().getHostName(), is("proxy1")); } @Test public void shouldReturnProxy1ForConfiguredProxyMappingWithSubDomain() { - HttpHost proxy = proxyMappings.getProxyFor("awesome.account.google.com"); - assertThat(proxy, is(notNullValue())); - assertThat(proxy.getHostName(), is("proxy1")); + ProxyMapping proxy = proxyMappings.getProxyFor("awesome.account.google.com"); + assertThat(proxy.getProxyHost(), is(notNullValue())); + assertThat(proxy.getProxyHost().getHostName(), is("proxy1")); } @Test public void shouldReturnProxy2ForConfiguredProxyMapping() { - HttpHost proxy = proxyMappings.getProxyFor("login.facebook.com"); - assertThat(proxy, is(notNullValue())); - assertThat(proxy.getHostName(), is("proxy2")); + ProxyMapping proxy = proxyMappings.getProxyFor("login.facebook.com"); + assertThat(proxy.getProxyHost(), is(notNullValue())); + assertThat(proxy.getProxyHost().getHostName(), is("proxy2")); } @Test public void shouldReturnNoProxyForUnknownHost() { - HttpHost proxy = proxyMappings.getProxyFor("login.microsoft.com"); - assertThat(proxy, is(nullValue())); + ProxyMapping proxy = proxyMappings.getProxyFor("login.microsoft.com"); + assertThat(proxy.getProxyHost(), is(nullValue())); } @Test @@ -126,8 +134,8 @@ public class ProxyMappingsTest { ProxyMappings proxyMappingsWithFallback = ProxyMappings.valueOf(MAPPINGS_WITH_FALLBACK); - HttpHost proxy = proxyMappingsWithFallback.getProxyFor("login.salesforce.com"); - assertThat(proxy.getHostName(), is("fallback")); + ProxyMapping proxy = proxyMappingsWithFallback.getProxyFor("login.salesforce.com"); + assertThat(proxy.getProxyHost().getHostName(), is("fallback")); } @Test @@ -135,17 +143,17 @@ public class ProxyMappingsTest { ProxyMappings proxyMappingsWithFallback = ProxyMappings.valueOf(MAPPINGS_WITH_FALLBACK); - HttpHost forGoogle = proxyMappingsWithFallback.getProxyFor("login.google.com"); - assertThat(forGoogle.getHostName(), is("proxy1")); + ProxyMapping forGoogle = proxyMappingsWithFallback.getProxyFor("login.google.com"); + assertThat(forGoogle.getProxyHost().getHostName(), is("proxy1")); - HttpHost forFacebook = proxyMappingsWithFallback.getProxyFor("login.facebook.com"); - assertThat(forFacebook.getHostName(), is("proxy2")); + ProxyMapping forFacebook = proxyMappingsWithFallback.getProxyFor("login.facebook.com"); + assertThat(forFacebook.getProxyHost().getHostName(), is("proxy2")); - HttpHost forMicrosoft = proxyMappingsWithFallback.getProxyFor("login.microsoft.com"); - assertThat(forMicrosoft.getHostName(), is("fallback")); + ProxyMapping forMicrosoft = proxyMappingsWithFallback.getProxyFor("login.microsoft.com"); + assertThat(forMicrosoft.getProxyHost().getHostName(), is("fallback")); - HttpHost forSalesForce = proxyMappingsWithFallback.getProxyFor("login.salesforce.com"); - assertThat(forSalesForce.getHostName(), is("fallback")); + ProxyMapping forSalesForce = proxyMappingsWithFallback.getProxyFor("login.salesforce.com"); + assertThat(forSalesForce.getProxyHost().getHostName(), is("fallback")); } @Test @@ -153,19 +161,46 @@ public class ProxyMappingsTest { ProxyMappings proxyMappingsWithFallbackAndProxyException = ProxyMappings.valueOf(MAPPINGS_WITH_FALLBACK_AND_PROXY_EXCEPTION); - HttpHost forGoogle = proxyMappingsWithFallbackAndProxyException.getProxyFor("login.google.com"); - assertThat(forGoogle.getHostName(), is("proxy1")); + ProxyMapping forGoogle = proxyMappingsWithFallbackAndProxyException.getProxyFor("login.google.com"); + assertThat(forGoogle.getProxyHost().getHostName(), is("proxy1")); - HttpHost forFacebook = proxyMappingsWithFallbackAndProxyException.getProxyFor("login.facebook.com"); - assertThat(forFacebook.getHostName(), is("proxy2")); + ProxyMapping forFacebook = proxyMappingsWithFallbackAndProxyException.getProxyFor("login.facebook.com"); + assertThat(forFacebook.getProxyHost().getHostName(), is("proxy2")); - HttpHost forAcmeCorp = proxyMappingsWithFallbackAndProxyException.getProxyFor("myapp.acme.corp.com"); - assertThat(forAcmeCorp, is(nullValue())); + ProxyMapping forAcmeCorp = proxyMappingsWithFallbackAndProxyException.getProxyFor("myapp.acme.corp.com"); + assertThat(forAcmeCorp.getProxyHost(), is(nullValue())); - HttpHost forMicrosoft = proxyMappingsWithFallbackAndProxyException.getProxyFor("login.microsoft.com"); - assertThat(forMicrosoft.getHostName(), is("fallback")); + ProxyMapping forMicrosoft = proxyMappingsWithFallbackAndProxyException.getProxyFor("login.microsoft.com"); + assertThat(forMicrosoft.getProxyHost().getHostName(), is("fallback")); - HttpHost forSalesForce = proxyMappingsWithFallbackAndProxyException.getProxyFor("login.salesforce.com"); - assertThat(forSalesForce.getHostName(), is("fallback")); + ProxyMapping forSalesForce = proxyMappingsWithFallbackAndProxyException.getProxyFor("login.salesforce.com"); + assertThat(forSalesForce.getProxyHost().getHostName(), is("fallback")); + } + + @Test + public void shouldReturnProxyAuthentication() { + + ProxyMappings proxyMappingsWithProxyAuthen = ProxyMappings.valueOf(MAPPINGS_WITH_PROXY_AUTHENTICATION); + + ProxyMapping forGoogle = proxyMappingsWithProxyAuthen.getProxyFor("login.google.com"); + assertThat(forGoogle.getProxyHost().getHostName(), is("proxy1")); + + ProxyMapping forFacebook = proxyMappingsWithProxyAuthen.getProxyFor("login.facebook.com"); + assertThat(forFacebook.getProxyHost().getHostName(), is("proxy2")); + + ProxyMapping forStackOverflow = proxyMappingsWithProxyAuthen.getProxyFor("stackexchange.com"); + assertThat(forStackOverflow.getProxyHost().getHostName(), is("proxy3")); + assertThat(forStackOverflow.getProxyHost().getPort(), is(88)); + assertThat(forStackOverflow.getProxyCredentials().getUserName(), is("user01")); + assertThat(forStackOverflow.getProxyCredentials().getPassword(), is("pas2w0rd")); + + ProxyMapping forAcmeCorp = proxyMappingsWithProxyAuthen.getProxyFor("myapp.acme.corp.com"); + assertThat(forAcmeCorp.getProxyHost(), is(nullValue())); + + ProxyMapping forMicrosoft = proxyMappingsWithProxyAuthen.getProxyFor("login.microsoft.com"); + assertThat(forMicrosoft.getProxyHost().getHostName(), is("fallback")); + + ProxyMapping forSalesForce = proxyMappingsWithProxyAuthen.getProxyFor("login.salesforce.com"); + assertThat(forSalesForce.getProxyHost().getHostName(), is("fallback")); } } \ No newline at end of file