Skip to content

Commit 537d126

Browse files
committed
HTTPCLIENT-2381 Enable automatic mapping of HTTP(S)_PROXY and NO_PROXY environment variables to standard JDK proxy system properties via new EnvironmentProxyConfigurer and make HttpClientBuilder use it by default
1 parent 9d38e5f commit 537d126

File tree

5 files changed

+280
-0
lines changed

5 files changed

+280
-0
lines changed

httpclient5/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@
118118
<artifactId>zstd-jni</artifactId>
119119
<scope>test</scope>
120120
</dependency>
121+
<dependency>
122+
<groupId>com.github.stefanbirkner</groupId>
123+
<artifactId>system-lambda</artifactId>
124+
<scope>test</scope>
125+
</dependency>
121126
</dependencies>
122127

123128
<build>
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.http.config;
28+
29+
import java.net.URI;
30+
import java.security.AccessController;
31+
import java.security.PrivilegedAction;
32+
33+
import org.apache.hc.core5.annotation.Contract;
34+
import org.apache.hc.core5.annotation.ThreadingBehavior;
35+
36+
/**
37+
* <p>Bridges “*nix-style” proxy environment variables to the
38+
* standard JDK proxy system-properties used by {@code ProxySelector}
39+
* and other HTTP libraries that rely on them.</p>
40+
*
41+
* <h4>Mapping rules</h4>
42+
* <ul>
43+
* <li>{@code HTTP_PROXY} → {@code http.proxyHost}, {@code http.proxyPort},
44+
* {@code http.proxyUser}, {@code http.proxyPassword}</li>
45+
* <li>{@code HTTPS_PROXY} → {@code https.proxyHost}, {@code https.proxyPort},
46+
* {@code https.proxyUser}, {@code https.proxyPassword}</li>
47+
* <li>{@code NO_PROXY} → {@code http.nonProxyHosts},
48+
* {@code https.nonProxyHosts} (comma → pipe conversion)</li>
49+
* <li>Lower-case variants ({@code http_proxy}, {@code https_proxy},
50+
* {@code no_proxy}) are also recognised.</li>
51+
* </ul>
52+
*
53+
* <p>The method is <strong>idempotent</strong>: it never overwrites a
54+
* property that is already set (via {@code -D…} or earlier code). All
55+
* writes go through one {@code System.setProperty(…)} call, so it is
56+
* thread-safe.</p>
57+
*
58+
* <p>Automatically invoked from {@code HttpClientBuilder.build()} when
59+
* {@code useSystemProperties()} is in effect (the default of
60+
* {@code HttpClientBuilder.create()}).</p>
61+
*
62+
* @since 5.6
63+
*/
64+
@Contract(threading = ThreadingBehavior.STATELESS)
65+
public final class EnvironmentProxyConfigurer {
66+
67+
private EnvironmentProxyConfigurer() {
68+
}
69+
70+
public static void apply() {
71+
configureForScheme("http", "HTTP_PROXY", "http_proxy");
72+
configureForScheme("https", "HTTPS_PROXY", "https_proxy");
73+
74+
final String noProxy = firstNonEmpty(getenv("NO_PROXY"), getenv("no_proxy"));
75+
if (noProxy != null && System.getProperty("http.nonProxyHosts") == null) {
76+
final String list = noProxy.replace(',', '|');
77+
setProperty("http.nonProxyHosts", list);
78+
/* only write HTTPS when it is still unset */
79+
if (System.getProperty("https.nonProxyHosts") == null) { // ← FIX
80+
setProperty("https.nonProxyHosts", list);
81+
}
82+
}
83+
}
84+
85+
/* -------------------------------------------------------------- */
86+
87+
private static void configureForScheme(final String scheme,
88+
final String upperEnv,
89+
final String lowerEnv) {
90+
91+
if (System.getProperty(scheme + ".proxyHost") != null) {
92+
return; // already configured via -D
93+
}
94+
String val = firstNonEmpty(getenv(upperEnv), getenv(lowerEnv));
95+
if (val == null || val.isEmpty()) {
96+
return;
97+
}
98+
if (val.indexOf("://") < 0) {
99+
val = scheme + "://" + val;
100+
}
101+
102+
final URI uri = URI.create(val);
103+
104+
if (uri.getHost() != null) {
105+
setProperty(scheme + ".proxyHost", uri.getHost());
106+
}
107+
if (uri.getPort() > 0) {
108+
setProperty(scheme + ".proxyPort", Integer.toString(uri.getPort()));
109+
}
110+
111+
final String ui = uri.getUserInfo(); // user:pass
112+
if (ui != null && !ui.isEmpty()) {
113+
final String[] parts = ui.split(":", 2);
114+
setProperty(scheme + ".proxyUser", parts[0]);
115+
if (parts.length == 2) {
116+
setProperty(scheme + ".proxyPassword", parts[1]);
117+
}
118+
}
119+
}
120+
121+
private static String firstNonEmpty(final String a, final String b) {
122+
return (a != null && !a.isEmpty()) ? a
123+
: (b != null && !b.isEmpty()) ? b
124+
: null;
125+
}
126+
127+
private static String getenv(final String key) {
128+
return AccessController.doPrivileged(
129+
(PrivilegedAction<String>) () -> System.getenv(key));
130+
}
131+
132+
private static void setProperty(final String key, final String value) {
133+
AccessController.doPrivileged(
134+
(PrivilegedAction<Void>) () -> {
135+
System.setProperty(key, value);
136+
return null;
137+
});
138+
}
139+
}

httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.apache.hc.client5.http.classic.BackoffManager;
5252
import org.apache.hc.client5.http.classic.ConnectionBackoffStrategy;
5353
import org.apache.hc.client5.http.classic.ExecChainHandler;
54+
import org.apache.hc.client5.http.config.EnvironmentProxyConfigurer;
5455
import org.apache.hc.client5.http.config.RequestConfig;
5556
import org.apache.hc.client5.http.cookie.BasicCookieStore;
5657
import org.apache.hc.client5.http.cookie.CookieSpecFactory;
@@ -838,6 +839,11 @@ protected Function<HttpContext, HttpClientContext> contextAdaptor() {
838839
}
839840

840841
public CloseableHttpClient build() {
842+
843+
if (systemProperties) {
844+
EnvironmentProxyConfigurer.apply();
845+
}
846+
841847
// Create main request executor
842848
// We copy the instance fields to avoid changing them, and rename to avoid accidental use of the wrong version
843849
HttpRequestExecutor requestExecCopy = this.requestExec;
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
28+
package org.apache.hc.client5.http.config;
29+
30+
import static com.github.stefanbirkner.systemlambda.SystemLambda.withEnvironmentVariable;
31+
import static org.junit.jupiter.api.Assertions.assertEquals;
32+
import static org.junit.jupiter.api.Assertions.assertNull;
33+
34+
import java.util.HashMap;
35+
import java.util.Map;
36+
37+
import org.junit.jupiter.api.AfterEach;
38+
import org.junit.jupiter.api.Test;
39+
import org.junit.jupiter.api.condition.EnabledForJreRange;
40+
import org.junit.jupiter.api.condition.JRE;
41+
42+
/**
43+
* Verifies EnvironmentProxyConfigurer on JDKs that allow environment
44+
* mutation without --add-opens. Disabled automatically on 16+.
45+
*/
46+
@EnabledForJreRange(max = JRE.JAVA_15) // ⬅️ key line
47+
class EnvironmentProxyConfigurerTest {
48+
49+
/**
50+
* keep original values (null allowed)
51+
*/
52+
private final Map<String, String> backup = new HashMap<>();
53+
54+
private void backup(final String... keys) {
55+
for (final String k : keys) {
56+
backup.put(k, System.getProperty(k)); // value may be null
57+
}
58+
}
59+
60+
@AfterEach
61+
void restore() {
62+
backup.forEach((k, v) -> {
63+
if (v == null) {
64+
System.clearProperty(k);
65+
} else {
66+
System.setProperty(k, v);
67+
}
68+
});
69+
backup.clear();
70+
}
71+
72+
@Test
73+
void sets_http_system_properties_from_uppercase_env() throws Exception {
74+
backup("http.proxyHost", "http.proxyPort", "http.proxyUser", "http.proxyPassword");
75+
76+
withEnvironmentVariable("HTTP_PROXY", "http://user:[email protected]:8080")
77+
.execute(() -> {
78+
EnvironmentProxyConfigurer.apply();
79+
assertEquals("proxy.acme.com", System.getProperty("http.proxyHost"));
80+
assertEquals("8080", System.getProperty("http.proxyPort"));
81+
assertEquals("user", System.getProperty("http.proxyUser"));
82+
assertEquals("pass", System.getProperty("http.proxyPassword"));
83+
});
84+
}
85+
86+
@Test
87+
void does_not_overwrite_already_set_properties() throws Exception {
88+
backup("http.proxyHost");
89+
System.setProperty("http.proxyHost", "preset");
90+
91+
withEnvironmentVariable("HTTP_PROXY", "http://other:1111")
92+
.execute(() -> {
93+
EnvironmentProxyConfigurer.apply();
94+
assertEquals("preset", System.getProperty("http.proxyHost"));
95+
});
96+
}
97+
98+
@Test
99+
void translates_no_proxy_to_pipe_delimited_hosts() throws Exception {
100+
backup("http.nonProxyHosts", "https.nonProxyHosts");
101+
102+
// ensure both props are null before we invoke the bridge
103+
System.clearProperty("http.nonProxyHosts");
104+
System.clearProperty("https.nonProxyHosts");
105+
106+
withEnvironmentVariable("NO_PROXY", "localhost,127.0.0.1")
107+
.execute(() -> {
108+
EnvironmentProxyConfigurer.apply();
109+
assertEquals("localhost|127.0.0.1",
110+
System.getProperty("http.nonProxyHosts"));
111+
assertEquals("localhost|127.0.0.1",
112+
System.getProperty("https.nonProxyHosts"));
113+
});
114+
}
115+
116+
@Test
117+
void noop_when_no_relevant_env_vars() {
118+
backup("http.proxyHost");
119+
System.clearProperty("http.proxyHost");
120+
121+
EnvironmentProxyConfigurer.apply();
122+
assertNull(System.getProperty("http.proxyHost"));
123+
}
124+
}

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
<hc.animal-sniffer.signature.ignores>javax.net.ssl.SSLEngine,javax.net.ssl.SSLParameters,java.nio.ByteBuffer,java.nio.CharBuffer</hc.animal-sniffer.signature.ignores>
8181
<commons.compress.version>1.27.1</commons.compress.version>
8282
<zstd.jni.version>1.5.7-4</zstd.jni.version>
83+
<stefanbirkner.version>1.2.1</stefanbirkner.version>
8384
</properties>
8485

8586
<dependencyManagement>
@@ -216,6 +217,11 @@
216217
<artifactId>zstd-jni</artifactId>
217218
<version>${zstd.jni.version}</version>
218219
</dependency>
220+
<dependency>
221+
<groupId>com.github.stefanbirkner</groupId>
222+
<artifactId>system-lambda</artifactId>
223+
<version>${stefanbirkner.version}</version>
224+
</dependency>
219225
</dependencies>
220226
</dependencyManagement>
221227

0 commit comments

Comments
 (0)