Skip to content

Commit a989753

Browse files
committed
Add SOCKS proxy support, close AsyncHttpClient#1466, close AsyncHttpClient#533
Motivation: We currently only support HTTP proxies. We could support SOCKS proxies as well as Netty provides a SOCKS codec. Modifications: All credits got to @Lesiuk, see AsyncHttpClient#1466 for his Pull Request! * Introduce ProxyType and ProxyServer#getProxyType() * Add `Socks4ProxyHandler` or `Socks5ProxyHandler` to the pipeline when required Result: SOCKS4 and SOCKS5 proxies support
1 parent 6cb1030 commit a989753

File tree

11 files changed

+383
-37
lines changed

11 files changed

+383
-37
lines changed

client/pom.xml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
1+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
23
<parent>
34
<groupId>org.asynchttpclient</groupId>
45
<artifactId>async-http-client-project</artifactId>
@@ -38,6 +39,14 @@
3839
<groupId>io.netty</groupId>
3940
<artifactId>netty-handler</artifactId>
4041
</dependency>
42+
<dependency>
43+
<groupId>io.netty</groupId>
44+
<artifactId>netty-codec-socks</artifactId>
45+
</dependency>
46+
<dependency>
47+
<groupId>io.netty</groupId>
48+
<artifactId>netty-handler-proxy</artifactId>
49+
</dependency>
4150
<dependency>
4251
<groupId>io.netty</groupId>
4352
<artifactId>netty-transport-native-epoll</artifactId>

client/src/main/java/org/asynchttpclient/channel/ChannelPoolPartitioning.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
package org.asynchttpclient.channel;
1414

1515
import org.asynchttpclient.proxy.ProxyServer;
16+
import org.asynchttpclient.proxy.ProxyType;
1617
import org.asynchttpclient.uri.Uri;
1718
import org.asynchttpclient.util.HttpUtils;
1819

@@ -23,12 +24,14 @@ class ProxyPartitionKey {
2324
private final int proxyPort;
2425
private final boolean secured;
2526
private final String targetHostBaseUrl;
27+
private final ProxyType proxyType;
2628

27-
public ProxyPartitionKey(String proxyHost, int proxyPort, boolean secured, String targetHostBaseUrl) {
29+
public ProxyPartitionKey(String proxyHost, int proxyPort, boolean secured, String targetHostBaseUrl, ProxyType proxyType) {
2830
this.proxyHost = proxyHost;
2931
this.proxyPort = proxyPort;
3032
this.secured = secured;
3133
this.targetHostBaseUrl = targetHostBaseUrl;
34+
this.proxyType = proxyType;
3235
}
3336

3437
@Override
@@ -39,6 +42,7 @@ public int hashCode() {
3942
result = prime * result + proxyPort;
4043
result = prime * result + (secured ? 1231 : 1237);
4144
result = prime * result + ((targetHostBaseUrl == null) ? 0 : targetHostBaseUrl.hashCode());
45+
result = prime * result + proxyType.hashCode();
4246
return result;
4347
}
4448

@@ -65,6 +69,8 @@ public boolean equals(Object obj) {
6569
return false;
6670
} else if (!targetHostBaseUrl.equals(other.targetHostBaseUrl))
6771
return false;
72+
if (proxyType != other.proxyType)
73+
return false;
6874
return true;
6975
}
7076

@@ -75,6 +81,7 @@ public String toString() {
7581
.append(", proxyPort=").append(proxyPort)//
7682
.append(", secured=").append(secured)//
7783
.append(", targetHostBaseUrl=").append(targetHostBaseUrl)//
84+
.append(", proxyType=").append(proxyType)//
7885
.toString();
7986
}
8087
}
@@ -89,8 +96,8 @@ public Object getPartitionKey(Uri uri, String virtualHost, ProxyServer proxyServ
8996
String targetHostBaseUrl = virtualHost != null ? virtualHost : HttpUtils.getBaseUrl(uri);
9097
if (proxyServer != null) {
9198
return uri.isSecured() ? //
92-
new ProxyPartitionKey(proxyServer.getHost(), proxyServer.getSecuredPort(), true, targetHostBaseUrl)
93-
: new ProxyPartitionKey(proxyServer.getHost(), proxyServer.getPort(), false, targetHostBaseUrl);
99+
new ProxyPartitionKey(proxyServer.getHost(), proxyServer.getSecuredPort(), true, targetHostBaseUrl, proxyServer.getProxyType())
100+
: new ProxyPartitionKey(proxyServer.getHost(), proxyServer.getPort(), false, targetHostBaseUrl, proxyServer.getProxyType());
94101
} else {
95102
return targetHostBaseUrl;
96103
}

client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,7 @@
1515

1616
import io.netty.bootstrap.Bootstrap;
1717
import io.netty.buffer.ByteBufAllocator;
18-
import io.netty.channel.Channel;
19-
import io.netty.channel.ChannelFactory;
20-
import io.netty.channel.ChannelInitializer;
21-
import io.netty.channel.ChannelOption;
22-
import io.netty.channel.ChannelPipeline;
23-
import io.netty.channel.EventLoopGroup;
18+
import io.netty.channel.*;
2419
import io.netty.channel.group.ChannelGroup;
2520
import io.netty.channel.group.DefaultChannelGroup;
2621
import io.netty.channel.nio.NioEventLoopGroup;
@@ -32,12 +27,15 @@
3227
import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator;
3328
import io.netty.handler.logging.LogLevel;
3429
import io.netty.handler.logging.LoggingHandler;
30+
import io.netty.handler.proxy.Socks4ProxyHandler;
31+
import io.netty.handler.proxy.Socks5ProxyHandler;
3532
import io.netty.handler.ssl.SslHandler;
3633
import io.netty.handler.stream.ChunkedWriteHandler;
34+
import io.netty.resolver.NameResolver;
3735
import io.netty.util.Timer;
38-
import io.netty.util.concurrent.DefaultThreadFactory;
39-
import io.netty.util.concurrent.GlobalEventExecutor;
36+
import io.netty.util.concurrent.*;
4037

38+
import java.net.InetAddress;
4139
import java.net.InetSocketAddress;
4240
import java.util.Map;
4341
import java.util.Map.Entry;
@@ -75,6 +73,7 @@ public class ChannelManager {
7573
public static final String PINNED_ENTRY = "entry";
7674
public static final String HTTP_CLIENT_CODEC = "http";
7775
public static final String SSL_HANDLER = "ssl";
76+
public static final String SOCKS_HANDLER = "socks";
7877
public static final String DEFLATER_HANDLER = "deflater";
7978
public static final String INFLATER_HANDLER = "inflater";
8079
public static final String CHUNKED_WRITER_HANDLER = "chunked-writer";
@@ -385,8 +384,55 @@ public SslHandler addSslHandler(ChannelPipeline pipeline, Uri uri, String virtua
385384
return sslHandler;
386385
}
387386

388-
public Bootstrap getBootstrap(Uri uri, ProxyServer proxy) {
389-
return uri.isWebSocket() && proxy == null ? wsBootstrap : httpBootstrap;
387+
public Future<Bootstrap> getBootstrap(Uri uri, NameResolver<InetAddress> nameResolver, ProxyServer proxy) {
388+
389+
final Promise<Bootstrap> promise = ImmediateEventExecutor.INSTANCE.newPromise();
390+
391+
if (uri.isWebSocket() && proxy == null) {
392+
return promise.setSuccess(wsBootstrap);
393+
394+
} else if (proxy != null && proxy.getProxyType().isSocks()) {
395+
Bootstrap socksBootstrap = httpBootstrap.clone();
396+
ChannelHandler httpBootstrapHandler = socksBootstrap.config().handler();
397+
398+
nameResolver.resolve(proxy.getHost()).addListener((Future<InetAddress> whenProxyAddress) -> {
399+
if (whenProxyAddress.isSuccess()) {
400+
socksBootstrap.handler(new ChannelInitializer<Channel>() {
401+
@Override
402+
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
403+
httpBootstrapHandler.handlerAdded(ctx);
404+
super.handlerAdded(ctx);
405+
}
406+
407+
@Override
408+
protected void initChannel(Channel channel) throws Exception {
409+
InetSocketAddress proxyAddress = new InetSocketAddress(whenProxyAddress.get(), proxy.getPort());
410+
switch (proxy.getProxyType()) {
411+
case SOCKS_V4:
412+
channel.pipeline().addFirst(SOCKS_HANDLER, new Socks4ProxyHandler(proxyAddress));
413+
break;
414+
415+
case SOCKS_V5:
416+
channel.pipeline().addFirst(SOCKS_HANDLER, new Socks5ProxyHandler(proxyAddress));
417+
break;
418+
419+
default:
420+
throw new IllegalArgumentException("Only SOCKS4 and SOCKS5 supported at the moment.");
421+
}
422+
}
423+
});
424+
promise.setSuccess(socksBootstrap);
425+
426+
} else {
427+
promise.setFailure(whenProxyAddress.cause());
428+
}
429+
});
430+
431+
} else {
432+
promise.setSuccess(httpBootstrap);
433+
}
434+
435+
return promise;
390436
}
391437

392438
public void upgradePipelineForWebSockets(ChannelPipeline pipeline) {

client/src/main/java/org/asynchttpclient/netty/channel/NettyConnectListener.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.asynchttpclient.netty.future.StackTraceInspector;
2626
import org.asynchttpclient.netty.request.NettyRequestSender;
2727
import org.asynchttpclient.netty.timeout.TimeoutsHolder;
28+
import org.asynchttpclient.proxy.ProxyServer;
2829
import org.asynchttpclient.uri.Uri;
2930
import org.slf4j.Logger;
3031
import org.slf4j.LoggerFactory;
@@ -109,8 +110,10 @@ public void onSuccess(Channel channel, InetSocketAddress remoteAddress) {
109110

110111
timeoutsHolder.setResolvedRemoteAddress(remoteAddress);
111112

113+
ProxyServer proxyServer = future.getProxyServer();
114+
112115
// in case of proxy tunneling, we'll add the SslHandler later, after the CONNECT request
113-
if (future.getProxyServer() == null && uri.isSecured()) {
116+
if ((proxyServer == null || proxyServer.getProxyType().isSocks()) && uri.isSecured()) {
114117
SslHandler sslHandler = null;
115118
try {
116119
sslHandler = channelManager.addSslHandler(channel.pipeline(), uri, request.getVirtualHost());

client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ private String requestUri(Uri uri, ProxyServer proxyServer, boolean connect) {
237237
// proxy tunnelling, connect need host and explicit port
238238
return getAuthority(uri);
239239

240-
} else if (proxyServer != null && !uri.isSecured()) {
240+
} else if (proxyServer != null && !uri.isSecured() && proxyServer.getProxyType().isHttp()) {
241241
// proxy over HTTP, need full url
242242
return uri.toUrl();
243243

client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,10 @@ public <T> ListenableFuture<T> sendRequest(final Request request, //
106106
ProxyServer proxyServer = getProxyServer(config, request);
107107

108108
// WebSockets use connect tunneling to work with proxies
109-
if (proxyServer != null && (request.getUri().isSecured() || request.getUri().isWebSocket())
110-
&& !isConnectDone(request, future)) {
109+
if (proxyServer != null //
110+
&& (request.getUri().isSecured() || request.getUri().isWebSocket()) //
111+
&& !isConnectDone(request, future) //
112+
&& proxyServer.getProxyType().isHttp()) {
111113
// Proxy with HTTPS or WebSocket: CONNECT for sure
112114
if (future != null && future.isConnectAllowed()) {
113115
// Perform CONNECT
@@ -292,10 +294,6 @@ private <T> ListenableFuture<T> sendRequestWithNewChannel(//
292294
future.setInProxyAuth(
293295
proxyRealm != null && proxyRealm.isUsePreemptiveAuth() && proxyRealm.getScheme() != AuthScheme.NTLM);
294296

295-
// Do not throw an exception when we need an extra connection for a redirect
296-
// FIXME why? This violate the max connection per host handling, right?
297-
Bootstrap bootstrap = channelManager.getBootstrap(request.getUri(), proxy);
298-
299297
Object partitionKey = future.getPartitionKey();
300298

301299
try {
@@ -322,7 +320,16 @@ protected void onSuccess(List<InetSocketAddress> addresses) {
322320
NettyChannelConnector connector = new NettyChannelConnector(request.getLocalAddress(),
323321
addresses, asyncHandler, clientState, config);
324322
if (!future.isDone()) {
325-
connector.connect(bootstrap, connectListener);
323+
// Do not throw an exception when we need an extra connection for a redirect
324+
// FIXME why? This violate the max connection per host handling, right?
325+
channelManager.getBootstrap(request.getUri(), request.getNameResolver(), proxy)
326+
.addListener((Future<Bootstrap> whenBootstrap) -> {
327+
if (whenBootstrap.isSuccess()) {
328+
connector.connect(whenBootstrap.get(), connectListener);
329+
} else {
330+
abort(null, future, whenBootstrap.cause());
331+
}
332+
});
326333
}
327334
}
328335

@@ -343,7 +350,7 @@ private <T> Future<List<InetSocketAddress>> resolveAddresses(Request request, //
343350
Uri uri = request.getUri();
344351
final Promise<List<InetSocketAddress>> promise = ImmediateEventExecutor.INSTANCE.newPromise();
345352

346-
if (proxy != null && !proxy.isIgnoredForHost(uri.getHost())) {
353+
if (proxy != null && !proxy.isIgnoredForHost(uri.getHost()) && proxy.getProxyType().isHttp()) {
347354
int port = uri.isSecured() ? proxy.getSecuredPort() : proxy.getPort();
348355
InetSocketAddress unresolvedRemoteAddress = InetSocketAddress.createUnresolved(proxy.getHost(), port);
349356
scheduleRequestTimeout(future, unresolvedRemoteAddress);

client/src/main/java/org/asynchttpclient/proxy/ProxyServer.java

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,16 @@ public class ProxyServer {
3535
private final int securedPort;
3636
private final Realm realm;
3737
private final List<String> nonProxyHosts;
38+
private final ProxyType proxyType;
3839

39-
public ProxyServer(String host, int port, int securedPort, Realm realm, List<String> nonProxyHosts) {
40+
public ProxyServer(String host, int port, int securedPort, Realm realm, List<String> nonProxyHosts,
41+
ProxyType proxyType) {
4042
this.host = host;
4143
this.port = port;
4244
this.securedPort = securedPort;
4345
this.realm = realm;
4446
this.nonProxyHosts = nonProxyHosts;
47+
this.proxyType = proxyType;
4548
}
4649

4750
public String getHost() {
@@ -64,13 +67,24 @@ public Realm getRealm() {
6467
return realm;
6568
}
6669

70+
public ProxyType getProxyType() {
71+
return proxyType;
72+
}
73+
6774
/**
68-
* Checks whether proxy should be used according to nonProxyHosts settings of it, or we want to go directly to target host. If <code>null</code> proxy is passed in, this method
69-
* returns true -- since there is NO proxy, we should avoid to use it. Simple hostname pattern matching using "*" are supported, but only as prefixes.
75+
* Checks whether proxy should be used according to nonProxyHosts settings of
76+
* it, or we want to go directly to target host. If <code>null</code> proxy is
77+
* passed in, this method returns true -- since there is NO proxy, we should
78+
* avoid to use it. Simple hostname pattern matching using "*" are supported,
79+
* but only as prefixes.
7080
*
71-
* @param hostname the hostname
72-
* @return true if we have to ignore proxy use (obeying non-proxy hosts settings), false otherwise.
73-
* @see <a href="https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html">Networking Properties</a>
81+
* @param hostname
82+
* the hostname
83+
* @return true if we have to ignore proxy use (obeying non-proxy hosts
84+
* settings), false otherwise.
85+
* @see <a href=
86+
* "https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html">Networking
87+
* Properties</a>
7488
*/
7589
public boolean isIgnoredForHost(String hostname) {
7690
assertNotNull(hostname, "hostname");
@@ -88,7 +102,8 @@ private boolean matchNonProxyHost(String targetHost, String nonProxyHost) {
88102

89103
if (nonProxyHost.length() > 1) {
90104
if (nonProxyHost.charAt(0) == '*') {
91-
return targetHost.regionMatches(true, targetHost.length() - nonProxyHost.length() + 1, nonProxyHost, 1, nonProxyHost.length() - 1);
105+
return targetHost.regionMatches(true, targetHost.length() - nonProxyHost.length() + 1, nonProxyHost, 1,
106+
nonProxyHost.length() - 1);
92107
} else if (nonProxyHost.charAt(nonProxyHost.length() - 1) == '*')
93108
return targetHost.regionMatches(true, 0, nonProxyHost, 0, nonProxyHost.length() - 1);
94109
}
@@ -103,6 +118,7 @@ public static class Builder {
103118
private int securedPort;
104119
private Realm realm;
105120
private List<String> nonProxyHosts;
121+
private ProxyType proxyType;
106122

107123
public Builder(String host, int port) {
108124
this.host = host;
@@ -137,9 +153,16 @@ public Builder setNonProxyHosts(List<String> nonProxyHosts) {
137153
return this;
138154
}
139155

156+
public Builder setProxyType(ProxyType proxyType) {
157+
this.proxyType = proxyType;
158+
return this;
159+
}
160+
140161
public ProxyServer build() {
141-
List<String> nonProxyHosts = this.nonProxyHosts != null ? Collections.unmodifiableList(this.nonProxyHosts) : Collections.emptyList();
142-
return new ProxyServer(host, port, securedPort, realm, nonProxyHosts);
162+
List<String> nonProxyHosts = this.nonProxyHosts != null ? Collections.unmodifiableList(this.nonProxyHosts)
163+
: Collections.emptyList();
164+
ProxyType proxyType = this.proxyType != null ? this.proxyType : ProxyType.HTTP;
165+
return new ProxyServer(host, port, securedPort, realm, nonProxyHosts, proxyType);
143166
}
144167
}
145168
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (c) 2018 AsyncHttpClient Project. All rights reserved.
3+
*
4+
* This program is licensed to you under the Apache License Version 2.0,
5+
* and you may not use this file except in compliance with the Apache License Version 2.0.
6+
* You may obtain a copy of the Apache License Version 2.0 at
7+
* http://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* Unless required by applicable law or agreed to in writing,
10+
* software distributed under the Apache License Version 2.0 is distributed on an
11+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
13+
*/
14+
package org.asynchttpclient.proxy;
15+
16+
public enum ProxyType {
17+
HTTP(true), SOCKS_V4(false), SOCKS_V5(false);
18+
19+
private final boolean http;
20+
21+
private ProxyType(boolean http) {
22+
this.http = http;
23+
}
24+
25+
public boolean isHttp() {
26+
return http;
27+
}
28+
29+
public boolean isSocks() {
30+
return !isHttp();
31+
}
32+
}

0 commit comments

Comments
 (0)