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:
Simone Bordet 2023-05-03 19:37:50 +02:00
parent 28cd6d8ada
commit d4e9f6a520
No known key found for this signature in database
GPG Key ID: 1677D141BCF3584D
6 changed files with 1066 additions and 704 deletions

View File

@ -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]
---- ----

View File

@ -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();

View File

@ -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;
} }
} }
} }

View File

@ -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);
} }
} }
} }

View File

@ -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();