KEYCLOAK-10659 Proxy authentication support for proxy-mappings

This commit is contained in:
k-tamura 2019-06-20 22:19:56 +09:00 committed by Hynek Mlnařík
parent 5aab03d915
commit 562dc3ff8c
3 changed files with 141 additions and 62 deletions

View file

@ -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<ProxyMapping> entries;
private static Map<String, ProxyMapping> 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 +
'}';
}
}

View file

@ -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;
}
}

View file

@ -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<String> MAPPINGS_WITH_FALLBACK_AND_PROXY_EXCEPTION = new ArrayList<>();
private static final List<String> 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"));
}
}