Initial implementation for #7091 - SOCKS5 support.
This commit is contained in:
parent
6a0752384f
commit
28cd6d8ada
|
@ -0,0 +1,152 @@
|
||||||
|
//
|
||||||
|
// ========================================================================
|
||||||
|
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||||
|
//
|
||||||
|
// This program and the accompanying materials are made available under the
|
||||||
|
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||||
|
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||||
|
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||||
|
// ========================================================================
|
||||||
|
//
|
||||||
|
|
||||||
|
package org.eclipse.jetty.client;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
public class Socks5
|
||||||
|
{
|
||||||
|
|
||||||
|
public enum RequestStage
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Authentication
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* get supported authentication type
|
||||||
|
* @see AuthType
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
byte getAuthType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* write authorize command
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
ByteBuffer authorize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class NoAuthentication implements Authentication
|
||||||
|
{
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte getAuthType()
|
||||||
|
{
|
||||||
|
return AuthType.NO_AUTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer authorize()
|
||||||
|
{
|
||||||
|
throw new UnsupportedOperationException("authorize error");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UsernamePasswordAuthentication implements Authentication
|
||||||
|
{
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
public UsernamePasswordAuthentication(String username, String password)
|
||||||
|
{
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte getAuthType()
|
||||||
|
{
|
||||||
|
return AuthType.USER_PASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer authorize()
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
userPass.put(password.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
userPass.flip();
|
||||||
|
return userPass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,459 @@
|
||||||
|
//
|
||||||
|
// ========================================================================
|
||||||
|
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||||
|
//
|
||||||
|
// This program and the accompanying materials are made available under the
|
||||||
|
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||||
|
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||||
|
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||||
|
// ========================================================================
|
||||||
|
//
|
||||||
|
|
||||||
|
package org.eclipse.jetty.client;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
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.api.Connection;
|
||||||
|
import org.eclipse.jetty.io.AbstractConnection;
|
||||||
|
import org.eclipse.jetty.io.ClientConnectionFactory;
|
||||||
|
import org.eclipse.jetty.io.ClientConnector;
|
||||||
|
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.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class Socks5Proxy extends Proxy
|
||||||
|
{
|
||||||
|
private static final int MAX_AUTHRATIONS = 255;
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(Socks5Proxy.class);
|
||||||
|
|
||||||
|
private LinkedHashMap<Byte, Authentication> authorizations = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
public Socks5Proxy(String host, int port)
|
||||||
|
{
|
||||||
|
this(new Origin.Address(host, port), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remove authorization by type
|
||||||
|
* @see AuthType
|
||||||
|
* @param type authorization type
|
||||||
|
*/
|
||||||
|
public Socks5Proxy removeAuthentication(byte type)
|
||||||
|
{
|
||||||
|
authorizations.remove(type);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientConnectionFactory newClientConnectionFactory(ClientConnectionFactory connectionFactory)
|
||||||
|
{
|
||||||
|
return new Socks5ProxyClientConnectionFactory(connectionFactory, authorizations);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(Origin origin)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Socks5ProxyConnection extends AbstractConnection implements Callback
|
||||||
|
{
|
||||||
|
private static final Pattern IPv4_PATTERN = Pattern.compile("(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})");
|
||||||
|
private final ClientConnectionFactory connectionFactory;
|
||||||
|
private final Map<String, Object> context;
|
||||||
|
|
||||||
|
private LinkedHashMap<Byte, Authentication> 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<String, Object> context)
|
||||||
|
{
|
||||||
|
super(endPoint, executor);
|
||||||
|
this.connectionFactory = connectionFactory;
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onOpen()
|
||||||
|
{
|
||||||
|
super.onOpen();
|
||||||
|
this.writeHandshakeCmd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeHandshakeCmd()
|
||||||
|
{
|
||||||
|
switch (requestStage)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void succeeded()
|
||||||
|
{
|
||||||
|
if (LOG.isDebugEnabled())
|
||||||
|
{
|
||||||
|
LOG.debug("Written SOCKS5 handshake request");
|
||||||
|
}
|
||||||
|
this.fillInterested();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void failed(Throwable x)
|
||||||
|
{
|
||||||
|
this.close();
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Promise<Connection> promise = (Promise<Connection>)this.context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
|
||||||
|
promise.failed(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onFillable()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Socks5Parser parser = new Socks5Parser();
|
||||||
|
ByteBuffer buffer;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
this.failed(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSocks5Response(byte[] bs) throws SocketException
|
||||||
|
{
|
||||||
|
switch (responseStage)
|
||||||
|
{
|
||||||
|
case INIT:
|
||||||
|
if (bs[0] != SockConst.VER)
|
||||||
|
{
|
||||||
|
throw new SocketException("SOCKS5 tunnel failed with err VER " + bs[0]);
|
||||||
|
}
|
||||||
|
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:
|
||||||
|
tunnel();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new SocketException("BAD SOCKS5 PROTOCOL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tunnel()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
|
||||||
|
// Don't want to do DNS resolution here.
|
||||||
|
InetSocketAddress address = InetSocketAddress.createUnresolved(destination.getHost(), destination.getPort());
|
||||||
|
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);
|
||||||
|
getEndPoint().upgrade(newConnection);
|
||||||
|
if (LOG.isDebugEnabled())
|
||||||
|
{
|
||||||
|
LOG.debug("SOCKS5 tunnel established: {} over {}", this, newConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
this.failed(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setResponseStage(ResponseStage responseStage)
|
||||||
|
{
|
||||||
|
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<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,819 @@
|
||||||
|
//
|
||||||
|
// ========================================================================
|
||||||
|
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||||
|
//
|
||||||
|
// This program and the accompanying materials are made available under the
|
||||||
|
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||||
|
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||||
|
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||||
|
// ========================================================================
|
||||||
|
//
|
||||||
|
|
||||||
|
package org.eclipse.jetty.client;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.ServerSocketChannel;
|
||||||
|
import java.nio.channels.SocketChannel;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
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.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.io.ClientConnector;
|
||||||
|
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||||
|
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
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.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
public class Socks5ProxyTest
|
||||||
|
{
|
||||||
|
private ServerSocketChannel proxy;
|
||||||
|
private HttpClient client;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void prepare() throws Exception
|
||||||
|
{
|
||||||
|
proxy = ServerSocketChannel.open();
|
||||||
|
proxy.bind(new InetSocketAddress("127.0.0.1", 0));
|
||||||
|
|
||||||
|
ClientConnector connector = new ClientConnector();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void dispose() throws Exception
|
||||||
|
{
|
||||||
|
client.stop();
|
||||||
|
proxy.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSocks5ProxyIpv4NoAuth() throws Exception
|
||||||
|
{
|
||||||
|
int proxyPort = proxy.socket().getLocalPort();
|
||||||
|
client.getProxyConfiguration().addProxy(new Socks5Proxy("127.0.0.1", proxyPort));
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
byte ip1 = 127;
|
||||||
|
byte ip2 = 0;
|
||||||
|
byte ip3 = 0;
|
||||||
|
byte ip4 = 13;
|
||||||
|
String serverHost = ip1 + "." + ip2 + "." + ip3 + "." + ip4;
|
||||||
|
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);
|
||||||
|
assertEquals(initLen, read);
|
||||||
|
assertEquals(SockConst.VER, buffer.get(0) & 0xFF);
|
||||||
|
assertEquals(1, buffer.get(1) & 0xFF);
|
||||||
|
assertEquals(AuthType.NO_AUTH, buffer.get(2) & 0xFF);
|
||||||
|
|
||||||
|
// write acceptable methods
|
||||||
|
channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, AuthType.NO_AUTH}));
|
||||||
|
|
||||||
|
// read addr
|
||||||
|
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());
|
||||||
|
|
||||||
|
// 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)));
|
||||||
|
|
||||||
|
assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSocks5ProxyDomainNoAuth() throws Exception
|
||||||
|
{
|
||||||
|
int proxyPort = proxy.socket().getLocalPort();
|
||||||
|
client.getProxyConfiguration().addProxy(new Socks5Proxy("127.0.0.1", proxyPort));
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
String serverHost = "example.com";
|
||||||
|
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);
|
||||||
|
assertEquals(initLen, read);
|
||||||
|
assertEquals(SockConst.VER, buffer.get(0) & 0xFF);
|
||||||
|
assertEquals(1, buffer.get(1) & 0xFF);
|
||||||
|
assertEquals(AuthType.NO_AUTH, buffer.get(2) & 0xFF);
|
||||||
|
|
||||||
|
// write acceptable methods
|
||||||
|
channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, AuthType.NO_AUTH}));
|
||||||
|
|
||||||
|
// read addr
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
|
||||||
|
// 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)));
|
||||||
|
|
||||||
|
assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSocks5ProxyIpv4UsernamePasswordAuth() 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)));
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
byte ip1 = 127;
|
||||||
|
byte ip2 = 0;
|
||||||
|
byte ip3 = 0;
|
||||||
|
byte ip4 = 13;
|
||||||
|
String serverHost = ip1 + "." + ip2 + "." + ip3 + "." + ip4;
|
||||||
|
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 = 2;
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(initLen);
|
||||||
|
int read = channel.read(buffer);
|
||||||
|
assertEquals(initLen, read);
|
||||||
|
assertEquals(SockConst.VER, buffer.get(0) & 0xFF);
|
||||||
|
int authTypeLen = buffer.get(1) & 0xFF;
|
||||||
|
assertTrue(authTypeLen > 0);
|
||||||
|
|
||||||
|
buffer = ByteBuffer.allocate(authTypeLen);
|
||||||
|
read = channel.read(buffer);
|
||||||
|
|
||||||
|
// assert contains username password authorization
|
||||||
|
assertEquals(authTypeLen, read);
|
||||||
|
buffer.flip();
|
||||||
|
byte[] authTypes = new byte[authTypeLen];
|
||||||
|
buffer.get(authTypes);
|
||||||
|
assertTrue(containAuthType(authTypes, AuthType.USER_PASS));
|
||||||
|
|
||||||
|
// write acceptable methods
|
||||||
|
channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, AuthType.USER_PASS}));
|
||||||
|
|
||||||
|
// read username password
|
||||||
|
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));
|
||||||
|
|
||||||
|
// authorization success
|
||||||
|
channel.write(ByteBuffer.wrap(new byte[]{SockConst.USER_PASS_VER, SockConst.SUCCEEDED}));
|
||||||
|
|
||||||
|
// read addr
|
||||||
|
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());
|
||||||
|
|
||||||
|
// 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)));
|
||||||
|
|
||||||
|
assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSocks5ProxyIpv4AuthNoAcceptable() throws Exception
|
||||||
|
{
|
||||||
|
int proxyPort = proxy.socket().getLocalPort();
|
||||||
|
client.getProxyConfiguration().addProxy(new Socks5Proxy("127.0.0.1", proxyPort));
|
||||||
|
|
||||||
|
long timeout = 1000;
|
||||||
|
String serverHost = "127.0.0.13";
|
||||||
|
int serverPort = proxyPort + 1; // Any port will do
|
||||||
|
String method = "GET";
|
||||||
|
String path = "/path";
|
||||||
|
Request request = client.newRequest(serverHost, serverPort)
|
||||||
|
.method(method)
|
||||||
|
.path(path)
|
||||||
|
.timeout(timeout, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
|
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);
|
||||||
|
assertEquals(initLen, read);
|
||||||
|
|
||||||
|
// write acceptable methods
|
||||||
|
channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, AuthType.NO_ACCEPTABLE}));
|
||||||
|
|
||||||
|
ExecutionException x = assertThrows(ExecutionException.class, () -> listener.get(2 * timeout, TimeUnit.MILLISECONDS));
|
||||||
|
assertThat(x.getCause(), instanceOf(SocketException.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSocks5ProxyIpv4UsernamePasswordAuthFailed() 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)));
|
||||||
|
|
||||||
|
long timeout = 1000;
|
||||||
|
String serverHost = "127.0.0.13";
|
||||||
|
int serverPort = proxyPort + 1; // Any port will do
|
||||||
|
String method = "GET";
|
||||||
|
String path = "/path";
|
||||||
|
Request request = client.newRequest(serverHost, serverPort)
|
||||||
|
.method(method)
|
||||||
|
.path(path)
|
||||||
|
.timeout(timeout, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
|
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);
|
||||||
|
assertEquals(initLen, read);
|
||||||
|
assertEquals(SockConst.VER, buffer.get(0) & 0xFF);
|
||||||
|
int authTypeLen = buffer.get(1) & 0xFF;
|
||||||
|
assertTrue(authTypeLen > 0);
|
||||||
|
|
||||||
|
buffer = ByteBuffer.allocate(authTypeLen);
|
||||||
|
read = channel.read(buffer);
|
||||||
|
|
||||||
|
// assert contains username password authorization
|
||||||
|
assertEquals(authTypeLen, read);
|
||||||
|
buffer.flip();
|
||||||
|
byte[] authTypes = new byte[authTypeLen];
|
||||||
|
buffer.get(authTypes);
|
||||||
|
assertTrue(containAuthType(authTypes, AuthType.USER_PASS));
|
||||||
|
|
||||||
|
// write acceptable methods
|
||||||
|
channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, AuthType.USER_PASS}));
|
||||||
|
|
||||||
|
// read username password
|
||||||
|
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));
|
||||||
|
|
||||||
|
// authorization failed
|
||||||
|
channel.write(ByteBuffer.wrap(new byte[]{SockConst.USER_PASS_VER, SockConst.AUTH_FAILED}));
|
||||||
|
|
||||||
|
ExecutionException x = assertThrows(ExecutionException.class, () -> listener.get(2 * timeout, TimeUnit.MILLISECONDS));
|
||||||
|
assertThat(x.getCause(), instanceOf(SocketException.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSocks5ProxyDomainUsernamePasswordAuth() 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)));
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
String serverHost = "example.com";
|
||||||
|
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 = 2;
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(initLen);
|
||||||
|
int read = channel.read(buffer);
|
||||||
|
assertEquals(initLen, read);
|
||||||
|
assertEquals(SockConst.VER, buffer.get(0) & 0xFF);
|
||||||
|
int authTypeLen = buffer.get(1) & 0xFF;
|
||||||
|
assertTrue(authTypeLen > 0);
|
||||||
|
|
||||||
|
buffer = ByteBuffer.allocate(authTypeLen);
|
||||||
|
read = channel.read(buffer);
|
||||||
|
|
||||||
|
// assert contains username password authorization
|
||||||
|
assertEquals(authTypeLen, read);
|
||||||
|
buffer.flip();
|
||||||
|
byte[] authTypes = new byte[authTypeLen];
|
||||||
|
buffer.get(authTypes);
|
||||||
|
assertTrue(containAuthType(authTypes, AuthType.USER_PASS));
|
||||||
|
|
||||||
|
// write acceptable methods
|
||||||
|
channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, AuthType.USER_PASS}));
|
||||||
|
|
||||||
|
// read username password
|
||||||
|
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));
|
||||||
|
|
||||||
|
// authorization success
|
||||||
|
channel.write(ByteBuffer.wrap(new byte[]{SockConst.USER_PASS_VER, SockConst.SUCCEEDED}));
|
||||||
|
|
||||||
|
// read addr
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
|
||||||
|
// 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)));
|
||||||
|
|
||||||
|
assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSocks5ProxyDomainUsernamePasswordAuthWithSplitResponse() 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)));
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
String serverHost = "example.com";
|
||||||
|
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 = 2;
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(initLen);
|
||||||
|
int read = channel.read(buffer);
|
||||||
|
assertEquals(initLen, read);
|
||||||
|
assertEquals(SockConst.VER, buffer.get(0) & 0xFF);
|
||||||
|
int authTypeLen = buffer.get(1) & 0xFF;
|
||||||
|
assertTrue(authTypeLen > 0);
|
||||||
|
|
||||||
|
buffer = ByteBuffer.allocate(authTypeLen);
|
||||||
|
read = channel.read(buffer);
|
||||||
|
|
||||||
|
// assert contains username password authorization
|
||||||
|
assertEquals(authTypeLen, read);
|
||||||
|
buffer.flip();
|
||||||
|
byte[] authTypes = new byte[authTypeLen];
|
||||||
|
buffer.get(authTypes);
|
||||||
|
assertTrue(containAuthType(authTypes, AuthType.USER_PASS));
|
||||||
|
|
||||||
|
// write acceptable methods
|
||||||
|
channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, AuthType.USER_PASS}));
|
||||||
|
|
||||||
|
// read username password
|
||||||
|
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));
|
||||||
|
|
||||||
|
// authorization success
|
||||||
|
channel.write(ByteBuffer.wrap(new byte[]{SockConst.USER_PASS_VER, SockConst.SUCCEEDED}));
|
||||||
|
|
||||||
|
// read addr
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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};
|
||||||
|
channel.write(ByteBuffer.wrap(chunk1));
|
||||||
|
// Wait before sending the second chunk.
|
||||||
|
Thread.sleep(1000);
|
||||||
|
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());
|
||||||
|
|
||||||
|
// 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)));
|
||||||
|
|
||||||
|
assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSocks5ProxyIpv4NoAuthWithTlsServer() throws Exception
|
||||||
|
{
|
||||||
|
int proxyPort = proxy.socket().getLocalPort();
|
||||||
|
String serverHost = "127.0.0.13"; // Server host different from proxy host.
|
||||||
|
int serverPort = proxyPort + 1; // Any port will do.
|
||||||
|
|
||||||
|
SslContextFactory clientTLS = client.getSslContextFactory();
|
||||||
|
clientTLS.reload(ssl ->
|
||||||
|
{
|
||||||
|
// The client keystore contains the trustedCertEntry for the
|
||||||
|
// self-signed server certificate, so it acts as a truststore.
|
||||||
|
ssl.setTrustStorePath("src/test/resources/client_keystore.p12");
|
||||||
|
ssl.setTrustStorePassword("storepwd");
|
||||||
|
// Disable TLS hostname verification, but
|
||||||
|
// enable application hostname verification.
|
||||||
|
ssl.setEndpointIdentificationAlgorithm(null);
|
||||||
|
// The hostname must be that of the server, not of the proxy.
|
||||||
|
ssl.setHostnameVerifier((hostname, session) -> serverHost.equals(hostname));
|
||||||
|
});
|
||||||
|
client.getProxyConfiguration().addProxy(new Socks5Proxy("127.0.0.1", proxyPort));
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
String method = "GET";
|
||||||
|
String path = "/path";
|
||||||
|
client.newRequest(serverHost, serverPort)
|
||||||
|
.scheme(HttpScheme.HTTPS.asString())
|
||||||
|
.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);
|
||||||
|
assertEquals(initLen, read);
|
||||||
|
|
||||||
|
// write acceptable methods
|
||||||
|
channel.write(ByteBuffer.wrap(new byte[]{SockConst.VER, AuthType.NO_AUTH}));
|
||||||
|
|
||||||
|
// read addr
|
||||||
|
int addrLen = 10;
|
||||||
|
buffer = ByteBuffer.allocate(addrLen);
|
||||||
|
read = channel.read(buffer);
|
||||||
|
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}));
|
||||||
|
|
||||||
|
// Wrap the socket with TLS.
|
||||||
|
SslContextFactory.Server serverTLS = new SslContextFactory.Server();
|
||||||
|
serverTLS.setKeyStorePath("src/test/resources/keystore.p12");
|
||||||
|
serverTLS.setKeyStorePassword("storepwd");
|
||||||
|
serverTLS.start();
|
||||||
|
SSLContext sslContext = serverTLS.getSslContext();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the 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.flush();
|
||||||
|
|
||||||
|
assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRequestTimeoutWhenSocksProxyDoesNotRespond() throws Exception
|
||||||
|
{
|
||||||
|
int proxyPort = proxy.socket().getLocalPort();
|
||||||
|
client.getProxyConfiguration().addProxy(new Socks4Proxy("127.0.0.1", proxyPort));
|
||||||
|
|
||||||
|
long timeout = 1000;
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
.timeout(timeout, TimeUnit.MILLISECONDS);
|
||||||
|
FutureResponseListener listener = new FutureResponseListener(request);
|
||||||
|
request.send(listener);
|
||||||
|
|
||||||
|
try (SocketChannel ignored = proxy.accept())
|
||||||
|
{
|
||||||
|
// Accept the connection, but do not reply and don't close.
|
||||||
|
|
||||||
|
ExecutionException x = assertThrows(ExecutionException.class, () -> listener.get(2 * timeout, TimeUnit.MILLISECONDS));
|
||||||
|
assertThat(x.getCause(), instanceOf(TimeoutException.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSocksProxyClosesConnectionImmediately() 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())
|
||||||
|
{
|
||||||
|
// Immediately close the connection.
|
||||||
|
channel.close();
|
||||||
|
|
||||||
|
ExecutionException x = assertThrows(ExecutionException.class, () -> listener.get(5, TimeUnit.SECONDS));
|
||||||
|
assertThat(x.getCause(), instanceOf(SocketException.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSocksProxyResponseGarbageBytes() 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())
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containAuthType(byte[] methods, byte method)
|
||||||
|
{
|
||||||
|
for (byte m : methods)
|
||||||
|
{
|
||||||
|
if (m == method)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue