Fixes #7091 - Add SOCKS5 support.
Spin-off of the work in #9653. Simplified the implementation, fixed a few mistakes, added more tests. Made the implementation of Socks5.Authentication more extensible (for example to implement GSSAPI authentication). Updated documentation. Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
parent
28cd6d8ada
commit
d4e9f6a520
|
@ -16,7 +16,12 @@
|
||||||
|
|
||||||
Jetty's `HttpClient` can be configured to use proxies to connect to destinations.
|
Jetty's `HttpClient` can be configured to use proxies to connect to destinations.
|
||||||
|
|
||||||
Two types of proxies are available out of the box: a HTTP proxy (provided by class `org.eclipse.jetty.client.HttpProxy`) and a SOCKS 4 proxy (provided by class `org.eclipse.jetty.client.Socks4Proxy`).
|
These types of proxies are available out of the box:
|
||||||
|
|
||||||
|
* HTTP proxy (provided by class `org.eclipse.jetty.client.HttpProxy`)
|
||||||
|
* SOCKS 4 proxy (provided by class `org.eclipse.jetty.client.Socks4Proxy`)
|
||||||
|
* xref:pg-client-http-proxy-socks5[SOCKS 5 proxy] (provided by class `org.eclipse.jetty.client.Socks5Proxy`)
|
||||||
|
|
||||||
Other implementations may be written by subclassing `ProxyConfiguration.Proxy`.
|
Other implementations may be written by subclassing `ProxyConfiguration.Proxy`.
|
||||||
|
|
||||||
The following is a typical configuration:
|
The following is a typical configuration:
|
||||||
|
@ -30,14 +35,26 @@ You specify the proxy host and proxy port, and optionally also the addresses tha
|
||||||
|
|
||||||
Configured in this way, `HttpClient` makes requests to the HTTP proxy (for plain-text HTTP requests) or establishes a tunnel via HTTP `CONNECT` (for encrypted HTTPS requests).
|
Configured in this way, `HttpClient` makes requests to the HTTP proxy (for plain-text HTTP requests) or establishes a tunnel via HTTP `CONNECT` (for encrypted HTTPS requests).
|
||||||
|
|
||||||
Proxying is supported for HTTP/1.1 and HTTP/2.
|
Proxying is supported for any version of the HTTP protocol.
|
||||||
|
|
||||||
|
[[pg-client-http-proxy-socks5]]
|
||||||
|
===== SOCKS5 Proxy Support
|
||||||
|
|
||||||
|
SOCKS 5 (defined in link:https://datatracker.ietf.org/doc/html/rfc1928[RFC 1928]) offers choices for authentication methods and supports IPv6 (things that SOCKS 4 does not support).
|
||||||
|
|
||||||
|
A typical SOCKS 5 proxy configuration with the username/password authentication method is the following:
|
||||||
|
|
||||||
|
[source,java,indent=0]
|
||||||
|
----
|
||||||
|
include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=proxySocks5]
|
||||||
|
----
|
||||||
|
|
||||||
[[pg-client-http-proxy-authentication]]
|
[[pg-client-http-proxy-authentication]]
|
||||||
===== Proxy Authentication Support
|
===== HTTP Proxy Authentication Support
|
||||||
|
|
||||||
Jetty's `HttpClient` supports proxy authentication in the same way it supports xref:pg-client-http-authentication[server authentication].
|
Jetty's `HttpClient` supports HTTP proxy authentication in the same way it supports xref:pg-client-http-authentication[server authentication].
|
||||||
|
|
||||||
In the example below, the proxy requires `BASIC` authentication, but the server requires `DIGEST` authentication, and therefore:
|
In the example below, the HTTP proxy requires `BASIC` authentication, but the server requires `DIGEST` authentication, and therefore:
|
||||||
|
|
||||||
[source,java,indent=0]
|
[source,java,indent=0]
|
||||||
----
|
----
|
||||||
|
|
|
@ -34,6 +34,8 @@ import org.eclipse.jetty.client.HttpDestination;
|
||||||
import org.eclipse.jetty.client.HttpProxy;
|
import org.eclipse.jetty.client.HttpProxy;
|
||||||
import org.eclipse.jetty.client.ProxyConfiguration;
|
import org.eclipse.jetty.client.ProxyConfiguration;
|
||||||
import org.eclipse.jetty.client.RoundRobinConnectionPool;
|
import org.eclipse.jetty.client.RoundRobinConnectionPool;
|
||||||
|
import org.eclipse.jetty.client.Socks5;
|
||||||
|
import org.eclipse.jetty.client.Socks5Proxy;
|
||||||
import org.eclipse.jetty.client.api.Authentication;
|
import org.eclipse.jetty.client.api.Authentication;
|
||||||
import org.eclipse.jetty.client.api.AuthenticationStore;
|
import org.eclipse.jetty.client.api.AuthenticationStore;
|
||||||
import org.eclipse.jetty.client.api.ContentResponse;
|
import org.eclipse.jetty.client.api.ContentResponse;
|
||||||
|
@ -665,6 +667,30 @@ public class HTTPClientDocs
|
||||||
// end::proxy[]
|
// end::proxy[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void proxySocks5() throws Exception
|
||||||
|
{
|
||||||
|
HttpClient httpClient = new HttpClient();
|
||||||
|
httpClient.start();
|
||||||
|
|
||||||
|
// tag::proxySocks5[]
|
||||||
|
Socks5Proxy proxy = new Socks5Proxy("proxyHost", 8888);
|
||||||
|
String socks5User = "jetty";
|
||||||
|
String socks5Pass = "secret";
|
||||||
|
var socks5AuthenticationFactory = new Socks5.UsernamePasswordAuthenticationFactory(socks5User, socks5Pass);
|
||||||
|
// Add the authentication method to the proxy.
|
||||||
|
proxy.putAuthenticationFactory(socks5AuthenticationFactory);
|
||||||
|
|
||||||
|
// Do not proxy requests for localhost:8080.
|
||||||
|
proxy.getExcludedAddresses().add("localhost:8080");
|
||||||
|
|
||||||
|
// Add the new proxy to the list of proxies already registered.
|
||||||
|
ProxyConfiguration proxyConfig = httpClient.getProxyConfiguration();
|
||||||
|
proxyConfig.addProxy(proxy);
|
||||||
|
|
||||||
|
ContentResponse response = httpClient.GET("http://domain.com/path");
|
||||||
|
// end::proxySocks5[]
|
||||||
|
}
|
||||||
|
|
||||||
public void proxyAuthentication() throws Exception
|
public void proxyAuthentication() throws Exception
|
||||||
{
|
{
|
||||||
HttpClient httpClient = new HttpClient();
|
HttpClient httpClient = new HttpClient();
|
||||||
|
|
|
@ -13,140 +13,226 @@
|
||||||
|
|
||||||
package org.eclipse.jetty.client;
|
package org.eclipse.jetty.client;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.ClosedChannelException;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.io.EndPoint;
|
||||||
|
import org.eclipse.jetty.util.BufferUtil;
|
||||||
|
import org.eclipse.jetty.util.Callback;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Helper class for SOCKS5 proxying.</p>
|
||||||
|
*
|
||||||
|
* @see Socks5Proxy
|
||||||
|
*/
|
||||||
public class Socks5
|
public class Socks5
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* The SOCKS protocol version: {@value}.
|
||||||
|
*/
|
||||||
|
public static final byte VERSION = 0x05;
|
||||||
|
/**
|
||||||
|
* The SOCKS5 {@code CONNECT} command used in SOCKS5 connect requests.
|
||||||
|
*/
|
||||||
|
public static final byte COMMAND_CONNECT = 0x01;
|
||||||
|
/**
|
||||||
|
* The reserved byte value: {@value}.
|
||||||
|
*/
|
||||||
|
public static final byte RESERVED = 0x00;
|
||||||
|
/**
|
||||||
|
* The address type for IPv4 used in SOCKS5 connect requests and responses.
|
||||||
|
*/
|
||||||
|
public static final byte ADDRESS_TYPE_IPV4 = 0x01;
|
||||||
|
/**
|
||||||
|
* The address type for domain names used in SOCKS5 connect requests and responses.
|
||||||
|
*/
|
||||||
|
public static final byte ADDRESS_TYPE_DOMAIN = 0x03;
|
||||||
|
/**
|
||||||
|
* The address type for IPv6 used in SOCKS5 connect requests and responses.
|
||||||
|
*/
|
||||||
|
public static final byte ADDRESS_TYPE_IPV6 = 0x04;
|
||||||
|
|
||||||
public enum RequestStage
|
private Socks5()
|
||||||
{
|
{
|
||||||
INIT,
|
|
||||||
AUTH,
|
|
||||||
CONNECTING
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ResponseStage
|
|
||||||
{
|
|
||||||
INIT,
|
|
||||||
AUTH,
|
|
||||||
CONNECTING,
|
|
||||||
CONNECTED_IPV4,
|
|
||||||
CONNECTED_DOMAIN_NAME,
|
|
||||||
CONNECTED_IPV6,
|
|
||||||
READ_REPLY_VARIABLE
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface SockConst
|
|
||||||
{
|
|
||||||
byte VER = 0x05;
|
|
||||||
byte USER_PASS_VER = 0x01;
|
|
||||||
byte RSV = 0x00;
|
|
||||||
byte SUCCEEDED = 0x00;
|
|
||||||
byte AUTH_FAILED = 0x01;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface AuthType
|
|
||||||
{
|
|
||||||
byte NO_AUTH = 0x00;
|
|
||||||
byte USER_PASS = 0x02;
|
|
||||||
byte NO_ACCEPTABLE = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Command
|
|
||||||
{
|
|
||||||
|
|
||||||
byte CONNECT = 0x01;
|
|
||||||
byte BIND = 0x02;
|
|
||||||
byte UDP = 0x03;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Reply
|
|
||||||
{
|
|
||||||
|
|
||||||
byte GENERAL = 0x01;
|
|
||||||
byte RULE_BAN = 0x02;
|
|
||||||
byte NETWORK_UNREACHABLE = 0x03;
|
|
||||||
byte HOST_UNREACHABLE = 0x04;
|
|
||||||
byte CONNECT_REFUSE = 0x05;
|
|
||||||
byte TTL_TIMEOUT = 0x06;
|
|
||||||
byte CMD_UNSUPPORTED = 0x07;
|
|
||||||
byte ATYPE_UNSUPPORTED = 0x08;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface AddrType
|
|
||||||
{
|
|
||||||
byte IPV4 = 0x01;
|
|
||||||
byte DOMAIN_NAME = 0x03;
|
|
||||||
byte IPV6 = 0x04;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>A SOCKS5 authentication method.</p>
|
||||||
|
* <p>Implementations should send and receive the bytes that
|
||||||
|
* are specific to the particular authentication method.</p>
|
||||||
|
*/
|
||||||
public interface Authentication
|
public interface Authentication
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* get supported authentication type
|
* <p>Performs the authentication send and receive bytes
|
||||||
* @see AuthType
|
* exchanges specific for this {@link Authentication}.</p>
|
||||||
* @return
|
*
|
||||||
|
* @param endPoint the {@link EndPoint} to send to and receive from the SOCKS5 server
|
||||||
|
* @param callback the callback to complete when the authentication is complete
|
||||||
*/
|
*/
|
||||||
byte getAuthType();
|
void authenticate(EndPoint endPoint, Callback callback);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* write authorize command
|
* A factory for {@link Authentication}s.
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
ByteBuffer authorize();
|
interface Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return the authentication method defined by RFC 1928
|
||||||
|
*/
|
||||||
|
byte getMethod();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return a new {@link Authentication}
|
||||||
|
*/
|
||||||
|
Authentication newAuthentication();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class NoAuthentication implements Authentication
|
/**
|
||||||
|
* <p>The implementation of the {@code NO AUTH} authentication method defined in
|
||||||
|
* <a href="https://datatracker.ietf.org/doc/html/rfc1928">RFC 1928</a>.</p>
|
||||||
|
*/
|
||||||
|
public static class NoAuthenticationFactory implements Authentication.Factory
|
||||||
{
|
{
|
||||||
|
public static final byte METHOD = 0x00;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte getAuthType()
|
public byte getMethod()
|
||||||
{
|
{
|
||||||
return AuthType.NO_AUTH;
|
return METHOD;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ByteBuffer authorize()
|
public Authentication newAuthentication()
|
||||||
{
|
{
|
||||||
throw new UnsupportedOperationException("authorize error");
|
return (endPoint, callback) -> callback.succeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class UsernamePasswordAuthentication implements Authentication
|
/**
|
||||||
|
* <p>The implementation of the {@code USERNAME/PASSWORD} authentication method defined in
|
||||||
|
* <a href="https://datatracker.ietf.org/doc/html/rfc1929">RFC 1929</a>.</p>
|
||||||
|
*/
|
||||||
|
public static class UsernamePasswordAuthenticationFactory implements Authentication.Factory
|
||||||
{
|
{
|
||||||
private String username;
|
public static final byte METHOD = 0x02;
|
||||||
private String password;
|
public static final byte VERSION = 0x01;
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(UsernamePasswordAuthenticationFactory.class);
|
||||||
|
|
||||||
public UsernamePasswordAuthentication(String username, String password)
|
private final String userName;
|
||||||
|
private final String password;
|
||||||
|
private final Charset charset;
|
||||||
|
|
||||||
|
public UsernamePasswordAuthenticationFactory(String userName, String password)
|
||||||
{
|
{
|
||||||
this.username = username;
|
this(userName, password, StandardCharsets.US_ASCII);
|
||||||
this.password = password;
|
}
|
||||||
|
|
||||||
|
public UsernamePasswordAuthenticationFactory(String userName, String password, Charset charset)
|
||||||
|
{
|
||||||
|
this.userName = Objects.requireNonNull(userName);
|
||||||
|
this.password = Objects.requireNonNull(password);
|
||||||
|
this.charset = Objects.requireNonNull(charset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte getAuthType()
|
public byte getMethod()
|
||||||
{
|
{
|
||||||
return AuthType.USER_PASS;
|
return METHOD;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ByteBuffer authorize()
|
public Authentication newAuthentication()
|
||||||
{
|
{
|
||||||
byte uLen = (byte)username.length();
|
return new UsernamePasswordAuthentication(this);
|
||||||
byte pLen = (byte)(password == null ? 0 : password.length());
|
}
|
||||||
ByteBuffer userPass = ByteBuffer.allocate(3 + uLen + pLen);
|
|
||||||
userPass.put(SockConst.USER_PASS_VER)
|
private static class UsernamePasswordAuthentication implements Authentication, Callback
|
||||||
.put(uLen)
|
{
|
||||||
.put(username.getBytes(StandardCharsets.UTF_8))
|
private final ByteBuffer byteBuffer = BufferUtil.allocate(2);
|
||||||
.put(pLen);
|
private final UsernamePasswordAuthenticationFactory factory;
|
||||||
if (password != null)
|
private EndPoint endPoint;
|
||||||
|
private Callback callback;
|
||||||
|
|
||||||
|
private UsernamePasswordAuthentication(UsernamePasswordAuthenticationFactory factory)
|
||||||
{
|
{
|
||||||
userPass.put(password.getBytes(StandardCharsets.UTF_8));
|
this.factory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void authenticate(EndPoint endPoint, Callback callback)
|
||||||
|
{
|
||||||
|
this.endPoint = endPoint;
|
||||||
|
this.callback = callback;
|
||||||
|
|
||||||
|
byte[] userNameBytes = factory.userName.getBytes(factory.charset);
|
||||||
|
byte[] passwordBytes = factory.password.getBytes(factory.charset);
|
||||||
|
ByteBuffer byteBuffer = ByteBuffer.allocate(3 + userNameBytes.length + passwordBytes.length)
|
||||||
|
.put(VERSION)
|
||||||
|
.put((byte)userNameBytes.length)
|
||||||
|
.put(userNameBytes)
|
||||||
|
.put((byte)passwordBytes.length)
|
||||||
|
.put(passwordBytes)
|
||||||
|
.flip();
|
||||||
|
endPoint.write(Callback.from(this::authenticationSent, this::failed), byteBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void authenticationSent()
|
||||||
|
{
|
||||||
|
if (LOG.isDebugEnabled())
|
||||||
|
LOG.debug("Written SOCKS5 username/password authentication request");
|
||||||
|
endPoint.fillInterested(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void succeeded()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int filled = endPoint.fill(byteBuffer);
|
||||||
|
if (filled < 0)
|
||||||
|
throw new ClosedChannelException();
|
||||||
|
if (byteBuffer.remaining() < 2)
|
||||||
|
{
|
||||||
|
endPoint.fillInterested(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (LOG.isDebugEnabled())
|
||||||
|
LOG.debug("Received SOCKS5 username/password authentication response");
|
||||||
|
byte version = byteBuffer.get();
|
||||||
|
if (version != VERSION)
|
||||||
|
throw new IOException("Unsupported username/password authentication version: " + version);
|
||||||
|
byte status = byteBuffer.get();
|
||||||
|
if (status != 0)
|
||||||
|
throw new IOException("SOCK5 username/password authentication failure");
|
||||||
|
if (LOG.isDebugEnabled())
|
||||||
|
LOG.debug("SOCKS5 username/password authentication succeeded");
|
||||||
|
callback.succeeded();
|
||||||
|
}
|
||||||
|
catch (Throwable x)
|
||||||
|
{
|
||||||
|
failed(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void failed(Throwable x)
|
||||||
|
{
|
||||||
|
callback.failed(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InvocationType getInvocationType()
|
||||||
|
{
|
||||||
|
return InvocationType.NON_BLOCKING;
|
||||||
}
|
}
|
||||||
userPass.flip();
|
|
||||||
return userPass;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,26 +13,21 @@
|
||||||
|
|
||||||
package org.eclipse.jetty.client;
|
package org.eclipse.jetty.client;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.SocketException;
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.ClosedChannelException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import org.eclipse.jetty.client.ProxyConfiguration.Proxy;
|
import org.eclipse.jetty.client.ProxyConfiguration.Proxy;
|
||||||
import org.eclipse.jetty.client.Socks5.AddrType;
|
import org.eclipse.jetty.client.Socks5.NoAuthenticationFactory;
|
||||||
import org.eclipse.jetty.client.Socks5.AuthType;
|
|
||||||
import org.eclipse.jetty.client.Socks5.Authentication;
|
|
||||||
import org.eclipse.jetty.client.Socks5.Command;
|
|
||||||
import org.eclipse.jetty.client.Socks5.NoAuthentication;
|
|
||||||
import org.eclipse.jetty.client.Socks5.Reply;
|
|
||||||
import org.eclipse.jetty.client.Socks5.RequestStage;
|
|
||||||
import org.eclipse.jetty.client.Socks5.ResponseStage;
|
|
||||||
import org.eclipse.jetty.client.Socks5.SockConst;
|
|
||||||
import org.eclipse.jetty.client.api.Connection;
|
import org.eclipse.jetty.client.api.Connection;
|
||||||
import org.eclipse.jetty.io.AbstractConnection;
|
import org.eclipse.jetty.io.AbstractConnection;
|
||||||
import org.eclipse.jetty.io.ClientConnectionFactory;
|
import org.eclipse.jetty.io.ClientConnectionFactory;
|
||||||
|
@ -41,15 +36,26 @@ import org.eclipse.jetty.io.EndPoint;
|
||||||
import org.eclipse.jetty.util.BufferUtil;
|
import org.eclipse.jetty.util.BufferUtil;
|
||||||
import org.eclipse.jetty.util.Callback;
|
import org.eclipse.jetty.util.Callback;
|
||||||
import org.eclipse.jetty.util.Promise;
|
import org.eclipse.jetty.util.Promise;
|
||||||
|
import org.eclipse.jetty.util.URIUtil;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Client-side proxy configuration for SOCKS5, defined by
|
||||||
|
* <a href="https://datatracker.ietf.org/doc/html/rfc1928">RFC 1928</a>.</p>
|
||||||
|
* <p>Multiple authentication methods are supported via
|
||||||
|
* {@link #putAuthenticationFactory(Socks5.Authentication.Factory)}.
|
||||||
|
* By default only the {@link Socks5.NoAuthenticationFactory NO AUTH}
|
||||||
|
* authentication method is configured.
|
||||||
|
* The {@link Socks5.UsernamePasswordAuthenticationFactory USERNAME/PASSWORD}
|
||||||
|
* is available to applications but must be explicitly configured and
|
||||||
|
* added.</p>
|
||||||
|
*/
|
||||||
public class Socks5Proxy extends Proxy
|
public class Socks5Proxy extends Proxy
|
||||||
{
|
{
|
||||||
private static final int MAX_AUTHRATIONS = 255;
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(Socks5Proxy.class);
|
private static final Logger LOG = LoggerFactory.getLogger(Socks5Proxy.class);
|
||||||
|
|
||||||
private LinkedHashMap<Byte, Authentication> authorizations = new LinkedHashMap<>();
|
private final Map<Byte, Socks5.Authentication.Factory> authentications = new LinkedHashMap<>();
|
||||||
|
|
||||||
public Socks5Proxy(String host, int port)
|
public Socks5Proxy(String host, int port)
|
||||||
{
|
{
|
||||||
|
@ -59,284 +65,324 @@ public class Socks5Proxy extends Proxy
|
||||||
public Socks5Proxy(Origin.Address address, boolean secure)
|
public Socks5Proxy(Origin.Address address, boolean secure)
|
||||||
{
|
{
|
||||||
super(address, secure, null, null);
|
super(address, secure, null, null);
|
||||||
// default support no_auth
|
putAuthenticationFactory(new NoAuthenticationFactory());
|
||||||
addAuthentication(new NoAuthentication());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Socks5Proxy addAuthentication(Authentication authentication)
|
|
||||||
{
|
|
||||||
if (authorizations.size() >= MAX_AUTHRATIONS)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("too much authentications");
|
|
||||||
}
|
|
||||||
authorizations.put(authentication.getAuthType(), authentication);
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* remove authorization by type
|
* <p>Provides this class with the given SOCKS5 authentication method.</p>
|
||||||
* @see AuthType
|
*
|
||||||
* @param type authorization type
|
* @param authenticationFactory the SOCKS5 authentication factory
|
||||||
|
* @return the previous authentication method of the same type, or {@code null}
|
||||||
|
* if there was none of that type already present
|
||||||
*/
|
*/
|
||||||
public Socks5Proxy removeAuthentication(byte type)
|
public Socks5.Authentication.Factory putAuthenticationFactory(Socks5.Authentication.Factory authenticationFactory)
|
||||||
{
|
{
|
||||||
authorizations.remove(type);
|
return authentications.put(authenticationFactory.getMethod(), authenticationFactory);
|
||||||
return this;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Removes the authentication of the given {@code method}.</p>
|
||||||
|
*
|
||||||
|
* @param method the authentication method to remove
|
||||||
|
*/
|
||||||
|
public Socks5.Authentication.Factory removeAuthenticationFactory(byte method)
|
||||||
|
{
|
||||||
|
return authentications.remove(method);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ClientConnectionFactory newClientConnectionFactory(ClientConnectionFactory connectionFactory)
|
public ClientConnectionFactory newClientConnectionFactory(ClientConnectionFactory connectionFactory)
|
||||||
{
|
{
|
||||||
return new Socks5ProxyClientConnectionFactory(connectionFactory, authorizations);
|
return new Socks5ProxyClientConnectionFactory(connectionFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private class Socks5ProxyClientConnectionFactory implements ClientConnectionFactory
|
||||||
public boolean matches(Origin origin)
|
|
||||||
{
|
{
|
||||||
return true;
|
private final ClientConnectionFactory connectionFactory;
|
||||||
|
|
||||||
|
private Socks5ProxyClientConnectionFactory(ClientConnectionFactory connectionFactory)
|
||||||
|
{
|
||||||
|
this.connectionFactory = connectionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map<String, Object> context)
|
||||||
|
{
|
||||||
|
HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
|
||||||
|
Executor executor = destination.getHttpClient().getExecutor();
|
||||||
|
Socks5ProxyConnection connection = new Socks5ProxyConnection(endPoint, executor, connectionFactory, context, authentications);
|
||||||
|
return customize(connection, context);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Socks5ProxyConnection extends AbstractConnection implements Callback
|
private static class Socks5ProxyConnection extends AbstractConnection implements org.eclipse.jetty.io.Connection.UpgradeFrom
|
||||||
{
|
{
|
||||||
private static final Pattern IPv4_PATTERN = Pattern.compile("(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})");
|
private static final Pattern IPv4_PATTERN = Pattern.compile("(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})");
|
||||||
|
|
||||||
|
// SOCKS5 response max length is 262 bytes.
|
||||||
|
private final ByteBuffer byteBuffer = BufferUtil.allocate(512);
|
||||||
private final ClientConnectionFactory connectionFactory;
|
private final ClientConnectionFactory connectionFactory;
|
||||||
private final Map<String, Object> context;
|
private final Map<String, Object> context;
|
||||||
|
private final Map<Byte, Socks5.Authentication.Factory> authentications;
|
||||||
|
private State state = State.HANDSHAKE;
|
||||||
|
|
||||||
private LinkedHashMap<Byte, Authentication> authorizations;
|
private Socks5ProxyConnection(EndPoint endPoint, Executor executor, ClientConnectionFactory connectionFactory, Map<String, Object> context, Map<Byte, Socks5.Authentication.Factory> authentications)
|
||||||
|
|
||||||
private Authentication selectedAuthentication;
|
|
||||||
private RequestStage requestStage = RequestStage.INIT;
|
|
||||||
private ResponseStage responseStage = null;
|
|
||||||
private int variableLen;
|
|
||||||
|
|
||||||
public Socks5ProxyConnection(EndPoint endPoint, Executor executor, ClientConnectionFactory connectionFactory, Map<String, Object> context)
|
|
||||||
{
|
{
|
||||||
super(endPoint, executor);
|
super(endPoint, executor);
|
||||||
this.connectionFactory = connectionFactory;
|
this.connectionFactory = connectionFactory;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
this.authentications = Map.copyOf(authentications);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer onUpgradeFrom()
|
||||||
|
{
|
||||||
|
return BufferUtil.copy(byteBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void onOpen()
|
public void onOpen()
|
||||||
{
|
{
|
||||||
super.onOpen();
|
super.onOpen();
|
||||||
this.writeHandshakeCmd();
|
sendHandshake();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeHandshakeCmd()
|
private void sendHandshake()
|
||||||
{
|
{
|
||||||
switch (requestStage)
|
try
|
||||||
{
|
{
|
||||||
case INIT:
|
// +-------------+--------------------+------------------+
|
||||||
// write supported authorizations
|
// | version (1) | num of methods (1) | methods (1..255) |
|
||||||
int authLen = authorizations.size();
|
// +-------------+--------------------+------------------+
|
||||||
ByteBuffer init = ByteBuffer.allocate(2 + authLen);
|
int size = authentications.size();
|
||||||
init.put(SockConst.VER).put((byte)authLen);
|
ByteBuffer byteBuffer = ByteBuffer.allocate(1 + 1 + size)
|
||||||
for (byte type : authorizations.keySet())
|
.put(Socks5.VERSION)
|
||||||
{
|
.put((byte)size);
|
||||||
init.put(type);
|
authentications.keySet().forEach(byteBuffer::put);
|
||||||
}
|
byteBuffer.flip();
|
||||||
init.flip();
|
getEndPoint().write(Callback.from(this::handshakeSent, this::fail), byteBuffer);
|
||||||
setResponseStage(ResponseStage.INIT);
|
}
|
||||||
this.getEndPoint().write(this, init);
|
catch (Throwable x)
|
||||||
break;
|
{
|
||||||
case AUTH:
|
fail(x);
|
||||||
ByteBuffer auth = selectedAuthentication.authorize();
|
|
||||||
setResponseStage(ResponseStage.AUTH);
|
|
||||||
this.getEndPoint().write(this, auth);
|
|
||||||
break;
|
|
||||||
case CONNECTING:
|
|
||||||
HttpDestination destination = (HttpDestination)this.context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
|
|
||||||
String host = destination.getHost();
|
|
||||||
short port = (short)destination.getPort();
|
|
||||||
setResponseStage(ResponseStage.CONNECTING);
|
|
||||||
Matcher matcher = IPv4_PATTERN.matcher(host);
|
|
||||||
if (matcher.matches())
|
|
||||||
{
|
|
||||||
// ip
|
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(10);
|
|
||||||
buffer.put(SockConst.VER)
|
|
||||||
.put(Command.CONNECT)
|
|
||||||
.put(SockConst.RSV)
|
|
||||||
.put(AddrType.IPV4);
|
|
||||||
for (int i = 1; i <= 4; ++i)
|
|
||||||
{
|
|
||||||
buffer.put((byte)Integer.parseInt(matcher.group(i)));
|
|
||||||
}
|
|
||||||
buffer.putShort(port);
|
|
||||||
buffer.flip();
|
|
||||||
this.getEndPoint().write(this, buffer);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// domain
|
|
||||||
byte[] hostBytes = host.getBytes(StandardCharsets.UTF_8);
|
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(7 + hostBytes.length);
|
|
||||||
buffer.put(SockConst.VER)
|
|
||||||
.put(Command.CONNECT)
|
|
||||||
.put(SockConst.RSV)
|
|
||||||
.put(AddrType.DOMAIN_NAME);
|
|
||||||
|
|
||||||
buffer.put((byte)hostBytes.length)
|
|
||||||
.put(hostBytes)
|
|
||||||
.putShort(port);
|
|
||||||
buffer.flip();
|
|
||||||
this.getEndPoint().write(this, buffer);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void succeeded()
|
private void handshakeSent()
|
||||||
{
|
{
|
||||||
if (LOG.isDebugEnabled())
|
if (LOG.isDebugEnabled())
|
||||||
{
|
|
||||||
LOG.debug("Written SOCKS5 handshake request");
|
LOG.debug("Written SOCKS5 handshake request");
|
||||||
}
|
state = State.HANDSHAKE;
|
||||||
this.fillInterested();
|
fillInterested();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void failed(Throwable x)
|
private void fail(Throwable x)
|
||||||
{
|
{
|
||||||
this.close();
|
if (LOG.isDebugEnabled())
|
||||||
|
LOG.debug("SOCKS5 failure", x);
|
||||||
|
getEndPoint().close(x);
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Promise<Connection> promise = (Promise<Connection>)this.context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
|
Promise<Connection> promise = (Promise<Connection>)this.context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
|
||||||
promise.failed(x);
|
promise.failed(x);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onIdleExpired()
|
||||||
|
{
|
||||||
|
fail(new TimeoutException("Idle timeout expired"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void onFillable()
|
public void onFillable()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Socks5Parser parser = new Socks5Parser();
|
switch (state)
|
||||||
ByteBuffer buffer;
|
|
||||||
do
|
|
||||||
{
|
{
|
||||||
buffer = BufferUtil.allocate(parser.expected());
|
case HANDSHAKE:
|
||||||
int filled = this.getEndPoint().fill(buffer);
|
receiveHandshake();
|
||||||
if (LOG.isDebugEnabled())
|
break;
|
||||||
{
|
case CONNECT:
|
||||||
LOG.debug("Read SOCKS5 connect response, {} bytes", (long)filled);
|
receiveConnect();
|
||||||
}
|
break;
|
||||||
|
default:
|
||||||
if (filled < 0)
|
throw new IllegalStateException();
|
||||||
{
|
|
||||||
throw new SocketException("SOCKS5 tunnel failed, connection closed");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filled == 0)
|
|
||||||
{
|
|
||||||
this.fillInterested();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
while (!parser.parse(buffer));
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Throwable x)
|
||||||
{
|
{
|
||||||
this.failed(e);
|
fail(x);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onSocks5Response(byte[] bs) throws SocketException
|
private void receiveHandshake() throws IOException
|
||||||
{
|
{
|
||||||
switch (responseStage)
|
// +-------------+------------+
|
||||||
|
// | version (1) | method (1) |
|
||||||
|
// +-------------+------------+
|
||||||
|
int filled = getEndPoint().fill(byteBuffer);
|
||||||
|
if (filled < 0)
|
||||||
|
throw new ClosedChannelException();
|
||||||
|
if (byteBuffer.remaining() < 2)
|
||||||
{
|
{
|
||||||
case INIT:
|
fillInterested();
|
||||||
if (bs[0] != SockConst.VER)
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LOG.isDebugEnabled())
|
||||||
|
LOG.debug("Received SOCKS5 handshake response {}", BufferUtil.toDetailString(byteBuffer));
|
||||||
|
|
||||||
|
byte version = byteBuffer.get();
|
||||||
|
if (version != Socks5.VERSION)
|
||||||
|
throw new IOException("Unsupported SOCKS5 version: " + version);
|
||||||
|
|
||||||
|
byte method = byteBuffer.get();
|
||||||
|
if (method == -1)
|
||||||
|
throw new IOException("Unacceptable SOCKS5 authentication methods");
|
||||||
|
|
||||||
|
Socks5.Authentication.Factory factory = authentications.get(method);
|
||||||
|
if (factory == null)
|
||||||
|
throw new IOException("Unknown SOCKS5 authentication method: " + method);
|
||||||
|
|
||||||
|
factory.newAuthentication().authenticate(getEndPoint(), Callback.from(this::sendConnect, this::fail));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendConnect()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// +-------------+-------------+--------------+------------------+------------------------+----------+
|
||||||
|
// | version (1) | command (1) | reserved (1) | address type (1) | address bytes (4..255) | port (2) |
|
||||||
|
// +-------------+-------------+--------------+------------------+------------------------+----------+
|
||||||
|
HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
|
||||||
|
Origin.Address address = destination.getOrigin().getAddress();
|
||||||
|
String host = address.getHost();
|
||||||
|
short port = (short)address.getPort();
|
||||||
|
|
||||||
|
ByteBuffer byteBuffer;
|
||||||
|
Matcher matcher = IPv4_PATTERN.matcher(host);
|
||||||
|
if (matcher.matches())
|
||||||
|
{
|
||||||
|
byteBuffer = ByteBuffer.allocate(10)
|
||||||
|
.put(Socks5.VERSION)
|
||||||
|
.put(Socks5.COMMAND_CONNECT)
|
||||||
|
.put(Socks5.RESERVED)
|
||||||
|
.put(Socks5.ADDRESS_TYPE_IPV4);
|
||||||
|
for (int i = 1; i <= 4; ++i)
|
||||||
{
|
{
|
||||||
throw new SocketException("SOCKS5 tunnel failed with err VER " + bs[0]);
|
byteBuffer.put(Byte.parseByte(matcher.group(i)));
|
||||||
}
|
}
|
||||||
if (bs[1] == AuthType.NO_AUTH)
|
byteBuffer.putShort(port)
|
||||||
{
|
.flip();
|
||||||
requestStage = RequestStage.CONNECTING;
|
}
|
||||||
writeHandshakeCmd();
|
else if (URIUtil.isValidHostRegisteredName(host))
|
||||||
}
|
{
|
||||||
else if (bs[1] == AuthType.NO_ACCEPTABLE)
|
byte[] bytes = host.getBytes(StandardCharsets.US_ASCII);
|
||||||
{
|
if (bytes.length > 255)
|
||||||
throw new SocketException("SOCKS : No acceptable methods");
|
throw new IOException("Invalid host name: " + host);
|
||||||
}
|
byteBuffer = ByteBuffer.allocate(7 + bytes.length)
|
||||||
else
|
.put(Socks5.VERSION)
|
||||||
{
|
.put(Socks5.COMMAND_CONNECT)
|
||||||
selectedAuthentication = authorizations.get(bs[1]);
|
.put(Socks5.RESERVED)
|
||||||
if (selectedAuthentication == null)
|
.put(Socks5.ADDRESS_TYPE_DOMAIN)
|
||||||
{
|
.put((byte)bytes.length)
|
||||||
throw new SocketException("SOCKS5 tunnel failed with unknown auth type");
|
.put(bytes)
|
||||||
}
|
.putShort(port)
|
||||||
requestStage = RequestStage.AUTH;
|
.flip();
|
||||||
writeHandshakeCmd();
|
}
|
||||||
}
|
else
|
||||||
break;
|
{
|
||||||
case AUTH:
|
// Assume IPv6.
|
||||||
if (bs[0] != SockConst.USER_PASS_VER)
|
byte[] bytes = InetAddress.getByName(host).getAddress();
|
||||||
{
|
byteBuffer = ByteBuffer.allocate(22)
|
||||||
throw new SocketException("SOCKS5 tunnel failed with err UserPassVer " + bs[0]);
|
.put(Socks5.VERSION)
|
||||||
}
|
.put(Socks5.COMMAND_CONNECT)
|
||||||
if (bs[1] != SockConst.SUCCEEDED)
|
.put(Socks5.RESERVED)
|
||||||
{
|
.put(Socks5.ADDRESS_TYPE_IPV6)
|
||||||
throw new SocketException("SOCKS : authentication failed");
|
.put(bytes)
|
||||||
}
|
.putShort(port)
|
||||||
// authorization successful
|
.flip();
|
||||||
requestStage = RequestStage.CONNECTING;
|
}
|
||||||
writeHandshakeCmd();
|
|
||||||
break;
|
getEndPoint().write(Callback.from(this::connectSent, this::fail), byteBuffer);
|
||||||
case CONNECTING:
|
}
|
||||||
if (bs[0] != SockConst.VER)
|
catch (Throwable x)
|
||||||
{
|
{
|
||||||
throw new SocketException("SOCKS5 tunnel failed with err VER " + bs[0]);
|
fail(x);
|
||||||
}
|
}
|
||||||
switch (bs[1])
|
}
|
||||||
{
|
|
||||||
case SockConst.SUCCEEDED:
|
private void connectSent()
|
||||||
switch (bs[3])
|
{
|
||||||
{
|
if (LOG.isDebugEnabled())
|
||||||
case AddrType.IPV4:
|
LOG.debug("Written SOCKS5 connect request");
|
||||||
setResponseStage(ResponseStage.CONNECTED_IPV4);
|
state = State.CONNECT;
|
||||||
fillInterested();
|
fillInterested();
|
||||||
break;
|
}
|
||||||
case AddrType.DOMAIN_NAME:
|
|
||||||
setResponseStage(ResponseStage.CONNECTED_DOMAIN_NAME);
|
private void receiveConnect() throws IOException
|
||||||
fillInterested();
|
{
|
||||||
break;
|
// +-------------+-----------+--------------+------------------+------------------------+----------+
|
||||||
case AddrType.IPV6:
|
// | version (1) | reply (1) | reserved (1) | address type (1) | address bytes (4..255) | port (2) |
|
||||||
setResponseStage(ResponseStage.CONNECTED_IPV6);
|
// +-------------+-----------+--------------+------------------+------------------------+----------+
|
||||||
fillInterested();
|
int filled = getEndPoint().fill(byteBuffer);
|
||||||
break;
|
if (filled < 0)
|
||||||
default:
|
throw new ClosedChannelException();
|
||||||
throw new SocketException("SOCKS: unknown addr type " + bs[3]);
|
if (byteBuffer.remaining() < 5)
|
||||||
}
|
{
|
||||||
break;
|
fillInterested();
|
||||||
case Reply.GENERAL:
|
return;
|
||||||
throw new SocketException("SOCKS server general failure");
|
}
|
||||||
case Reply.RULE_BAN:
|
byte addressType = byteBuffer.get(3);
|
||||||
throw new SocketException("SOCKS: Connection not allowed by ruleset");
|
int length = 6;
|
||||||
case Reply.NETWORK_UNREACHABLE:
|
if (addressType == Socks5.ADDRESS_TYPE_IPV4)
|
||||||
throw new SocketException("SOCKS: Network unreachable");
|
length += 4;
|
||||||
case Reply.HOST_UNREACHABLE:
|
else if (addressType == Socks5.ADDRESS_TYPE_DOMAIN)
|
||||||
throw new SocketException("SOCKS: Host unreachable");
|
length += 1 + (byteBuffer.get(4) & 0xFF);
|
||||||
case Reply.CONNECT_REFUSE:
|
else if (addressType == Socks5.ADDRESS_TYPE_IPV6)
|
||||||
throw new SocketException("SOCKS: Connection refused");
|
length += 16;
|
||||||
case Reply.TTL_TIMEOUT:
|
else
|
||||||
throw new SocketException("SOCKS: TTL expired");
|
throw new IOException("Invalid SOCKS5 address type: " + addressType);
|
||||||
case Reply.CMD_UNSUPPORTED:
|
if (byteBuffer.remaining() < length)
|
||||||
throw new SocketException("SOCKS: Command not supported");
|
{
|
||||||
case Reply.ATYPE_UNSUPPORTED:
|
fillInterested();
|
||||||
throw new SocketException("SOCKS: address type not supported");
|
return;
|
||||||
default:
|
}
|
||||||
throw new SocketException("SOCKS: unknown code " + bs[1]);
|
|
||||||
}
|
if (LOG.isDebugEnabled())
|
||||||
break;
|
LOG.debug("Received SOCKS5 connect response {}", BufferUtil.toDetailString(byteBuffer));
|
||||||
case CONNECTED_DOMAIN_NAME:
|
|
||||||
case CONNECTED_IPV6:
|
// We have all the SOCKS5 bytes.
|
||||||
variableLen = 2 + bs[0];
|
byte version = byteBuffer.get();
|
||||||
setResponseStage(ResponseStage.READ_REPLY_VARIABLE);
|
if (version != Socks5.VERSION)
|
||||||
fillInterested();
|
throw new IOException("Unsupported SOCKS5 version: " + version);
|
||||||
break;
|
|
||||||
case CONNECTED_IPV4:
|
byte status = byteBuffer.get();
|
||||||
case READ_REPLY_VARIABLE:
|
switch (status)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
// Consume the buffer before upgrading to the tunnel.
|
||||||
|
byteBuffer.position(length);
|
||||||
tunnel();
|
tunnel();
|
||||||
break;
|
break;
|
||||||
|
case 1:
|
||||||
|
throw new IOException("SOCKS5 general failure");
|
||||||
|
case 2:
|
||||||
|
throw new IOException("SOCKS5 connection not allowed");
|
||||||
|
case 3:
|
||||||
|
throw new IOException("SOCKS5 network unreachable");
|
||||||
|
case 4:
|
||||||
|
throw new IOException("SOCKS5 host unreachable");
|
||||||
|
case 5:
|
||||||
|
throw new IOException("SOCKS5 connection refused");
|
||||||
|
case 6:
|
||||||
|
throw new IOException("SOCKS5 timeout expired");
|
||||||
|
case 7:
|
||||||
|
throw new IOException("SOCKS5 unsupported command");
|
||||||
|
case 8:
|
||||||
|
throw new IOException("SOCKS5 unsupported address");
|
||||||
default:
|
default:
|
||||||
throw new SocketException("BAD SOCKS5 PROTOCOL");
|
throw new IOException("SOCKS5 unknown status: " + status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,110 +396,21 @@ public class Socks5Proxy extends Proxy
|
||||||
context.put(ClientConnector.REMOTE_SOCKET_ADDRESS_CONTEXT_KEY, address);
|
context.put(ClientConnector.REMOTE_SOCKET_ADDRESS_CONTEXT_KEY, address);
|
||||||
ClientConnectionFactory connectionFactory = this.connectionFactory;
|
ClientConnectionFactory connectionFactory = this.connectionFactory;
|
||||||
if (destination.isSecure())
|
if (destination.isSecure())
|
||||||
{
|
|
||||||
connectionFactory = destination.newSslClientConnectionFactory(null, connectionFactory);
|
connectionFactory = destination.newSslClientConnectionFactory(null, connectionFactory);
|
||||||
}
|
var newConnection = connectionFactory.newConnection(getEndPoint(), context);
|
||||||
org.eclipse.jetty.io.Connection newConnection = connectionFactory.newConnection(getEndPoint(), context);
|
|
||||||
getEndPoint().upgrade(newConnection);
|
getEndPoint().upgrade(newConnection);
|
||||||
if (LOG.isDebugEnabled())
|
if (LOG.isDebugEnabled())
|
||||||
{
|
|
||||||
LOG.debug("SOCKS5 tunnel established: {} over {}", this, newConnection);
|
LOG.debug("SOCKS5 tunnel established: {} over {}", this, newConnection);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Throwable x)
|
||||||
{
|
{
|
||||||
this.failed(e);
|
fail(x);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setResponseStage(ResponseStage responseStage)
|
private enum State
|
||||||
{
|
{
|
||||||
LOG.debug("set responseStage to {}", responseStage);
|
HANDSHAKE, CONNECT
|
||||||
this.responseStage = responseStage;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Socks5Parser
|
|
||||||
{
|
|
||||||
private final int expectedLength;
|
|
||||||
private final byte[] bs;
|
|
||||||
private int cursor;
|
|
||||||
|
|
||||||
private Socks5Parser()
|
|
||||||
{
|
|
||||||
switch (Socks5ProxyConnection.this.responseStage)
|
|
||||||
{
|
|
||||||
case INIT:
|
|
||||||
expectedLength = 2;
|
|
||||||
break;
|
|
||||||
case AUTH:
|
|
||||||
expectedLength = 2;
|
|
||||||
break;
|
|
||||||
case CONNECTING:
|
|
||||||
expectedLength = 4;
|
|
||||||
break;
|
|
||||||
case CONNECTED_IPV4:
|
|
||||||
expectedLength = 6;
|
|
||||||
break;
|
|
||||||
case CONNECTED_IPV6:
|
|
||||||
expectedLength = 1;
|
|
||||||
break;
|
|
||||||
case CONNECTED_DOMAIN_NAME:
|
|
||||||
expectedLength = 1;
|
|
||||||
break;
|
|
||||||
case READ_REPLY_VARIABLE:
|
|
||||||
expectedLength = Socks5ProxyConnection.this.variableLen;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
expectedLength = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
bs = new byte[expectedLength];
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean parse(ByteBuffer buffer) throws SocketException
|
|
||||||
{
|
|
||||||
while (buffer.hasRemaining())
|
|
||||||
{
|
|
||||||
byte current = buffer.get();
|
|
||||||
bs[cursor] = current;
|
|
||||||
|
|
||||||
++this.cursor;
|
|
||||||
if (this.cursor != expectedLength)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSocks5Response(bs);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int expected()
|
|
||||||
{
|
|
||||||
return expectedLength - this.cursor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Socks5ProxyClientConnectionFactory implements ClientConnectionFactory
|
|
||||||
{
|
|
||||||
private final ClientConnectionFactory connectionFactory;
|
|
||||||
private final LinkedHashMap<Byte, Authentication> authorizations;
|
|
||||||
|
|
||||||
public Socks5ProxyClientConnectionFactory(ClientConnectionFactory connectionFactory, LinkedHashMap<Byte, Authentication> authorizations)
|
|
||||||
{
|
|
||||||
this.connectionFactory = connectionFactory;
|
|
||||||
this.authorizations = authorizations;
|
|
||||||
}
|
|
||||||
|
|
||||||
public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map<String, Object> context)
|
|
||||||
{
|
|
||||||
HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
|
|
||||||
Executor executor = destination.getHttpClient().getExecutor();
|
|
||||||
Socks5ProxyConnection connection = new Socks5ProxyConnection(endPoint, executor, this.connectionFactory, context);
|
|
||||||
connection.authorizations = authorizations;
|
|
||||||
return this.customize(connection, context);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -176,6 +176,36 @@ public class HttpTester
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Request parseRequest(InputStream inputStream) throws IOException
|
||||||
|
{
|
||||||
|
return parseRequest(from(inputStream));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Request parseRequest(ReadableByteChannel channel) throws IOException
|
||||||
|
{
|
||||||
|
return parseRequest(from(channel));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Request parseRequest(Input input) throws IOException
|
||||||
|
{
|
||||||
|
Request request;
|
||||||
|
HttpParser parser = input.takeHttpParser();
|
||||||
|
if (parser != null)
|
||||||
|
{
|
||||||
|
request = (Request)parser.getHandler();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
request = newRequest();
|
||||||
|
parser = new HttpParser(request);
|
||||||
|
}
|
||||||
|
parse(input, parser);
|
||||||
|
if (request.isComplete())
|
||||||
|
return request;
|
||||||
|
input.setHttpParser(parser);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public static Response parseResponse(String response)
|
public static Response parseResponse(String response)
|
||||||
{
|
{
|
||||||
Response r = new Response();
|
Response r = new Response();
|
||||||
|
@ -230,7 +260,7 @@ public class HttpTester
|
||||||
else
|
else
|
||||||
r = (Response)parser.getHandler();
|
r = (Response)parser.getHandler();
|
||||||
|
|
||||||
parseResponse(in, parser, r);
|
parse(in, parser);
|
||||||
|
|
||||||
if (r.isComplete())
|
if (r.isComplete())
|
||||||
return r;
|
return r;
|
||||||
|
@ -246,13 +276,13 @@ public class HttpTester
|
||||||
{
|
{
|
||||||
parser = new HttpParser(response);
|
parser = new HttpParser(response);
|
||||||
}
|
}
|
||||||
parseResponse(in, parser, response);
|
parse(in, parser);
|
||||||
|
|
||||||
if (!response.isComplete())
|
if (!response.isComplete())
|
||||||
in.setHttpParser(parser);
|
in.setHttpParser(parser);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void parseResponse(Input in, HttpParser parser, Response r) throws IOException
|
private static void parse(Input in, HttpParser parser) throws IOException
|
||||||
{
|
{
|
||||||
ByteBuffer buffer = in.getBuffer();
|
ByteBuffer buffer = in.getBuffer();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue