diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-proxy.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-proxy.adoc
index 998a9dd14a0..45d1e360fa2 100644
--- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-proxy.adoc
+++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-proxy.adoc
@@ -16,7 +16,12 @@
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`.
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).
-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]]
-===== 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]
----
diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java
index 65d7717d75e..2954db45189 100644
--- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java
+++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java
@@ -34,6 +34,8 @@ import org.eclipse.jetty.client.HttpDestination;
import org.eclipse.jetty.client.HttpProxy;
import org.eclipse.jetty.client.ProxyConfiguration;
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.AuthenticationStore;
import org.eclipse.jetty.client.api.ContentResponse;
@@ -665,6 +667,30 @@ public class HTTPClientDocs
// 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
{
HttpClient httpClient = new HttpClient();
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/Socks5.java b/jetty-client/src/main/java/org/eclipse/jetty/client/Socks5.java
index 668735e14e4..1d71f2e098a 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/Socks5.java
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/Socks5.java
@@ -13,140 +13,226 @@
package org.eclipse.jetty.client;
+import java.io.IOException;
import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.nio.charset.Charset;
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;
+
+/**
+ *
Helper class for SOCKS5 proxying.
+ *
+ * @see Socks5Proxy
+ */
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;
}
+ /**
+ * A SOCKS5 authentication method.
+ * Implementations should send and receive the bytes that
+ * are specific to the particular authentication method.
+ */
public interface Authentication
{
/**
- * get supported authentication type
- * @see AuthType
- * @return
+ * Performs the authentication send and receive bytes
+ * exchanges specific for this {@link Authentication}.
+ *
+ * @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
- * @return
+ * A factory for {@link Authentication}s.
*/
- 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
+ /**
+ * The implementation of the {@code NO AUTH} authentication method defined in
+ * RFC 1928.
+ */
+ public static class NoAuthenticationFactory implements Authentication.Factory
{
+ public static final byte METHOD = 0x00;
@Override
- public byte getAuthType()
+ public byte getMethod()
{
- return AuthType.NO_AUTH;
+ return METHOD;
}
@Override
- public ByteBuffer authorize()
+ public Authentication newAuthentication()
{
- throw new UnsupportedOperationException("authorize error");
+ return (endPoint, callback) -> callback.succeeded();
}
-
}
- public static class UsernamePasswordAuthentication implements Authentication
+ /**
+ * The implementation of the {@code USERNAME/PASSWORD} authentication method defined in
+ * RFC 1929.
+ */
+ public static class UsernamePasswordAuthenticationFactory implements Authentication.Factory
{
- private String username;
- private String password;
+ public static final byte METHOD = 0x02;
+ 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.password = password;
+ this(userName, password, StandardCharsets.US_ASCII);
+ }
+
+ public UsernamePasswordAuthenticationFactory(String userName, String password, Charset charset)
+ {
+ this.userName = Objects.requireNonNull(userName);
+ this.password = Objects.requireNonNull(password);
+ this.charset = Objects.requireNonNull(charset);
}
@Override
- public byte getAuthType()
+ public byte getMethod()
{
- return AuthType.USER_PASS;
+ return METHOD;
}
@Override
- public ByteBuffer authorize()
+ public Authentication newAuthentication()
{
- byte uLen = (byte)username.length();
- byte pLen = (byte)(password == null ? 0 : password.length());
- ByteBuffer userPass = ByteBuffer.allocate(3 + uLen + pLen);
- userPass.put(SockConst.USER_PASS_VER)
- .put(uLen)
- .put(username.getBytes(StandardCharsets.UTF_8))
- .put(pLen);
- if (password != null)
+ return new UsernamePasswordAuthentication(this);
+ }
+
+ private static class UsernamePasswordAuthentication implements Authentication, Callback
+ {
+ private final ByteBuffer byteBuffer = BufferUtil.allocate(2);
+ private final UsernamePasswordAuthenticationFactory factory;
+ 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;
}
}
}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/Socks5Proxy.java b/jetty-client/src/main/java/org/eclipse/jetty/client/Socks5Proxy.java
index 700d1accb32..d70791e20ee 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/Socks5Proxy.java
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/Socks5Proxy.java
@@ -13,26 +13,21 @@
package org.eclipse.jetty.client;
+import java.io.IOException;
+import java.net.InetAddress;
import java.net.InetSocketAddress;
-import java.net.SocketException;
import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Executor;
+import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jetty.client.ProxyConfiguration.Proxy;
-import org.eclipse.jetty.client.Socks5.AddrType;
-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.Socks5.NoAuthenticationFactory;
import org.eclipse.jetty.client.api.Connection;
import org.eclipse.jetty.io.AbstractConnection;
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.Callback;
import org.eclipse.jetty.util.Promise;
+import org.eclipse.jetty.util.URIUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Client-side proxy configuration for SOCKS5, defined by
+ * RFC 1928.
+ * 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.
+ */
public class Socks5Proxy extends Proxy
{
- private static final int MAX_AUTHRATIONS = 255;
private static final Logger LOG = LoggerFactory.getLogger(Socks5Proxy.class);
- private LinkedHashMap authorizations = new LinkedHashMap<>();
+ private final Map authentications = new LinkedHashMap<>();
public Socks5Proxy(String host, int port)
{
@@ -59,290 +65,330 @@ public class Socks5Proxy extends Proxy
public Socks5Proxy(Origin.Address address, boolean secure)
{
super(address, secure, null, null);
- // default support no_auth
- 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;
+ putAuthenticationFactory(new NoAuthenticationFactory());
}
/**
- * remove authorization by type
- * @see AuthType
- * @param type authorization type
+ * Provides this class with the given SOCKS5 authentication method.
+ *
+ * @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 this;
+ return authentications.put(authenticationFactory.getMethod(), authenticationFactory);
+ }
+
+ /**
+ * Removes the authentication of the given {@code method}.
+ *
+ * @param method the authentication method to remove
+ */
+ public Socks5.Authentication.Factory removeAuthenticationFactory(byte method)
+ {
+ return authentications.remove(method);
}
@Override
public ClientConnectionFactory newClientConnectionFactory(ClientConnectionFactory connectionFactory)
{
- return new Socks5ProxyClientConnectionFactory(connectionFactory, authorizations);
+ return new Socks5ProxyClientConnectionFactory(connectionFactory);
}
- @Override
- public boolean matches(Origin origin)
+ private class Socks5ProxyClientConnectionFactory implements ClientConnectionFactory
{
- return true;
+ private final ClientConnectionFactory connectionFactory;
+
+ private Socks5ProxyClientConnectionFactory(ClientConnectionFactory connectionFactory)
+ {
+ this.connectionFactory = connectionFactory;
+ }
+
+ public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map 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})");
+
+ // SOCKS5 response max length is 262 bytes.
+ private final ByteBuffer byteBuffer = BufferUtil.allocate(512);
private final ClientConnectionFactory connectionFactory;
private final Map context;
+ private final Map authentications;
+ private State state = State.HANDSHAKE;
- private LinkedHashMap authorizations;
-
- private Authentication selectedAuthentication;
- private RequestStage requestStage = RequestStage.INIT;
- private ResponseStage responseStage = null;
- private int variableLen;
-
- public Socks5ProxyConnection(EndPoint endPoint, Executor executor, ClientConnectionFactory connectionFactory, Map context)
+ private Socks5ProxyConnection(EndPoint endPoint, Executor executor, ClientConnectionFactory connectionFactory, Map context, Map authentications)
{
super(endPoint, executor);
this.connectionFactory = connectionFactory;
this.context = context;
+ this.authentications = Map.copyOf(authentications);
}
- public void onOpen()
+ @Override
+ public ByteBuffer onUpgradeFrom()
+ {
+ return BufferUtil.copy(byteBuffer);
+ }
+
+ @Override
+ public void onOpen()
{
super.onOpen();
- this.writeHandshakeCmd();
+ sendHandshake();
}
- private void writeHandshakeCmd()
+ private void sendHandshake()
{
- switch (requestStage)
+ try
{
- case INIT:
- // write supported authorizations
- int authLen = authorizations.size();
- ByteBuffer init = ByteBuffer.allocate(2 + authLen);
- init.put(SockConst.VER).put((byte)authLen);
- for (byte type : authorizations.keySet())
- {
- init.put(type);
- }
- init.flip();
- setResponseStage(ResponseStage.INIT);
- this.getEndPoint().write(this, init);
- break;
- case AUTH:
- 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;
+ // +-------------+--------------------+------------------+
+ // | version (1) | num of methods (1) | methods (1..255) |
+ // +-------------+--------------------+------------------+
+ int size = authentications.size();
+ ByteBuffer byteBuffer = ByteBuffer.allocate(1 + 1 + size)
+ .put(Socks5.VERSION)
+ .put((byte)size);
+ authentications.keySet().forEach(byteBuffer::put);
+ byteBuffer.flip();
+ getEndPoint().write(Callback.from(this::handshakeSent, this::fail), byteBuffer);
+ }
+ catch (Throwable x)
+ {
+ fail(x);
}
}
- public void succeeded()
+ private void handshakeSent()
{
- if (LOG.isDebugEnabled())
- {
+ if (LOG.isDebugEnabled())
LOG.debug("Written SOCKS5 handshake request");
- }
- this.fillInterested();
+ state = State.HANDSHAKE;
+ 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")
Promise promise = (Promise)this.context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
promise.failed(x);
}
- public void onFillable()
+ @Override
+ public boolean onIdleExpired()
{
- try
+ fail(new TimeoutException("Idle timeout expired"));
+ return false;
+ }
+
+ @Override
+ public void onFillable()
+ {
+ try
{
- Socks5Parser parser = new Socks5Parser();
- ByteBuffer buffer;
- do
+ switch (state)
{
- buffer = BufferUtil.allocate(parser.expected());
- int filled = this.getEndPoint().fill(buffer);
- if (LOG.isDebugEnabled())
- {
- LOG.debug("Read SOCKS5 connect response, {} bytes", (long)filled);
- }
-
- if (filled < 0)
- {
- throw new SocketException("SOCKS5 tunnel failed, connection closed");
- }
-
- if (filled == 0)
- {
- this.fillInterested();
- return;
- }
- }
- while (!parser.parse(buffer));
- }
- catch (Exception e)
+ case HANDSHAKE:
+ receiveHandshake();
+ break;
+ case CONNECT:
+ receiveConnect();
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ 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:
- if (bs[0] != SockConst.VER)
+ fillInterested();
+ 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)
- {
- requestStage = RequestStage.CONNECTING;
- writeHandshakeCmd();
- }
- else if (bs[1] == AuthType.NO_ACCEPTABLE)
- {
- throw new SocketException("SOCKS : No acceptable methods");
- }
- else
- {
- selectedAuthentication = authorizations.get(bs[1]);
- if (selectedAuthentication == null)
- {
- throw new SocketException("SOCKS5 tunnel failed with unknown auth type");
- }
- requestStage = RequestStage.AUTH;
- writeHandshakeCmd();
- }
- break;
- case AUTH:
- if (bs[0] != SockConst.USER_PASS_VER)
- {
- throw new SocketException("SOCKS5 tunnel failed with err UserPassVer " + bs[0]);
- }
- if (bs[1] != SockConst.SUCCEEDED)
- {
- throw new SocketException("SOCKS : authentication failed");
- }
- // authorization successful
- requestStage = RequestStage.CONNECTING;
- writeHandshakeCmd();
- break;
- case CONNECTING:
- if (bs[0] != SockConst.VER)
- {
- throw new SocketException("SOCKS5 tunnel failed with err VER " + bs[0]);
- }
- switch (bs[1])
- {
- case SockConst.SUCCEEDED:
- switch (bs[3])
- {
- case AddrType.IPV4:
- setResponseStage(ResponseStage.CONNECTED_IPV4);
- fillInterested();
- break;
- case AddrType.DOMAIN_NAME:
- setResponseStage(ResponseStage.CONNECTED_DOMAIN_NAME);
- fillInterested();
- break;
- case AddrType.IPV6:
- setResponseStage(ResponseStage.CONNECTED_IPV6);
- fillInterested();
- break;
- default:
- throw new SocketException("SOCKS: unknown addr type " + bs[3]);
- }
- break;
- case Reply.GENERAL:
- throw new SocketException("SOCKS server general failure");
- case Reply.RULE_BAN:
- throw new SocketException("SOCKS: Connection not allowed by ruleset");
- case Reply.NETWORK_UNREACHABLE:
- throw new SocketException("SOCKS: Network unreachable");
- case Reply.HOST_UNREACHABLE:
- throw new SocketException("SOCKS: Host unreachable");
- case Reply.CONNECT_REFUSE:
- throw new SocketException("SOCKS: Connection refused");
- case Reply.TTL_TIMEOUT:
- throw new SocketException("SOCKS: TTL expired");
- case Reply.CMD_UNSUPPORTED:
- throw new SocketException("SOCKS: Command not supported");
- case Reply.ATYPE_UNSUPPORTED:
- throw new SocketException("SOCKS: address type not supported");
- default:
- throw new SocketException("SOCKS: unknown code " + bs[1]);
- }
- break;
- case CONNECTED_DOMAIN_NAME:
- case CONNECTED_IPV6:
- variableLen = 2 + bs[0];
- setResponseStage(ResponseStage.READ_REPLY_VARIABLE);
- fillInterested();
- break;
- case CONNECTED_IPV4:
- case READ_REPLY_VARIABLE:
+ byteBuffer.putShort(port)
+ .flip();
+ }
+ else if (URIUtil.isValidHostRegisteredName(host))
+ {
+ byte[] bytes = host.getBytes(StandardCharsets.US_ASCII);
+ if (bytes.length > 255)
+ throw new IOException("Invalid host name: " + host);
+ byteBuffer = ByteBuffer.allocate(7 + bytes.length)
+ .put(Socks5.VERSION)
+ .put(Socks5.COMMAND_CONNECT)
+ .put(Socks5.RESERVED)
+ .put(Socks5.ADDRESS_TYPE_DOMAIN)
+ .put((byte)bytes.length)
+ .put(bytes)
+ .putShort(port)
+ .flip();
+ }
+ else
+ {
+ // Assume IPv6.
+ byte[] bytes = InetAddress.getByName(host).getAddress();
+ byteBuffer = ByteBuffer.allocate(22)
+ .put(Socks5.VERSION)
+ .put(Socks5.COMMAND_CONNECT)
+ .put(Socks5.RESERVED)
+ .put(Socks5.ADDRESS_TYPE_IPV6)
+ .put(bytes)
+ .putShort(port)
+ .flip();
+ }
+
+ getEndPoint().write(Callback.from(this::connectSent, this::fail), byteBuffer);
+ }
+ catch (Throwable x)
+ {
+ fail(x);
+ }
+ }
+
+ private void connectSent()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Written SOCKS5 connect request");
+ state = State.CONNECT;
+ fillInterested();
+ }
+
+ private void receiveConnect() throws IOException
+ {
+ // +-------------+-----------+--------------+------------------+------------------------+----------+
+ // | version (1) | reply (1) | reserved (1) | address type (1) | address bytes (4..255) | port (2) |
+ // +-------------+-----------+--------------+------------------+------------------------+----------+
+ int filled = getEndPoint().fill(byteBuffer);
+ if (filled < 0)
+ throw new ClosedChannelException();
+ if (byteBuffer.remaining() < 5)
+ {
+ fillInterested();
+ return;
+ }
+ byte addressType = byteBuffer.get(3);
+ int length = 6;
+ if (addressType == Socks5.ADDRESS_TYPE_IPV4)
+ length += 4;
+ else if (addressType == Socks5.ADDRESS_TYPE_DOMAIN)
+ length += 1 + (byteBuffer.get(4) & 0xFF);
+ else if (addressType == Socks5.ADDRESS_TYPE_IPV6)
+ length += 16;
+ else
+ throw new IOException("Invalid SOCKS5 address type: " + addressType);
+ if (byteBuffer.remaining() < length)
+ {
+ fillInterested();
+ return;
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("Received SOCKS5 connect response {}", BufferUtil.toDetailString(byteBuffer));
+
+ // We have all the SOCKS5 bytes.
+ byte version = byteBuffer.get();
+ if (version != Socks5.VERSION)
+ throw new IOException("Unsupported SOCKS5 version: " + version);
+
+ byte status = byteBuffer.get();
+ switch (status)
+ {
+ case 0:
+ // Consume the buffer before upgrading to the tunnel.
+ byteBuffer.position(length);
tunnel();
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:
- throw new SocketException("BAD SOCKS5 PROTOCOL");
+ throw new IOException("SOCKS5 unknown status: " + status);
}
}
- private void tunnel()
+ private void tunnel()
{
- try
+ try
{
HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
// Don't want to do DNS resolution here.
@@ -350,110 +396,21 @@ public class Socks5Proxy extends Proxy
context.put(ClientConnector.REMOTE_SOCKET_ADDRESS_CONTEXT_KEY, address);
ClientConnectionFactory connectionFactory = this.connectionFactory;
if (destination.isSecure())
- {
connectionFactory = destination.newSslClientConnectionFactory(null, connectionFactory);
- }
- org.eclipse.jetty.io.Connection newConnection = connectionFactory.newConnection(getEndPoint(), context);
+ var newConnection = connectionFactory.newConnection(getEndPoint(), context);
getEndPoint().upgrade(newConnection);
if (LOG.isDebugEnabled())
- {
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);
- 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 authorizations;
-
- public Socks5ProxyClientConnectionFactory(ClientConnectionFactory connectionFactory, LinkedHashMap authorizations)
- {
- this.connectionFactory = connectionFactory;
- this.authorizations = authorizations;
- }
-
- public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map 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);
+ HANDSHAKE, CONNECT
}
}
}
diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/Socks5ProxyTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/Socks5ProxyTest.java
index 8a27ba60809..ef955e2e05c 100644
--- a/jetty-client/src/test/java/org/eclipse/jetty/client/Socks5ProxyTest.java
+++ b/jetty-client/src/test/java/org/eclipse/jetty/client/Socks5ProxyTest.java
@@ -13,11 +13,12 @@
package org.eclipse.jetty.client;
-import java.io.InputStream;
+import java.io.IOException;
import java.io.OutputStream;
+import java.net.InetAddress;
import java.net.InetSocketAddress;
-import java.net.SocketException;
import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
@@ -28,15 +29,12 @@ import java.util.concurrent.TimeoutException;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
-import org.eclipse.jetty.client.Socks5.AddrType;
-import org.eclipse.jetty.client.Socks5.AuthType;
-import org.eclipse.jetty.client.Socks5.Command;
-import org.eclipse.jetty.client.Socks5.SockConst;
-import org.eclipse.jetty.client.Socks5.UsernamePasswordAuthentication;
+import org.eclipse.jetty.client.Socks5.UsernamePasswordAuthenticationFactory;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
import org.eclipse.jetty.client.util.FutureResponseListener;
import org.eclipse.jetty.http.HttpScheme;
+import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
@@ -47,10 +45,11 @@ import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
-public class Socks5ProxyTest
+public class Socks5ProxyTest
{
private ServerSocketChannel proxy;
private HttpClient client;
@@ -65,9 +64,7 @@ public class Socks5ProxyTest
QueuedThreadPool clientThreads = new QueuedThreadPool();
clientThreads.setName("client");
connector.setExecutor(clientThreads);
- connector.setSslContextFactory(new SslContextFactory.Client());
client = new HttpClient(new HttpClientTransportOverHTTP(connector));
- client.setExecutor(clientThreads);
client.start();
}
@@ -109,45 +106,49 @@ public class Socks5ProxyTest
int initLen = 3;
ByteBuffer buffer = ByteBuffer.allocate(initLen);
int read = channel.read(buffer);
+ buffer.flip();
assertEquals(initLen, read);
- assertEquals(SockConst.VER, buffer.get(0) & 0xFF);
- assertEquals(1, buffer.get(1) & 0xFF);
- assertEquals(AuthType.NO_AUTH, buffer.get(2) & 0xFF);
+ assertEquals(Socks5.VERSION, buffer.get());
+ assertEquals(1, buffer.get());
+ byte authenticationMethod = Socks5.NoAuthenticationFactory.METHOD;
+ assertEquals(authenticationMethod, buffer.get());
- // write acceptable methods
- channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, AuthType.NO_AUTH}));
+ // Write handshake response.
+ channel.write(ByteBuffer.wrap(new byte[]{Socks5.VERSION, authenticationMethod}));
- // read addr
+ // Read server address.
int addrLen = 10;
buffer = ByteBuffer.allocate(addrLen);
read = channel.read(buffer);
- assertEquals(addrLen, read);
- assertEquals(SockConst.VER, buffer.get(0) & 0xFF);
- assertEquals(Command.CONNECT, buffer.get(1) & 0xFF);
- assertEquals(SockConst.RSV, buffer.get(2) & 0xFF);
- assertEquals(AddrType.IPV4, buffer.get(3) & 0xFF);
- assertEquals(ip1, buffer.get(4) & 0xFF);
- assertEquals(ip2, buffer.get(5) & 0xFF);
- assertEquals(ip3, buffer.get(6) & 0xFF);
- assertEquals(ip4, buffer.get(7) & 0xFF);
- assertEquals(serverPort, buffer.getShort(8) & 0xFFFF);
-
- // Socks5 connect response.
- channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, SockConst.SUCCEEDED, SockConst.RSV, AddrType.IPV4, 0, 0, 0, 0, 0, 0}));
-
- buffer = ByteBuffer.allocate(method.length() + 1 + path.length());
- read = channel.read(buffer);
- assertEquals(buffer.capacity(), read);
buffer.flip();
- assertEquals(method + " " + path, StandardCharsets.UTF_8.decode(buffer).toString());
+ assertEquals(addrLen, read);
+ assertEquals(Socks5.VERSION, buffer.get());
+ assertEquals(Socks5.COMMAND_CONNECT, buffer.get());
+ assertEquals(Socks5.RESERVED, buffer.get());
+ assertEquals(Socks5.ADDRESS_TYPE_IPV4, buffer.get());
+ assertEquals(ip1, buffer.get());
+ assertEquals(ip2, buffer.get());
+ assertEquals(ip3, buffer.get());
+ assertEquals(ip4, buffer.get());
+ assertEquals(serverPort, buffer.getShort() & 0xFFFF);
- // http response
- String response =
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 0\r\n" +
- "Connection: close\r\n" +
- "\r\n";
- channel.write(ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8)));
+ // Write connect response.
+ channel.write(ByteBuffer.wrap(new byte[]{
+ Socks5.VERSION, 0, Socks5.RESERVED, Socks5.ADDRESS_TYPE_IPV4, 127, 0, 0, 2, 13, 13
+ }));
+
+ // Parse the HTTP request.
+ HttpTester.Request request = HttpTester.parseRequest(channel);
+ assertNotNull(request);
+ assertEquals(method, request.getMethod());
+ assertEquals(path, request.getUri());
+
+ // Write the HTTP response.
+ String response = "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 0\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+ channel.write(ByteBuffer.wrap(response.getBytes(StandardCharsets.US_ASCII)));
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@@ -180,46 +181,51 @@ public class Socks5ProxyTest
int initLen = 3;
ByteBuffer buffer = ByteBuffer.allocate(initLen);
int read = channel.read(buffer);
+ buffer.flip();
assertEquals(initLen, read);
- assertEquals(SockConst.VER, buffer.get(0) & 0xFF);
- assertEquals(1, buffer.get(1) & 0xFF);
- assertEquals(AuthType.NO_AUTH, buffer.get(2) & 0xFF);
+ assertEquals(Socks5.VERSION, buffer.get());
+ assertEquals(1, buffer.get());
+ byte authenticationMethod = Socks5.NoAuthenticationFactory.METHOD;
+ assertEquals(authenticationMethod, buffer.get());
- // write acceptable methods
- channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, AuthType.NO_AUTH}));
+ // Write handshake response.
+ channel.write(ByteBuffer.wrap(new byte[]{Socks5.VERSION, authenticationMethod}));
- // read addr
+ // Read server address.
int addrLen = 7 + serverHost.length();
buffer = ByteBuffer.allocate(addrLen);
read = channel.read(buffer);
+ buffer.flip();
assertEquals(addrLen, read);
- buffer.flip();
- byte[] bs = buffer.array();
- assertEquals(SockConst.VER, bs[0] & 0xFF);
- assertEquals(Command.CONNECT, bs[1] & 0xFF);
- assertEquals(SockConst.RSV, bs[2] & 0xFF);
- assertEquals(AddrType.DOMAIN_NAME, bs[3] & 0xFF);
- int hLen = bs[4] & 0xFF;
- assertEquals(serverHost.length(), hLen);
- assertEquals(serverHost, new String(bs, 5, hLen, StandardCharsets.UTF_8));
- assertEquals(serverPort, buffer.getShort(5 + hLen) & 0xFFFF);
+ assertEquals(Socks5.VERSION, buffer.get());
+ assertEquals(Socks5.COMMAND_CONNECT, buffer.get());
+ assertEquals(Socks5.RESERVED, buffer.get());
+ assertEquals(Socks5.ADDRESS_TYPE_DOMAIN, buffer.get());
+ int hostLen = buffer.get() & 0xFF;
+ assertEquals(serverHost.length(), hostLen);
+ int limit = buffer.limit();
+ buffer.limit(buffer.position() + hostLen);
+ assertEquals(serverHost, StandardCharsets.US_ASCII.decode(buffer).toString());
+ buffer.limit(limit);
+ assertEquals(serverPort, buffer.getShort() & 0xFFFF);
- // Socks5 connect response.
- channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, SockConst.SUCCEEDED, SockConst.RSV, AddrType.IPV4, 0, 0, 0, 0, 0, 0}));
+ // Write connect response.
+ channel.write(ByteBuffer.wrap(new byte[]{
+ Socks5.VERSION, 0, Socks5.RESERVED, Socks5.ADDRESS_TYPE_IPV4, 127, 0, 0, 3, 11, 11
+ }));
- buffer = ByteBuffer.allocate(method.length() + 1 + path.length());
- read = channel.read(buffer);
- assertEquals(buffer.capacity(), read);
- buffer.flip();
- assertEquals(method + " " + path, StandardCharsets.UTF_8.decode(buffer).toString());
+ // Parse the HTTP request.
+ HttpTester.Request request = HttpTester.parseRequest(channel);
+ assertNotNull(request);
+ assertEquals(method, request.getMethod());
+ assertEquals(path, request.getUri());
- // http response
- String response =
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 0\r\n" +
- "Connection: close\r\n" +
- "\r\n";
- channel.write(ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8)));
+ // Write the HTTP response.
+ String response = "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 0\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+ channel.write(ByteBuffer.wrap(response.getBytes(StandardCharsets.US_ASCII)));
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@@ -231,8 +237,9 @@ public class Socks5ProxyTest
String username = "jetty";
String password = "pass";
int proxyPort = proxy.socket().getLocalPort();
- client.getProxyConfiguration().addProxy(new Socks5Proxy("127.0.0.1", proxyPort)
- .addAuthentication(new UsernamePasswordAuthentication(username, password)));
+ Socks5Proxy socks5Proxy = new Socks5Proxy("127.0.0.1", proxyPort);
+ socks5Proxy.putAuthenticationFactory(new UsernamePasswordAuthenticationFactory(username, password));
+ client.getProxyConfiguration().addProxy(socks5Proxy);
CountDownLatch latch = new CountDownLatch(1);
@@ -259,79 +266,83 @@ public class Socks5ProxyTest
int initLen = 2;
ByteBuffer buffer = ByteBuffer.allocate(initLen);
int read = channel.read(buffer);
+ buffer.flip();
assertEquals(initLen, read);
- assertEquals(SockConst.VER, buffer.get(0) & 0xFF);
- int authTypeLen = buffer.get(1) & 0xFF;
+ assertEquals(Socks5.VERSION, buffer.get());
+ int authTypeLen = buffer.get() & 0xFF;
assertTrue(authTypeLen > 0);
buffer = ByteBuffer.allocate(authTypeLen);
read = channel.read(buffer);
-
- // assert contains username password authorization
- assertEquals(authTypeLen, read);
buffer.flip();
+ assertEquals(authTypeLen, read);
byte[] authTypes = new byte[authTypeLen];
buffer.get(authTypes);
- assertTrue(containAuthType(authTypes, AuthType.USER_PASS));
+ byte authenticationMethod = Socks5.UsernamePasswordAuthenticationFactory.METHOD;
+ assertTrue(containsAuthType(authTypes, authenticationMethod));
- // write acceptable methods
- channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, AuthType.USER_PASS}));
+ // Write handshake response.
+ channel.write(ByteBuffer.wrap(new byte[]{Socks5.VERSION, authenticationMethod}));
- // read username password
+ // Read authentication request.
buffer = ByteBuffer.allocate(3 + username.length() + password.length());
read = channel.read(buffer);
- assertEquals(buffer.capacity(), read);
buffer.flip();
- byte[] userPass = buffer.array();
- assertEquals(SockConst.USER_PASS_VER, userPass[0] & 0xFF);
- int uLen = userPass[1] & 0xFF;
- assertEquals(username.length(), uLen);
- assertEquals(username, new String(userPass, 2, uLen, StandardCharsets.UTF_8));
- int pLen = userPass[2 + uLen];
- assertEquals(password.length(), pLen);
- assertEquals(password, new String(userPass, 3 + uLen, pLen, StandardCharsets.UTF_8));
+ assertEquals(buffer.capacity(), read);
+ assertEquals(1, buffer.get());
+ int usernameLen = buffer.get() & 0xFF;
+ assertEquals(username.length(), usernameLen);
+ int limit = buffer.limit();
+ buffer.limit(buffer.position() + usernameLen);
+ assertEquals(username, StandardCharsets.US_ASCII.decode(buffer).toString());
+ buffer.limit(limit);
+ int passwordLen = buffer.get() & 0xFF;
+ assertEquals(password.length(), passwordLen);
+ assertEquals(password, StandardCharsets.US_ASCII.decode(buffer).toString());
- // authorization success
- channel.write(ByteBuffer.wrap(new byte[]{SockConst.USER_PASS_VER, SockConst.SUCCEEDED}));
+ // Write authentication response.
+ channel.write(ByteBuffer.wrap(new byte[]{1, 0}));
- // read addr
+ // Read server address.
int addrLen = 10;
buffer = ByteBuffer.allocate(addrLen);
read = channel.read(buffer);
- assertEquals(addrLen, read);
- assertEquals(SockConst.VER, buffer.get(0) & 0xFF);
- assertEquals(Command.CONNECT, buffer.get(1) & 0xFF);
- assertEquals(SockConst.RSV, buffer.get(2) & 0xFF);
- assertEquals(AddrType.IPV4, buffer.get(3) & 0xFF);
- assertEquals(ip1, buffer.get(4) & 0xFF);
- assertEquals(ip2, buffer.get(5) & 0xFF);
- assertEquals(ip3, buffer.get(6) & 0xFF);
- assertEquals(ip4, buffer.get(7) & 0xFF);
- assertEquals(serverPort, buffer.getShort(8) & 0xFFFF);
-
- // Socks5 connect response.
- channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, SockConst.SUCCEEDED, SockConst.RSV, AddrType.IPV4, 0, 0, 0, 0, 0, 0}));
-
- buffer = ByteBuffer.allocate(method.length() + 1 + path.length());
- read = channel.read(buffer);
- assertEquals(buffer.capacity(), read);
buffer.flip();
- assertEquals(method + " " + path, StandardCharsets.UTF_8.decode(buffer).toString());
+ assertEquals(addrLen, read);
+ assertEquals(Socks5.VERSION, buffer.get());
+ assertEquals(Socks5.COMMAND_CONNECT, buffer.get());
+ assertEquals(Socks5.RESERVED, buffer.get());
+ assertEquals(Socks5.ADDRESS_TYPE_IPV4, buffer.get());
+ assertEquals(ip1, buffer.get());
+ assertEquals(ip2, buffer.get());
+ assertEquals(ip3, buffer.get());
+ assertEquals(ip4, buffer.get());
+ assertEquals(serverPort, buffer.getShort() & 0xFFFF);
- // http response
- String response =
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 0\r\n" +
- "Connection: close\r\n" +
- "\r\n";
- channel.write(ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8)));
+ // Write connect response.
+ channel.write(ByteBuffer.wrap(new byte[]{
+ Socks5.VERSION, 0, Socks5.RESERVED, Socks5.ADDRESS_TYPE_IPV4, 127, 0, 0, 4, 17, 17
+ }));
+
+ // Parse the HTTP request.
+ HttpTester.Request request = HttpTester.parseRequest(channel);
+ assertNotNull(request);
+ assertEquals(method, request.getMethod());
+ assertEquals(path, request.getUri());
+
+ // Write the HTTP response.
+ String response = "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 0\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+ channel.write(ByteBuffer.wrap(response.getBytes(StandardCharsets.US_ASCII)));
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
}
@Test
- public void testSocks5ProxyIpv4AuthNoAcceptable() throws Exception
+ public void testSocks5ProxyAuthNoAcceptable() throws Exception
{
int proxyPort = proxy.socket().getLocalPort();
client.getProxyConfiguration().addProxy(new Socks5Proxy("127.0.0.1", proxyPort));
@@ -356,22 +367,24 @@ public class Socks5ProxyTest
int read = channel.read(buffer);
assertEquals(initLen, read);
- // write acceptable methods
- channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, AuthType.NO_ACCEPTABLE}));
+ // Deny authentication method.
+ byte notAcceptable = -1;
+ channel.write(ByteBuffer.wrap(new byte[]{Socks5.VERSION, notAcceptable}));
ExecutionException x = assertThrows(ExecutionException.class, () -> listener.get(2 * timeout, TimeUnit.MILLISECONDS));
- assertThat(x.getCause(), instanceOf(SocketException.class));
+ assertThat(x.getCause(), instanceOf(IOException.class));
}
}
@Test
- public void testSocks5ProxyIpv4UsernamePasswordAuthFailed() throws Exception
+ public void testSocks5ProxyUsernamePasswordAuthFailed() throws Exception
{
String username = "jetty";
String password = "pass";
int proxyPort = proxy.socket().getLocalPort();
- client.getProxyConfiguration().addProxy(new Socks5Proxy("127.0.0.1", proxyPort)
- .addAuthentication(new UsernamePasswordAuthentication(username, password)));
+ Socks5Proxy socks5Proxy = new Socks5Proxy("127.0.0.1", proxyPort);
+ socks5Proxy.putAuthenticationFactory(new UsernamePasswordAuthenticationFactory(username, password));
+ client.getProxyConfiguration().addProxy(socks5Proxy);
long timeout = 1000;
String serverHost = "127.0.0.13";
@@ -391,43 +404,46 @@ public class Socks5ProxyTest
int initLen = 2;
ByteBuffer buffer = ByteBuffer.allocate(initLen);
int read = channel.read(buffer);
+ buffer.flip();
assertEquals(initLen, read);
- assertEquals(SockConst.VER, buffer.get(0) & 0xFF);
- int authTypeLen = buffer.get(1) & 0xFF;
+ assertEquals(Socks5.VERSION, buffer.get());
+ int authTypeLen = buffer.get() & 0xFF;
assertTrue(authTypeLen > 0);
buffer = ByteBuffer.allocate(authTypeLen);
read = channel.read(buffer);
-
- // assert contains username password authorization
- assertEquals(authTypeLen, read);
buffer.flip();
+ assertEquals(authTypeLen, read);
byte[] authTypes = new byte[authTypeLen];
buffer.get(authTypes);
- assertTrue(containAuthType(authTypes, AuthType.USER_PASS));
+ byte authenticationMethod = Socks5.UsernamePasswordAuthenticationFactory.METHOD;
+ assertTrue(containsAuthType(authTypes, authenticationMethod));
- // write acceptable methods
- channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, AuthType.USER_PASS}));
+ // Write handshake response.
+ channel.write(ByteBuffer.wrap(new byte[]{Socks5.VERSION, authenticationMethod}));
- // read username password
+ // Read authentication request.
buffer = ByteBuffer.allocate(3 + username.length() + password.length());
read = channel.read(buffer);
- assertEquals(buffer.capacity(), read);
buffer.flip();
- byte[] userPass = buffer.array();
- assertEquals(SockConst.USER_PASS_VER, userPass[0] & 0xFF);
- int uLen = userPass[1] & 0xFF;
- assertEquals(username.length(), uLen);
- assertEquals(username, new String(userPass, 2, uLen, StandardCharsets.UTF_8));
- int pLen = userPass[2 + uLen];
- assertEquals(password.length(), pLen);
- assertEquals(password, new String(userPass, 3 + uLen, pLen, StandardCharsets.UTF_8));
+ assertEquals(buffer.capacity(), read);
+ assertEquals(1, buffer.get());
+ int usernameLen = buffer.get() & 0xFF;
+ assertEquals(username.length(), usernameLen);
+ int limit = buffer.limit();
+ buffer.limit(buffer.position() + usernameLen);
+ assertEquals(username, StandardCharsets.US_ASCII.decode(buffer).toString());
+ buffer.limit(limit);
+ int passwordLen = buffer.get() & 0xFF;
+ assertEquals(password.length(), passwordLen);
+ assertEquals(password, StandardCharsets.US_ASCII.decode(buffer).toString());
- // authorization failed
- channel.write(ByteBuffer.wrap(new byte[]{SockConst.USER_PASS_VER, SockConst.AUTH_FAILED}));
+ // Fail authentication.
+ byte authenticationFailed = 1; // Any non-zero.
+ channel.write(ByteBuffer.wrap(new byte[]{1, authenticationFailed}));
ExecutionException x = assertThrows(ExecutionException.class, () -> listener.get(2 * timeout, TimeUnit.MILLISECONDS));
- assertThat(x.getCause(), instanceOf(SocketException.class));
+ assertThat(x.getCause(), instanceOf(IOException.class));
}
}
@@ -437,8 +453,9 @@ public class Socks5ProxyTest
String username = "jetty";
String password = "pass";
int proxyPort = proxy.socket().getLocalPort();
- client.getProxyConfiguration().addProxy(new Socks5Proxy("127.0.0.1", proxyPort)
- .addAuthentication(new UsernamePasswordAuthentication(username, password)));
+ Socks5Proxy socks5Proxy = new Socks5Proxy("127.0.0.1", proxyPort);
+ socks5Proxy.putAuthenticationFactory(new UsernamePasswordAuthenticationFactory(username, password));
+ client.getProxyConfiguration().addProxy(socks5Proxy);
CountDownLatch latch = new CountDownLatch(1);
@@ -461,73 +478,78 @@ public class Socks5ProxyTest
int initLen = 2;
ByteBuffer buffer = ByteBuffer.allocate(initLen);
int read = channel.read(buffer);
+ buffer.flip();
assertEquals(initLen, read);
- assertEquals(SockConst.VER, buffer.get(0) & 0xFF);
- int authTypeLen = buffer.get(1) & 0xFF;
+ assertEquals(Socks5.VERSION, buffer.get());
+ int authTypeLen = buffer.get() & 0xFF;
assertTrue(authTypeLen > 0);
buffer = ByteBuffer.allocate(authTypeLen);
read = channel.read(buffer);
-
- // assert contains username password authorization
- assertEquals(authTypeLen, read);
buffer.flip();
+ assertEquals(authTypeLen, read);
byte[] authTypes = new byte[authTypeLen];
buffer.get(authTypes);
- assertTrue(containAuthType(authTypes, AuthType.USER_PASS));
+ byte authenticationMethod = Socks5.UsernamePasswordAuthenticationFactory.METHOD;
+ assertTrue(containsAuthType(authTypes, authenticationMethod));
- // write acceptable methods
- channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, AuthType.USER_PASS}));
+ // Write handshake response.
+ channel.write(ByteBuffer.wrap(new byte[]{Socks5.VERSION, authenticationMethod}));
- // read username password
+ // Read authentication request.
buffer = ByteBuffer.allocate(3 + username.length() + password.length());
read = channel.read(buffer);
- assertEquals(buffer.capacity(), read);
buffer.flip();
- byte[] userPass = buffer.array();
- assertEquals(SockConst.USER_PASS_VER, userPass[0] & 0xFF);
- int uLen = userPass[1] & 0xFF;
- assertEquals(username.length(), uLen);
- assertEquals(username, new String(userPass, 2, uLen, StandardCharsets.UTF_8));
- int pLen = userPass[2 + uLen];
- assertEquals(password.length(), pLen);
- assertEquals(password, new String(userPass, 3 + uLen, pLen, StandardCharsets.UTF_8));
+ assertEquals(buffer.capacity(), read);
+ assertEquals(1, buffer.get());
+ int usernameLen = buffer.get() & 0xFF;
+ assertEquals(username.length(), usernameLen);
+ int limit = buffer.limit();
+ buffer.limit(buffer.position() + usernameLen);
+ assertEquals(username, StandardCharsets.US_ASCII.decode(buffer).toString());
+ buffer.limit(limit);
+ int passwordLen = buffer.get() & 0xFF;
+ assertEquals(password.length(), passwordLen);
+ assertEquals(password, StandardCharsets.US_ASCII.decode(buffer).toString());
- // authorization success
- channel.write(ByteBuffer.wrap(new byte[]{SockConst.USER_PASS_VER, SockConst.SUCCEEDED}));
+ // Write authentication response.
+ channel.write(ByteBuffer.wrap(new byte[]{1, 0}));
- // read addr
+ // Read server address.
int addrLen = 7 + serverHost.length();
buffer = ByteBuffer.allocate(addrLen);
read = channel.read(buffer);
+ buffer.flip();
assertEquals(addrLen, read);
- buffer.flip();
- byte[] bs = buffer.array();
- assertEquals(SockConst.VER, bs[0] & 0xFF);
- assertEquals(Command.CONNECT, bs[1] & 0xFF);
- assertEquals(SockConst.RSV, bs[2] & 0xFF);
- assertEquals(AddrType.DOMAIN_NAME, bs[3] & 0xFF);
- int hLen = bs[4] & 0xFF;
- assertEquals(serverHost.length(), hLen);
- assertEquals(serverHost, new String(bs, 5, hLen, StandardCharsets.UTF_8));
- assertEquals(serverPort, buffer.getShort(5 + hLen) & 0xFFFF);
+ assertEquals(Socks5.VERSION, buffer.get());
+ assertEquals(Socks5.COMMAND_CONNECT, buffer.get());
+ assertEquals(Socks5.RESERVED, buffer.get());
+ assertEquals(Socks5.ADDRESS_TYPE_DOMAIN, buffer.get());
+ int domainLen = buffer.get() & 0xFF;
+ assertEquals(serverHost.length(), domainLen);
+ limit = buffer.limit();
+ buffer.limit(buffer.position() + domainLen);
+ assertEquals(serverHost, StandardCharsets.US_ASCII.decode(buffer).toString());
+ buffer.limit(limit);
+ assertEquals(serverPort, buffer.getShort() & 0xFFFF);
- // Socks5 connect response.
- channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, SockConst.SUCCEEDED, SockConst.RSV, AddrType.IPV4, 0, 0, 0, 0, 0, 0}));
+ // Write connect response.
+ channel.write(ByteBuffer.wrap(new byte[]{
+ Socks5.VERSION, 0, Socks5.RESERVED, Socks5.ADDRESS_TYPE_IPV4, 127, 0, 0, 5, 19, 19
+ }));
- buffer = ByteBuffer.allocate(method.length() + 1 + path.length());
- read = channel.read(buffer);
- assertEquals(buffer.capacity(), read);
- buffer.flip();
- assertEquals(method + " " + path, StandardCharsets.UTF_8.decode(buffer).toString());
+ // Parse the HTTP request.
+ HttpTester.Request request = HttpTester.parseRequest(channel);
+ assertNotNull(request);
+ assertEquals(method, request.getMethod());
+ assertEquals(path, request.getUri());
- // http response
- String response =
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 0\r\n" +
- "Connection: close\r\n" +
- "\r\n";
- channel.write(ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8)));
+ // Write the HTTP response.
+ String response = "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 0\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+ channel.write(ByteBuffer.wrap(response.getBytes(StandardCharsets.US_ASCII)));
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@@ -539,8 +561,9 @@ public class Socks5ProxyTest
String username = "jetty";
String password = "pass";
int proxyPort = proxy.socket().getLocalPort();
- client.getProxyConfiguration().addProxy(new Socks5Proxy("127.0.0.1", proxyPort)
- .addAuthentication(new UsernamePasswordAuthentication(username, password)));
+ Socks5Proxy socks5Proxy = new Socks5Proxy("127.0.0.1", proxyPort);
+ socks5Proxy.putAuthenticationFactory(new UsernamePasswordAuthenticationFactory(username, password));
+ client.getProxyConfiguration().addProxy(socks5Proxy);
CountDownLatch latch = new CountDownLatch(1);
@@ -563,78 +586,81 @@ public class Socks5ProxyTest
int initLen = 2;
ByteBuffer buffer = ByteBuffer.allocate(initLen);
int read = channel.read(buffer);
+ buffer.flip();
assertEquals(initLen, read);
- assertEquals(SockConst.VER, buffer.get(0) & 0xFF);
- int authTypeLen = buffer.get(1) & 0xFF;
+ assertEquals(Socks5.VERSION, buffer.get());
+ int authTypeLen = buffer.get() & 0xFF;
assertTrue(authTypeLen > 0);
buffer = ByteBuffer.allocate(authTypeLen);
read = channel.read(buffer);
-
- // assert contains username password authorization
- assertEquals(authTypeLen, read);
buffer.flip();
+ assertEquals(authTypeLen, read);
byte[] authTypes = new byte[authTypeLen];
buffer.get(authTypes);
- assertTrue(containAuthType(authTypes, AuthType.USER_PASS));
+ byte authenticationMethod = Socks5.UsernamePasswordAuthenticationFactory.METHOD;
+ assertTrue(containsAuthType(authTypes, authenticationMethod));
- // write acceptable methods
- channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, AuthType.USER_PASS}));
+ // Write handshake response.
+ channel.write(ByteBuffer.wrap(new byte[]{Socks5.VERSION, authenticationMethod}));
- // read username password
+ // Read authentication request.
buffer = ByteBuffer.allocate(3 + username.length() + password.length());
read = channel.read(buffer);
- assertEquals(buffer.capacity(), read);
buffer.flip();
- byte[] userPass = buffer.array();
- assertEquals(SockConst.USER_PASS_VER, userPass[0] & 0xFF);
- int uLen = userPass[1] & 0xFF;
- assertEquals(username.length(), uLen);
- assertEquals(username, new String(userPass, 2, uLen, StandardCharsets.UTF_8));
- int pLen = userPass[2 + uLen];
- assertEquals(password.length(), pLen);
- assertEquals(password, new String(userPass, 3 + uLen, pLen, StandardCharsets.UTF_8));
+ assertEquals(buffer.capacity(), read);
+ assertEquals(1, buffer.get());
+ int usernameLen = buffer.get() & 0xFF;
+ assertEquals(username.length(), usernameLen);
+ int limit = buffer.limit();
+ buffer.limit(buffer.position() + usernameLen);
+ assertEquals(username, StandardCharsets.US_ASCII.decode(buffer).toString());
+ buffer.limit(limit);
+ int passwordLen = buffer.get() & 0xFF;
+ assertEquals(password.length(), passwordLen);
+ assertEquals(password, StandardCharsets.US_ASCII.decode(buffer).toString());
- // authorization success
- channel.write(ByteBuffer.wrap(new byte[]{SockConst.USER_PASS_VER, SockConst.SUCCEEDED}));
+ // Write authentication response.
+ channel.write(ByteBuffer.wrap(new byte[]{1, 0}));
- // read addr
+ // Read server address.
int addrLen = 7 + serverHost.length();
buffer = ByteBuffer.allocate(addrLen);
read = channel.read(buffer);
- assertEquals(addrLen, read);
buffer.flip();
- byte[] bs = buffer.array();
- assertEquals(SockConst.VER, bs[0] & 0xFF);
- assertEquals(Command.CONNECT, bs[1] & 0xFF);
- assertEquals(SockConst.RSV, bs[2] & 0xFF);
- assertEquals(AddrType.DOMAIN_NAME, bs[3] & 0xFF);
- int hLen = bs[4] & 0xFF;
- assertEquals(serverHost.length(), hLen);
- assertEquals(serverHost, new String(bs, 5, hLen, StandardCharsets.UTF_8));
- assertEquals(serverPort, buffer.getShort(5 + hLen) & 0xFFFF);
+ assertEquals(addrLen, read);
+ assertEquals(Socks5.VERSION, buffer.get());
+ assertEquals(Socks5.COMMAND_CONNECT, buffer.get());
+ assertEquals(Socks5.RESERVED, buffer.get());
+ assertEquals(Socks5.ADDRESS_TYPE_DOMAIN, buffer.get());
+ int domainLen = buffer.get() & 0xFF;
+ assertEquals(serverHost.length(), domainLen);
+ limit = buffer.limit();
+ buffer.limit(buffer.position() + domainLen);
+ assertEquals(serverHost, StandardCharsets.US_ASCII.decode(buffer).toString());
+ buffer.limit(limit);
+ assertEquals(serverPort, buffer.getShort() & 0xFFFF);
- // Socks5 connect response.
- byte[] chunk1 = new byte[]{SockConst.VER, SockConst.SUCCEEDED, SockConst.RSV, AddrType.IPV4};
- byte[] chunk2 = new byte[]{0, 0, 0, 0, 0, 0};
+ // Write connect response.
+ byte[] chunk1 = new byte[]{Socks5.VERSION, 0, Socks5.RESERVED, Socks5.ADDRESS_TYPE_IPV4};
channel.write(ByteBuffer.wrap(chunk1));
// Wait before sending the second chunk.
Thread.sleep(1000);
+ byte[] chunk2 = new byte[]{127, 0, 0, 6, 21, 21};
channel.write(ByteBuffer.wrap(chunk2));
- buffer = ByteBuffer.allocate(method.length() + 1 + path.length());
- read = channel.read(buffer);
- assertEquals(buffer.capacity(), read);
- buffer.flip();
- assertEquals(method + " " + path, StandardCharsets.UTF_8.decode(buffer).toString());
+ // Parse the HTTP request.
+ HttpTester.Request request = HttpTester.parseRequest(channel);
+ assertNotNull(request);
+ assertEquals(method, request.getMethod());
+ assertEquals(path, request.getUri());
- // http response
- String response =
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 0\r\n" +
- "Connection: close\r\n" +
- "\r\n";
- channel.write(ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8)));
+ // Write the HTTP response.
+ String response = "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 0\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+ channel.write(ByteBuffer.wrap(response.getBytes(StandardCharsets.US_ASCII)));
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@@ -647,7 +673,7 @@ public class Socks5ProxyTest
String serverHost = "127.0.0.13"; // Server host different from proxy host.
int serverPort = proxyPort + 1; // Any port will do.
- SslContextFactory clientTLS = client.getSslContextFactory();
+ SslContextFactory.Client clientTLS = client.getSslContextFactory();
clientTLS.reload(ssl ->
{
// The client keystore contains the trustedCertEntry for the
@@ -682,19 +708,23 @@ public class Socks5ProxyTest
int initLen = 3;
ByteBuffer buffer = ByteBuffer.allocate(initLen);
int read = channel.read(buffer);
+ buffer.flip();
assertEquals(initLen, read);
- // write acceptable methods
- channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, AuthType.NO_AUTH}));
+ // Write handshake response.
+ channel.write(ByteBuffer.wrap(new byte[]{Socks5.VERSION, Socks5.NoAuthenticationFactory.METHOD}));
- // read addr
+ // Read server address.
int addrLen = 10;
buffer = ByteBuffer.allocate(addrLen);
read = channel.read(buffer);
+ buffer.flip();
assertEquals(addrLen, read);
- // Socks5 connect response.
- channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, SockConst.SUCCEEDED, SockConst.RSV, AddrType.IPV4, 0, 0, 0, 0, 0, 0}));
+ // Write connect response.
+ channel.write(ByteBuffer.wrap(new byte[]{
+ Socks5.VERSION, 0, Socks5.RESERVED, Socks5.ADDRESS_TYPE_IPV4, 127, 0, 0, 7, 23, 23
+ }));
// Wrap the socket with TLS.
SslContextFactory.Server serverTLS = new SslContextFactory.Server();
@@ -705,30 +735,19 @@ public class Socks5ProxyTest
SSLSocket sslSocket = (SSLSocket)sslContext.getSocketFactory().createSocket(channel.socket(), serverHost, serverPort, false);
sslSocket.setUseClientMode(false);
- // Read the request.
- int crlfs = 0;
- InputStream input = sslSocket.getInputStream();
- while (true)
- {
- read = input.read();
- if (read < 0)
- break;
- if (read == '\r' || read == '\n')
- ++crlfs;
- else
- crlfs = 0;
- if (crlfs == 4)
- break;
- }
+ // Parse the HTTP request.
+ HttpTester.Request request = HttpTester.parseRequest(sslSocket.getInputStream());
+ assertNotNull(request);
+ assertEquals(method, request.getMethod());
+ assertEquals(path, request.getUri());
- // Send the response.
- String response =
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: 0\r\n" +
- "Connection: close\r\n" +
- "\r\n";
+ // Write the HTTP response.
+ String response = "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 0\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
OutputStream output = sslSocket.getOutputStream();
- output.write(response.getBytes(StandardCharsets.UTF_8));
+ output.write(response.getBytes(StandardCharsets.US_ASCII));
output.flush();
assertTrue(latch.await(5, TimeUnit.SECONDS));
@@ -761,7 +780,7 @@ public class Socks5ProxyTest
}
@Test
- public void testSocksProxyClosesConnectionImmediately() throws Exception
+ public void testSocksProxyClosesConnectionInHandshake() throws Exception
{
int proxyPort = proxy.socket().getLocalPort();
client.getProxyConfiguration().addProxy(new Socks5Proxy("127.0.0.1", proxyPort));
@@ -779,7 +798,112 @@ public class Socks5ProxyTest
channel.close();
ExecutionException x = assertThrows(ExecutionException.class, () -> listener.get(5, TimeUnit.SECONDS));
- assertThat(x.getCause(), instanceOf(SocketException.class));
+ assertThat(x.getCause(), instanceOf(ClosedChannelException.class));
+ }
+ }
+
+ @Test
+ public void testSocksProxyClosesConnectionInAuthentication() throws Exception
+ {
+ String username = "jetty";
+ String password = "pass";
+ int proxyPort = proxy.socket().getLocalPort();
+ Socks5Proxy socks5Proxy = new Socks5Proxy("127.0.0.1", proxyPort);
+ socks5Proxy.putAuthenticationFactory(new UsernamePasswordAuthenticationFactory(username, password));
+ client.getProxyConfiguration().addProxy(socks5Proxy);
+
+ // Use an address to avoid resolution of "localhost" to multiple addresses.
+ String serverHost = "127.0.0.13";
+ int serverPort = proxyPort + 1; // Any port will do
+ Request request = client.newRequest(serverHost, serverPort);
+ FutureResponseListener listener = new FutureResponseListener(request);
+ request.send(listener);
+
+ try (SocketChannel channel = proxy.accept())
+ {
+ int initLen = 2;
+ ByteBuffer buffer = ByteBuffer.allocate(initLen);
+ int read = channel.read(buffer);
+ buffer.flip();
+ assertEquals(initLen, read);
+ assertEquals(Socks5.VERSION, buffer.get());
+ int authTypeLen = buffer.get() & 0xFF;
+ assertTrue(authTypeLen > 0);
+
+ buffer = ByteBuffer.allocate(authTypeLen);
+ read = channel.read(buffer);
+ buffer.flip();
+ assertEquals(authTypeLen, read);
+ byte[] authTypes = new byte[authTypeLen];
+ buffer.get(authTypes);
+ byte authenticationMethod = Socks5.UsernamePasswordAuthenticationFactory.METHOD;
+ assertTrue(containsAuthType(authTypes, authenticationMethod));
+
+ // Write handshake response.
+ channel.write(ByteBuffer.wrap(new byte[]{Socks5.VERSION, authenticationMethod}));
+
+ // Read authentication request.
+ buffer = ByteBuffer.allocate(3 + username.length() + password.length());
+ read = channel.read(buffer);
+ buffer.flip();
+ assertEquals(buffer.capacity(), read);
+ assertEquals(1, buffer.get());
+ int usernameLen = buffer.get() & 0xFF;
+ assertEquals(username.length(), usernameLen);
+ int limit = buffer.limit();
+ buffer.limit(buffer.position() + usernameLen);
+ assertEquals(username, StandardCharsets.US_ASCII.decode(buffer).toString());
+ buffer.limit(limit);
+ int passwordLen = buffer.get() & 0xFF;
+ assertEquals(password.length(), passwordLen);
+ assertEquals(password, StandardCharsets.US_ASCII.decode(buffer).toString());
+
+ channel.close();
+
+ ExecutionException x = assertThrows(ExecutionException.class, () -> listener.get(5, TimeUnit.SECONDS));
+ assertThat(x.getCause(), instanceOf(ClosedChannelException.class));
+ }
+ }
+
+ @Test
+ public void testSocksProxyClosesConnectionInConnect() throws Exception
+ {
+ int proxyPort = proxy.socket().getLocalPort();
+ client.getProxyConfiguration().addProxy(new Socks5Proxy("127.0.0.1", proxyPort));
+
+ // Use an address to avoid resolution of "localhost" to multiple addresses.
+ String serverHost = "127.0.0.13";
+ int serverPort = proxyPort + 1; // Any port will do
+ Request request = client.newRequest(serverHost, serverPort);
+ FutureResponseListener listener = new FutureResponseListener(request);
+ request.send(listener);
+
+ try (SocketChannel channel = proxy.accept())
+ {
+ int initLen = 3;
+ ByteBuffer buffer = ByteBuffer.allocate(initLen);
+ int read = channel.read(buffer);
+ buffer.flip();
+ assertEquals(initLen, read);
+ assertEquals(Socks5.VERSION, buffer.get());
+ assertEquals(1, buffer.get());
+ byte authenticationMethod = Socks5.NoAuthenticationFactory.METHOD;
+ assertEquals(authenticationMethod, buffer.get());
+
+ // Write handshake response.
+ channel.write(ByteBuffer.wrap(new byte[]{Socks5.VERSION, authenticationMethod}));
+
+ // Read server address.
+ int addrLen = 10;
+ buffer = ByteBuffer.allocate(addrLen);
+ read = channel.read(buffer);
+ buffer.flip();
+ assertEquals(addrLen, read);
+
+ channel.close();
+
+ ExecutionException x = assertThrows(ExecutionException.class, () -> listener.get(5, TimeUnit.SECONDS));
+ assertThat(x.getCause(), instanceOf(ClosedChannelException.class));
}
}
@@ -801,18 +925,140 @@ public class Socks5ProxyTest
channel.write(ByteBuffer.wrap(new byte[]{1, 2, 3, 4, 5}));
ExecutionException x = assertThrows(ExecutionException.class, () -> listener.get(5, TimeUnit.SECONDS));
- assertThat(x.getCause(), instanceOf(SocketException.class));
+ assertThat(x.getCause(), instanceOf(IOException.class));
}
}
- private boolean containAuthType(byte[] methods, byte method)
+ @Test
+ public void testSocks5ProxyConnectFailed() throws Exception
+ {
+ int proxyPort = proxy.socket().getLocalPort();
+ client.getProxyConfiguration().addProxy(new Socks5Proxy("127.0.0.1", proxyPort));
+
+ String serverHost = "127.0.0.13";
+ int serverPort = proxyPort + 1; // Any port will do
+ Request request = client.newRequest(serverHost, serverPort);
+ FutureResponseListener listener = new FutureResponseListener(request);
+ request.send(listener);
+
+ try (SocketChannel channel = proxy.accept())
+ {
+ int initLen = 3;
+ ByteBuffer buffer = ByteBuffer.allocate(initLen);
+ int read = channel.read(buffer);
+ buffer.flip();
+ assertEquals(initLen, read);
+
+ // Write handshake response.
+ channel.write(ByteBuffer.wrap(new byte[]{Socks5.VERSION, Socks5.NoAuthenticationFactory.METHOD}));
+
+ // Read server address.
+ int addrLen = 10;
+ buffer = ByteBuffer.allocate(addrLen);
+ read = channel.read(buffer);
+ buffer.flip();
+ assertEquals(addrLen, read);
+
+ // Write connect response failure.
+ channel.write(ByteBuffer.wrap(new byte[]{
+ Socks5.VERSION, 1, Socks5.RESERVED, Socks5.ADDRESS_TYPE_IPV4, 127, 0, 0, 8, 29, 29
+ }));
+
+ ExecutionException x = assertThrows(ExecutionException.class, () -> listener.get(5, TimeUnit.SECONDS));
+ assertThat(x.getCause(), instanceOf(IOException.class));
+ }
+ }
+
+ @Test
+ public void testSocks5ProxyIPv6NoAuth() throws Exception
+ {
+ int proxyPort = proxy.socket().getLocalPort();
+ client.getProxyConfiguration().addProxy(new Socks5Proxy("127.0.0.1", proxyPort));
+
+ CountDownLatch latch = new CountDownLatch(1);
+
+ String serverHost = "::13";
+ int serverPort = proxyPort + 1; // Any port will do
+ String method = "GET";
+ String path = "/path";
+ client.newRequest(serverHost, serverPort)
+ .method(method)
+ .path(path)
+ .timeout(5, TimeUnit.SECONDS)
+ .send(result ->
+ {
+ if (result.isSucceeded())
+ latch.countDown();
+ });
+
+ try (SocketChannel channel = proxy.accept())
+ {
+ int initLen = 3;
+ ByteBuffer buffer = ByteBuffer.allocate(initLen);
+ int read = channel.read(buffer);
+ buffer.flip();
+ assertEquals(initLen, read);
+
+ // Write handshake response.
+ channel.write(ByteBuffer.wrap(new byte[]{Socks5.VERSION, Socks5.NoAuthenticationFactory.METHOD}));
+
+ // Read server address.
+ int addrLen = 22;
+ buffer = ByteBuffer.allocate(addrLen);
+ read = channel.read(buffer);
+ buffer.flip();
+ assertEquals(addrLen, read);
+ assertEquals(Socks5.VERSION, buffer.get());
+ assertEquals(Socks5.COMMAND_CONNECT, buffer.get());
+ assertEquals(Socks5.RESERVED, buffer.get());
+ assertEquals(Socks5.ADDRESS_TYPE_IPV6, buffer.get());
+ for (int i = 0; i < 15; ++i)
+ {
+ assertEquals(0, buffer.get());
+ }
+ assertEquals(0x13, buffer.get());
+ assertEquals(serverPort, buffer.getShort() & 0xFFFF);
+
+ // Write connect response.
+ ByteBuffer byteBuffer = ByteBuffer.allocate(22)
+ .put(Socks5.VERSION)
+ .put((byte)0)
+ .put(Socks5.RESERVED)
+ .put(Socks5.ADDRESS_TYPE_IPV6)
+ .put(InetAddress.getByName(serverHost).getAddress())
+ .putShort((short)3131)
+ .flip();
+ // Write slowly 1 byte at a time.
+ for (int limit = 1; limit <= buffer.capacity(); ++limit)
+ {
+ byteBuffer.limit(limit);
+ channel.write(byteBuffer);
+ Thread.sleep(100);
+ }
+
+ // Parse the HTTP request.
+ HttpTester.Request request = HttpTester.parseRequest(channel);
+ assertNotNull(request);
+ assertEquals(method, request.getMethod());
+ assertEquals(path, request.getUri());
+
+ // Write the HTTP response.
+ String response = "HTTP/1.1 200 OK\r\n" +
+ "Content-Length: 0\r\n" +
+ "Connection: close\r\n" +
+ "\r\n";
+ channel.write(ByteBuffer.wrap(response.getBytes(StandardCharsets.US_ASCII)));
+
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+ }
+ }
+
+ private boolean containsAuthType(byte[] methods, byte method)
{
for (byte m : methods)
{
if (m == method)
- {
return true;
- }
}
return false;
}
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTester.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTester.java
index 9cc0c59242b..52af1cdc97b 100644
--- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTester.java
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTester.java
@@ -176,6 +176,36 @@ public class HttpTester
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)
{
Response r = new Response();
@@ -230,7 +260,7 @@ public class HttpTester
else
r = (Response)parser.getHandler();
- parseResponse(in, parser, r);
+ parse(in, parser);
if (r.isComplete())
return r;
@@ -246,13 +276,13 @@ public class HttpTester
{
parser = new HttpParser(response);
}
- parseResponse(in, parser, response);
+ parse(in, parser);
if (!response.isComplete())
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();