forked from grpc/grpc-java
/
ProxyDetectorImpl.java
318 lines (295 loc) · 11.1 KB
/
ProxyDetectorImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
/*
* Copyright 2017 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Supplier;
import io.grpc.HttpConnectProxiedSocketAddress;
import io.grpc.ProxiedSocketAddress;
import io.grpc.ProxyDetector;
import java.io.IOException;
import java.net.Authenticator;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
/**
* A utility class that detects proxies using {@link ProxySelector} and detects authentication
* credentials using {@link Authenticator}.
*
*/
class ProxyDetectorImpl implements ProxyDetector {
// To validate this code: set up a local squid proxy instance, and
// try to communicate with grpc-test.sandbox.googleapis.com:443.
// The endpoint runs an instance of TestServiceGrpc, see
// AbstractInteropTest for an example how to run a
// TestService.EmptyCall RPC.
//
// The instructions below assume Squid 3.5.23 and a recent
// version of Debian.
//
// Set the contents of /etc/squid/squid.conf to be:
// WARNING: THESE CONFIGS HAVE NOT BEEN REVIEWED FOR SECURITY, DO
// NOT USE OUTSIDE OF TESTING. COMMENT OUT THIS WARNING TO
// UNBREAK THE CONFIG FILE.
// acl SSL_ports port 443
// acl Safe_ports port 80
// acl Safe_ports port 21
// acl Safe_ports port 443
// acl Safe_ports port 70
// acl Safe_ports port 210
// acl Safe_ports port 1025-65535
// acl Safe_ports port 280
// acl Safe_ports port 488
// acl Safe_ports port 591
// acl Safe_ports port 777
// acl CONNECT method CONNECT
// http_access deny !Safe_ports
// http_access deny CONNECT !SSL_ports
// http_access allow localhost manager
// http_access deny manager
// http_access allow localhost
// http_access deny all
// http_port 3128
// coredump_dir /var/spool/squid
// refresh_pattern ^ftp: 1440 20% 10080
// refresh_pattern ^gopher: 1440 0% 1440
// refresh_pattern -i (/cgi-bin/|\?) 0 0% 0
// refresh_pattern . 0 20% 4320
//
// Restart squid:
// $ sudo /etc/init.d/squid restart
//
// To test with passwords:
//
// Run this command and follow the instructions to set up a user/pass:
// $ sudo htpasswd -c /etc/squid/passwd myuser1
//
// Make the file readable to squid:
// $ sudo chmod 644 /etc/squid/passwd
//
// Validate the username and password, you should see OK printed:
// $ /usr/lib/squid3/basic_ncsa_auth /etc/squid/passwd
// myuser1 <your password here>
//
// Add these additional lines to the beginning of squid.conf (the ordering matters):
// auth_param basic program /usr/lib/squid3/basic_ncsa_auth /etc/squid/passwd
// auth_param basic children 5
// auth_param basic realm Squid proxy-caching web server
// auth_param basic credentialsttl 2 hours
// acl ncsa_users proxy_auth REQUIRED
// http_access allow ncsa_users
//
// Restart squid:
// $ sudo /etc/init.d/squid restart
//
// In both cases, start the JVM with -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=3128 to
// configure the proxy. For passwords, use java.net.Authenticator.setDefault().
//
// Testing with curl, no password:
// $ curl -U myuser1:pass1 -x http://localhost:3128 -L grpc.io
// Testing with curl, with password:
// $ curl -U myuser1:pass1 -x http://localhost:3128 -L grpc.io
//
// It may be helpful to monitor the squid access logs:
// $ sudo tail -f /var/log/squid/access.log
private static final Logger log = Logger.getLogger(ProxyDetectorImpl.class.getName());
@SuppressWarnings("UnnecessaryAnonymousClass") // grpc-java targets Java 7 (no method references)
private static final AuthenticationProvider DEFAULT_AUTHENTICATOR = new AuthenticationProvider() {
@Override
public PasswordAuthentication requestPasswordAuthentication(
String host, InetAddress addr, int port, String protocol, String prompt, String scheme) {
URL url = null;
try {
url = new URL(protocol, host, port, "");
} catch (MalformedURLException e) {
// let url be null
log.log(
Level.WARNING,
String.format("failed to create URL for Authenticator: %s %s", protocol, host));
}
// TODO(spencerfang): consider using java.security.AccessController here
return Authenticator.requestPasswordAuthentication(
host, addr, port, protocol, prompt, scheme, url, Authenticator.RequestorType.PROXY);
}
};
@SuppressWarnings("UnnecessaryAnonymousClass") // grpc-java targets Java 7 (no method references)
private static final Supplier<ProxySelector> DEFAULT_PROXY_SELECTOR =
new Supplier<ProxySelector>() {
@Override
public ProxySelector get() {
// TODO(spencerfang): consider using java.security.AccessController here
return ProxySelector.getDefault();
}
};
/**
* @deprecated Use the standard Java proxy configuration instead with flags such as:
* -Dhttps.proxyHost=HOST -Dhttps.proxyPort=PORT
*/
@Deprecated
private static final String GRPC_PROXY_ENV_VAR = "GRPC_PROXY_EXP";
// Do not hard code a ProxySelector because the global default ProxySelector can change
private final Supplier<ProxySelector> proxySelector;
private final AuthenticationProvider authenticationProvider;
private final InetSocketAddress overrideProxyAddress;
// We want an HTTPS proxy, which operates on the entire data stream (See IETF rfc2817).
static final String PROXY_SCHEME = "https";
/**
* A proxy selector that uses the global {@link ProxySelector#getDefault()} and
* {@link ProxyDetectorImpl.AuthenticationProvider} to detect proxy parameters.
*/
public ProxyDetectorImpl() {
this(DEFAULT_PROXY_SELECTOR, DEFAULT_AUTHENTICATOR, System.getenv(GRPC_PROXY_ENV_VAR));
}
@VisibleForTesting
ProxyDetectorImpl(
Supplier<ProxySelector> proxySelector,
AuthenticationProvider authenticationProvider,
@Nullable String proxyEnvString) {
this.proxySelector = checkNotNull(proxySelector);
this.authenticationProvider = checkNotNull(authenticationProvider);
if (proxyEnvString != null) {
overrideProxyAddress = overrideProxy(proxyEnvString);
} else {
overrideProxyAddress = null;
}
}
@Nullable
@Override
public ProxiedSocketAddress proxyFor(SocketAddress targetServerAddress) throws IOException {
if (!(targetServerAddress instanceof InetSocketAddress)) {
return null;
}
if (overrideProxyAddress != null) {
return HttpConnectProxiedSocketAddress.newBuilder()
.setProxyAddress(overrideProxyAddress)
.setTargetAddress((InetSocketAddress) targetServerAddress)
.build();
}
return detectProxy((InetSocketAddress) targetServerAddress);
}
private ProxiedSocketAddress detectProxy(InetSocketAddress targetAddr) throws IOException {
URI uri;
String host;
try {
host = GrpcUtil.getHost(targetAddr);
} catch (Throwable t) {
// Workaround for Android API levels < 19 if getHostName causes a NetworkOnMainThreadException
log.log(Level.WARNING, "Failed to get host for proxy lookup, proceeding without proxy", t);
return null;
}
try {
uri =
new URI(
PROXY_SCHEME,
null, /* userInfo */
host,
targetAddr.getPort(),
null, /* path */
null, /* query */
null /* fragment */);
} catch (final URISyntaxException e) {
log.log(
Level.WARNING,
"Failed to construct URI for proxy lookup, proceeding without proxy",
e);
return null;
}
ProxySelector proxySelector = this.proxySelector.get();
if (proxySelector == null) {
log.log(Level.FINE, "proxy selector is null, so continuing without proxy lookup");
return null;
}
List<Proxy> proxies = proxySelector.select(uri);
if (proxies.size() > 1) {
log.warning("More than 1 proxy detected, gRPC will select the first one");
}
Proxy proxy = proxies.get(0);
if (proxy.type() == Proxy.Type.DIRECT) {
return null;
}
InetSocketAddress proxyAddr = (InetSocketAddress) proxy.address();
// The prompt string should be the realm as returned by the server.
// We don't have it because we are avoiding the full handshake.
String promptString = "";
PasswordAuthentication auth = authenticationProvider.requestPasswordAuthentication(
GrpcUtil.getHost(proxyAddr),
proxyAddr.getAddress(),
proxyAddr.getPort(),
PROXY_SCHEME,
promptString,
null);
final InetSocketAddress resolvedProxyAddr;
if (proxyAddr.isUnresolved()) {
InetAddress resolvedAddress = InetAddress.getByName(proxyAddr.getHostName());
resolvedProxyAddr = new InetSocketAddress(resolvedAddress, proxyAddr.getPort());
} else {
resolvedProxyAddr = proxyAddr;
}
HttpConnectProxiedSocketAddress.Builder builder =
HttpConnectProxiedSocketAddress.newBuilder()
.setTargetAddress(targetAddr)
.setProxyAddress(resolvedProxyAddr);
if (auth == null) {
return builder.build();
}
return builder
.setUsername(auth.getUserName())
.setPassword(auth.getPassword() == null ? null : new String(auth.getPassword()))
.build();
}
/**
* GRPC_PROXY_EXP is deprecated but let's maintain compatibility for now.
*/
private static InetSocketAddress overrideProxy(String proxyHostPort) {
if (proxyHostPort == null) {
return null;
}
String[] parts = proxyHostPort.split(":", 2);
int port = 80;
if (parts.length > 1) {
port = Integer.parseInt(parts[1]);
}
log.warning(
"Detected GRPC_PROXY_EXP and will honor it, but this feature will "
+ "be removed in a future release. Use the JVM flags "
+ "\"-Dhttps.proxyHost=HOST -Dhttps.proxyPort=PORT\" to set the https proxy for "
+ "this JVM.");
return new InetSocketAddress(parts[0], port);
}
/**
* This interface makes unit testing easier by avoiding direct calls to static methods.
*/
interface AuthenticationProvider {
PasswordAuthentication requestPasswordAuthentication(
String host,
InetAddress addr,
int port,
String protocol,
String prompt,
String scheme);
}
}