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 properties</em>
46+ * ({@code http.proxyHost}, {@code http.proxyPort}, &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} (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. 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+ }
0 commit comments