KEYCLOAK-10659 Proxy authentication support for proxy-mappings
This commit is contained in:
parent
5aab03d915
commit
562dc3ff8c
3 changed files with 141 additions and 62 deletions
|
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue