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