diff --git a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java
index f98111a26d..b4743676ce 100755
--- a/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java
+++ b/services/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java
@@ -39,6 +39,8 @@ import java.util.concurrent.TimeUnit;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
+import static org.keycloak.utils.StringUtil.isBlank;
+
/**
* The default {@link HttpClientFactory} for {@link HttpClientProvider HttpClientProvider's} used by Keycloak for outbound HTTP calls.
*
@@ -63,6 +65,10 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
private static final Logger logger = Logger.getLogger(DefaultHttpClientFactory.class);
private static final String configScope = "keycloak.connectionsHttpClient.default.";
+ private static final String HTTPS_PROXY = "https_proxy";
+ private static final String HTTP_PROXY = "http_proxy";
+ private static final String NO_PROXY = "no_proxy";
+
private volatile CloseableHttpClient httpClient;
private Config.Scope config;
@@ -145,12 +151,27 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
String clientKeystore = config.get("client-keystore");
String clientKeystorePassword = config.get("client-keystore-password");
String clientPrivateKeyPassword = config.get("client-key-password");
- String[] proxyMappings = config.getArray("proxy-mappings");
boolean disableTrustManager = config.getBoolean("disable-trust-manager", false);
boolean expectContinueEnabled = getBooleanConfigWithSysPropFallback("expect-continue-enabled", false);
boolean resuseConnections = getBooleanConfigWithSysPropFallback("reuse-connections", true);
+ // optionally configure proxy mappings
+ // direct SPI config (e.g. via standalone.xml) takes precedence over env vars
+ // lower case env vars take precedence over upper case env vars
+ ProxyMappings proxyMappings = ProxyMappings.valueOf(config.getArray("proxy-mappings"));
+ if (proxyMappings == null || proxyMappings.isEmpty()) {
+ logger.debug("Trying to use proxy mapping from env vars");
+ String httpProxy = getEnvVarValue(HTTPS_PROXY);
+ if (isBlank(httpProxy)) {
+ httpProxy = getEnvVarValue(HTTP_PROXY);
+ }
+ String noProxy = getEnvVarValue(NO_PROXY);
+
+ logger.debugf("httpProxy: %s, noProxy: %s", httpProxy, noProxy);
+ proxyMappings = ProxyMappings.withFixedProxyMapping(httpProxy, noProxy);
+ }
+
HttpClientBuilder builder = new HttpClientBuilder();
builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS)
@@ -161,7 +182,7 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
.connectionTTL(connectionTTL, TimeUnit.MILLISECONDS)
.maxConnectionIdleTime(maxConnectionIdleTime, TimeUnit.MILLISECONDS)
.disableCookies(disableCookies)
- .proxyMappings(ProxyMappings.valueOf(proxyMappings))
+ .proxyMappings(proxyMappings)
.expectContinueEnabled(expectContinueEnabled)
.reuseConnections(resuseConnections);
@@ -215,4 +236,12 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
return value != null ? value : defaultValue;
}
+ private String getEnvVarValue(String name) {
+ String value = System.getenv(name.toLowerCase());
+ if (isBlank(value)) {
+ value = System.getenv(name.toUpperCase());
+ }
+ return value;
+ }
+
}
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 3186567a84..4e2b46bcf4 100644
--- a/services/src/main/java/org/keycloak/connections/httpclient/ProxyMappings.java
+++ b/services/src/main/java/org/keycloak/connections/httpclient/ProxyMappings.java
@@ -21,6 +21,7 @@ import org.apache.http.auth.UsernamePasswordCredentials;
import org.jboss.logging.Logger;
import java.net.URI;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -30,6 +31,8 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
+import static org.keycloak.utils.StringUtil.isBlank;
+
/**
* {@link ProxyMappings} describes an ordered mapping for hostname regex patterns to a {@link HttpHost} proxy.
*
@@ -44,9 +47,11 @@ public class ProxyMappings {
private static final ProxyMappings EMPTY_MAPPING = valueOf(Collections.emptyList());
+ private static final String NO_PROXY_DELIMITER = ",";
+
private final List entries;
- private static Map hostnameToProxyCache = new ConcurrentHashMap<>();
+ private static final Map hostnameToProxyCache = new ConcurrentHashMap<>();
/**
* Creates a {@link ProxyMappings} from the provided {@link ProxyMapping Entries}.
@@ -93,6 +98,34 @@ public class ProxyMappings {
return valueOf(Arrays.asList(proxyMappings));
}
+ /**
+ * Creates a new {@link ProxyMappings} from provided parameters representing the established {@code HTTP(S)_PROXY}
+ * and {@code NO_PROXY} environment variables.
+ *
+ * @param httpProxy a proxy used for all hosts except the ones specified in {@code noProxy}
+ * @param noProxy a list of hosts (separated by comma) that should not use proxy;
+ * all suffixes are matched too (e.g. redhat.com will also match access.redhat.com)
+ * @return
+ * @see https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/
+ */
+ public static ProxyMappings withFixedProxyMapping(String httpProxy, String noProxy) {
+ List proxyMappings = new ArrayList<>();
+
+ if (!isBlank(httpProxy)) {
+ // noProxy must be first as it's more specific than .*
+ if (!isBlank(noProxy)) {
+ for (String host : noProxy.split(NO_PROXY_DELIMITER)) {
+ // do not support regex in no_proxy
+ proxyMappings.add(new ProxyMapping(Pattern.compile("(?:.+\\.)?" + Pattern.quote(host)), null, null));
+ }
+ }
+
+ proxyMappings.add(ProxyMapping.valueOf(".*" + ProxyMapping.DELIMITER + httpProxy));
+ }
+
+ return proxyMappings.isEmpty() ? EMPTY_MAPPING : new ProxyMappings(proxyMappings);
+ }
+
public boolean isEmpty() {
return this.entries.isEmpty();
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 60c095b8e5..25fa531180 100644
--- a/services/src/test/java/org/keycloak/connections/httpclient/ProxyMappingsTest.java
+++ b/services/src/test/java/org/keycloak/connections/httpclient/ProxyMappingsTest.java
@@ -29,6 +29,8 @@ import java.util.List;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
/**
@@ -203,4 +205,48 @@ public class ProxyMappingsTest {
ProxyMapping forSalesForce = proxyMappingsWithProxyAuthen.getProxyFor("login.salesforce.com");
assertThat(forSalesForce.getProxyHost().getHostName(), is("fallback"));
}
+
+ @Test
+ public void shouldReturnMappingForHttpProxy() {
+ ProxyMappings proxyMappings = ProxyMappings.withFixedProxyMapping("https://some-proxy.redhat.com:8080", null);
+
+ ProxyMapping forGoogle = proxyMappings.getProxyFor("login.google.com");
+ assertEquals("some-proxy.redhat.com", forGoogle.getProxyHost().getHostName());
+ }
+
+ @Test
+ public void shouldReturnMappingForHttpProxyWithNoProxy() {
+ ProxyMappings proxyMappings = ProxyMappings.withFixedProxyMapping("https://some-proxy.redhat.com:8080", "login.facebook.com");
+
+ assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("login.google.com").getProxyHost().getHostName());
+ assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("facebook.com").getProxyHost().getHostName());
+
+ assertNull(proxyMappings.getProxyFor("login.facebook.com").getProxyHost());
+ assertNull(proxyMappings.getProxyFor("auth.login.facebook.com").getProxyHost());
+ }
+
+ @Test
+ public void shouldReturnMappingForHttpProxyWithMultipleNoProxy() {
+ ProxyMappings proxyMappings = ProxyMappings.withFixedProxyMapping("https://some-proxy.redhat.com:8080", "login.facebook.com,corp.com");
+
+ assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("login.google.com").getProxyHost().getHostName());
+ assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("facebook.com").getProxyHost().getHostName());
+
+ assertNull(proxyMappings.getProxyFor("login.facebook.com").getProxyHost());
+ assertNull(proxyMappings.getProxyFor("auth.login.facebook.com").getProxyHost());
+ assertNull(proxyMappings.getProxyFor("myapp.acme.corp.com").getProxyHost());
+ }
+
+ @Test
+ public void shouldReturnMappingForNoProxyWithInvalidChars() {
+ ProxyMappings proxyMappings = ProxyMappings.withFixedProxyMapping("https://some-proxy.redhat.com:8080", "[lj]ogin.facebook.com");
+
+ assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("login.facebook.com").getProxyHost().getHostName());
+ assertEquals("some-proxy.redhat.com", proxyMappings.getProxyFor("jogin.facebook.com").getProxyHost().getHostName());
+ }
+
+ @Test
+ public void shouldReturnEmptyMappingForEmptyHttpProxy() {
+ assertNull(ProxyMappings.withFixedProxyMapping(null, "facebook.com"));
+ }
}
\ No newline at end of file