Skip to content

Commit 236aac3

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 236aac3

File tree

5 files changed

+337
-0
lines changed

5 files changed

+337
-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: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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+
import org.slf4j.Logger;
36+
import org.slf4j.LoggerFactory;
37+
38+
/**
39+
* <h2>EnvironmentProxyConfigurer</h2>
40+
*
41+
* <p>
42+
* Many *nix shells, container runtimes, and CI systems advertise an
43+
* outbound HTTP proxy exclusively via the environment variables
44+
* {@code HTTP_PROXY}, {@code HTTPS_PROXY}, and {@code NO_PROXY}.
45+
* The JDK, however, expects the corresponding <em>system-properties</em>
46+
* &nbsp;({@code http.proxyHost}, {@code http.proxyPort}, &hellip;) when it
47+
* resolves a proxy through {@link java.net.ProxySelector} or performs
48+
* authentication through {@link java.net.Authenticator}.
49+
* </p>
50+
*
51+
* <p>
52+
* <strong>EnvironmentProxyConfigurer</strong> is a small, <em>opt-in</em>
53+
* utility that copies the commonly-used variables to the standard
54+
* properties once, at application start-up. It is <em>not</em> invoked
55+
* automatically by HttpClient – call it explicitly if you want this
56+
* behaviour:
57+
* </p>
58+
*
59+
* <pre>{@code
60+
* public static void main(String[] args) {
61+
* EnvironmentProxyConfigurer.apply(); // one-liner
62+
* CloseableHttpClient client = HttpClientBuilder.create()
63+
* .useSystemProperties() // default behaviour
64+
* .build();
65+
* …
66+
* }
67+
* }</pre>
68+
*
69+
* <h3>Mapping rules</h3>
70+
* <ul>
71+
* <li>{@code HTTP_PROXY} &rarr; {@code http.proxyHost},
72+
* {@code http.proxyPort}, {@code http.proxyUser},
73+
* {@code http.proxyPassword}</li>
74+
* <li>{@code HTTPS_PROXY} &rarr; {@code https.proxyHost},
75+
* {@code https.proxyPort}, {@code https.proxyUser},
76+
* {@code https.proxyPassword}</li>
77+
* <li>{@code NO_PROXY} &rarr; {@code http.nonProxyHosts},
78+
* {@code https.nonProxyHosts} &nbsp;(commas are converted to the
79+
* pipe&nbsp;‘|’ separator required by the JDK)</li>
80+
* <li>Lower-case aliases ({@code http_proxy}, {@code https_proxy},
81+
* {@code no_proxy}) are recognised as well.</li>
82+
* </ul>
83+
*
84+
* <h3>Design notes</h3>
85+
* <ul>
86+
* <li><strong>Idempotent:</strong> if a target property is already set
87+
* (for example with {@code -Dhttp.proxyHost=…}) it is left
88+
* untouched.</li>
89+
* <li><strong>Thread-safe:</strong> all reads and writes are wrapped in
90+
* {@code AccessController.doPrivileged} and synchronise only on the
91+
* global {@link System} properties map.</li>
92+
* <li><strong>No side-effects unless invoked:</strong> because the
93+
* method is <em>not</em> called from within
94+
* {@link org.apache.hc.client5.http.impl.classic.HttpClientBuilder},
95+
* applications that do not need proxy support see no change in
96+
* JVM-wide state.</li>
97+
* </ul>
98+
*
99+
* <p>
100+
* The class is {@linkplain org.apache.hc.core5.annotation.Contract
101+
* stateless} and safe to call multiple times; subsequent invocations will
102+
* be no-ops after the first successful copy.
103+
* </p>
104+
*
105+
* @since 5.6
106+
*/
107+
108+
@Contract(threading = ThreadingBehavior.STATELESS)
109+
public final class EnvironmentProxyConfigurer {
110+
111+
/**
112+
* Logger associated to this class.
113+
*/
114+
private static final Logger LOG = LoggerFactory.getLogger(EnvironmentProxyConfigurer.class);
115+
116+
private EnvironmentProxyConfigurer() {
117+
}
118+
119+
public static void apply() {
120+
configureForScheme("http", "HTTP_PROXY", "http_proxy");
121+
configureForScheme("https", "HTTPS_PROXY", "https_proxy");
122+
123+
final String noProxy = firstNonEmpty(getenv("NO_PROXY"), getenv("no_proxy"));
124+
if (noProxy != null && System.getProperty("http.nonProxyHosts") == null) {
125+
final String list = noProxy.replace(',', '|');
126+
setProperty("http.nonProxyHosts", list);
127+
128+
// only write HTTPS when it is still unset
129+
boolean httpsWritten = false;
130+
if (System.getProperty("https.nonProxyHosts") == null) {
131+
setProperty("https.nonProxyHosts", list);
132+
httpsWritten = true;
133+
}
134+
135+
if (LOG.isWarnEnabled()) {
136+
LOG.warn("Applied NO_PROXY → " + list
137+
+ (httpsWritten ? " (http & https)" : " (http only)"));
138+
}
139+
}
140+
}
141+
142+
/* -------------------------------------------------------------- */
143+
144+
private static void configureForScheme(final String scheme,
145+
final String upperEnv,
146+
final String lowerEnv) {
147+
148+
if (System.getProperty(scheme + ".proxyHost") != null) {
149+
return; // already configured via -D
150+
}
151+
String val = firstNonEmpty(getenv(upperEnv), getenv(lowerEnv));
152+
if (val == null || val.isEmpty()) {
153+
return;
154+
}
155+
if (val.indexOf("://") < 0) {
156+
val = scheme + "://" + val;
157+
}
158+
159+
final URI uri = URI.create(val);
160+
161+
if (uri.getHost() != null) {
162+
setProperty(scheme + ".proxyHost", uri.getHost());
163+
}
164+
if (uri.getPort() > 0) {
165+
setProperty(scheme + ".proxyPort", Integer.toString(uri.getPort()));
166+
}
167+
168+
final String ui = uri.getUserInfo(); // user:pass
169+
if (ui != null && !ui.isEmpty()) {
170+
final String[] parts = ui.split(":", 2);
171+
setProperty(scheme + ".proxyUser", parts[0]);
172+
if (parts.length == 2) {
173+
setProperty(scheme + ".proxyPassword", parts[1]);
174+
}
175+
}
176+
}
177+
178+
private static String firstNonEmpty(final String a, final String b) {
179+
return (a != null && !a.isEmpty()) ? a
180+
: (b != null && !b.isEmpty()) ? b
181+
: null;
182+
}
183+
184+
private static String getenv(final String key) {
185+
return AccessController.doPrivileged(
186+
(PrivilegedAction<String>) () -> System.getenv(key));
187+
}
188+
189+
private static void setProperty(final String key, final String value) {
190+
AccessController.doPrivileged(
191+
(PrivilegedAction<Void>) () -> {
192+
System.setProperty(key, value);
193+
return null;
194+
});
195+
}
196+
}

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)