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
+ * ({@code http.proxyHost}, {@code http.proxyPort}, …) 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} → {@code http.proxyHost},
72
+ * {@code http.proxyPort}, {@code http.proxyUser},
73
+ * {@code http.proxyPassword}</li>
74
+ * <li>{@code HTTPS_PROXY} → {@code https.proxyHost},
75
+ * {@code https.proxyPort}, {@code https.proxyUser},
76
+ * {@code https.proxyPassword}</li>
77
+ * <li>{@code NO_PROXY} → {@code http.nonProxyHosts},
78
+ * {@code https.nonProxyHosts} (commas are converted to the
79
+ * pipe ‘|’ 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
+ }
0 commit comments