Skip to content

Commit 77c3f08

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 77c3f08

File tree

5 files changed

+365
-0
lines changed

5 files changed

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

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

Lines changed: 36 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;
@@ -237,6 +238,9 @@ private ExecInterceptorEntry(
237238

238239
private List<Closeable> closeables;
239240

241+
private boolean applyEnvProxy;
242+
243+
240244
public static HttpClientBuilder create() {
241245
return new HttpClientBuilder();
242246
}
@@ -791,6 +795,33 @@ public final HttpClientBuilder disableDefaultUserAgent() {
791795
return this;
792796
}
793797

798+
/**
799+
* Enables transparent transfer of proxy-related environment variables
800+
* to the standard JDK system-properties <em>for this client only</em>.
801+
* <p>
802+
* When this flag is set, {@link EnvironmentProxyConfigurer#apply()}
803+
* will be invoked during {@link #build()}, copying
804+
* {@code HTTP_PROXY}, {@code HTTPS_PROXY} and {@code NO_PROXY}
805+
* (plus lower-case aliases) to their {@code http.*}/{@code https.*}
806+
* counterparts, provided those properties are not already defined.
807+
* </p>
808+
*
809+
* <p><strong>Opt-in behaviour:</strong> if you do not call
810+
* {@code useEnvironmentProxy()}, the builder leaves JVM system
811+
* properties untouched. Combine with
812+
* {@link #useSystemProperties()}&nbsp;(enabled by default via
813+
* {@code HttpClientBuilder.create()}) so the newly populated
814+
* properties are actually honoured by the client.</p>
815+
*
816+
* @return this builder, for method chaining
817+
* @since 5.6
818+
*/
819+
public HttpClientBuilder useEnvironmentProxy() {
820+
this.applyEnvProxy = true;
821+
return this;
822+
}
823+
824+
794825
/**
795826
* Sets the {@link ProxySelector} that will be used to select the proxies
796827
* to be used for establishing HTTP connections. If a non-null proxy selector is set,
@@ -838,6 +869,11 @@ protected Function<HttpContext, HttpClientContext> contextAdaptor() {
838869
}
839870

840871
public CloseableHttpClient build() {
872+
873+
if (applyEnvProxy) {
874+
EnvironmentProxyConfigurer.apply();
875+
}
876+
841877
// Create main request executor
842878
// We copy the instance fields to avoid changing them, and rename to avoid accidental use of the wrong version
843879
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+
}

0 commit comments

Comments
 (0)