Make Socks5TestProxy a subclass of Socks5Proxy

to reduce the duplicated code. This also means we are now testing the
real implementation.
This commit is contained in:
Florian Schmaus 2019-06-11 10:18:29 +02:00
parent 6e1193edaf
commit 9bb36fc63c
2 changed files with 42 additions and 236 deletions

View File

@ -72,7 +72,7 @@ import org.jivesoftware.smack.util.CloseableUtil;
*
* @author Henning Staib
*/
public final class Socks5Proxy {
public class Socks5Proxy {
private static final Logger LOGGER = Logger.getLogger(Socks5Proxy.class.getName());
private static final List<Socks5Proxy> RUNNING_PROXIES = new CopyOnWriteArrayList<>();
@ -95,7 +95,7 @@ public final class Socks5Proxy {
private int localSocks5ProxyPort = -7777;
/* reusable implementation of a SOCKS5 proxy server process */
private Socks5ServerProcess serverProcess;
private final Socks5ServerProcess serverProcess;
/* thread running the SOCKS5 server process */
private Thread serverThread;
@ -111,12 +111,20 @@ public final class Socks5Proxy {
private final Set<InetAddress> localAddresses = new LinkedHashSet<>(4);
/**
* If set to <code>true</code>, then all connections are allowed and the digest is not verified. Should be set to
* <code>false</code> for production usage and <code>true</code> for (unit) testing purposes.
*/
private final boolean allowAllConnections;
/**
* Private constructor.
*/
Socks5Proxy() {
this.serverProcess = new Socks5ServerProcess();
allowAllConnections = false;
Enumeration<NetworkInterface> networkInterfaces;
try {
networkInterfaces = NetworkInterface.getNetworkInterfaces();
@ -136,6 +144,22 @@ public final class Socks5Proxy {
replaceLocalAddresses(localAddresses);
}
/**
* Constructor a Socks5Proxy with the given socket. Used for unit test purposes.
*
* @param serverSocket the server socket to use
*/
protected Socks5Proxy(ServerSocket serverSocket) {
this.serverProcess = new Socks5ServerProcess();
this.serverSocket = serverSocket;
allowAllConnections = true;
startServerThread();
}
/**
* Returns true if the local Socks5 proxy should be started. Default is true.
*
@ -229,12 +253,7 @@ public final class Socks5Proxy {
}
if (this.serverSocket != null) {
this.serverThread = new Thread(this.serverProcess);
this.serverThread.setName("Smack Local SOCKS5 Proxy [" + this.serverSocket + ']');
this.serverThread.setDaemon(true);
RUNNING_PROXIES.add(this);
this.serverThread.start();
startServerThread();
}
}
catch (IOException e) {
@ -245,6 +264,15 @@ public final class Socks5Proxy {
return this.serverSocket;
}
private synchronized void startServerThread() {
this.serverThread = new Thread(this.serverProcess);
this.serverThread.setName("Smack Local SOCKS5 Proxy [" + this.serverSocket + ']');
this.serverThread.setDaemon(true);
RUNNING_PROXIES.add(this);
this.serverThread.start();
}
/**
* Stops the local SOCKS5 proxy server. If it is not running this method does nothing.
*/
@ -490,7 +518,7 @@ public final class Socks5Proxy {
String responseDigest = new String(connectionRequest, 5, connectionRequest[4], StandardCharsets.UTF_8);
// return error if digest is not allowed
if (!Socks5Proxy.this.allowedConnections.contains(responseDigest)) {
if (!allowAllConnections && !Socks5Proxy.this.allowedConnections.contains(responseDigest)) {
connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused)
out.write(connectionRequest);
out.flush();

View File

@ -16,22 +16,9 @@
*/
package org.jivesoftware.smackx.bytestreams.socks5;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.util.CloseableUtil;
import org.jivesoftware.smack.util.NetworkUtil;
/**
@ -40,225 +27,16 @@ import org.jivesoftware.smack.util.NetworkUtil;
*
* @author Henning Staib
*/
public final class Socks5TestProxy implements AutoCloseable {
private static final Logger LOGGER = Logger.getLogger(Socks5TestProxy.class.getName());
public final class Socks5TestProxy extends Socks5Proxy implements AutoCloseable {
/* reusable implementation of a SOCKS5 proxy server process */
private Socks5ServerProcess serverProcess;
public Socks5TestProxy(ServerSocket serverSocket) {
super(serverSocket);
}
/* thread running the SOCKS5 server process */
private Thread serverThread;
/* server socket to accept SOCKS5 connections */
private final ServerSocket serverSocket;
/* assigns a connection to a digest */
private final Map<String, Socket> connectionMap = new ConcurrentHashMap<String, Socket>();
private boolean startupComplete;
Socks5TestProxy() throws IOException {
public Socks5TestProxy() throws IOException {
this(NetworkUtil.getSocketOnLoopback());
}
Socks5TestProxy(ServerSocket serverSocket) {
this.serverSocket = serverSocket;
this.serverProcess = new Socks5ServerProcess();
this.serverThread = new Thread(this.serverProcess);
this.serverThread.start();
}
/**
* Stops the local SOCKS5 proxy server. If it is not running this method does nothing.
*/
public synchronized void stop() {
if (!isRunning()) {
return;
}
try {
this.serverSocket.close();
}
catch (IOException e) {
// do nothing
LOGGER.log(Level.SEVERE, "exception", e);
}
if (this.serverThread != null && this.serverThread.isAlive()) {
try {
this.serverThread.interrupt();
this.serverThread.join();
}
catch (InterruptedException e) {
// do nothing
LOGGER.log(Level.SEVERE, "exception", e);
}
}
this.serverThread = null;
}
/**
* Returns the host address of the local SOCKS5 proxy server.
*
* @return the host address of the local SOCKS5 proxy server
*/
public static String getAddress() {
try {
return InetAddress.getLocalHost().getHostAddress();
}
catch (UnknownHostException e) {
return null;
}
}
/**
* Returns the port of the local SOCKS5 proxy server. If it is not running -1 will be returned.
*
* @return the port of the local SOCKS5 proxy server or -1 if proxy is not running
*/
public int getPort() {
if (!isRunning()) {
return -1;
}
return this.serverSocket.getLocalPort();
}
/**
* Returns the socket for the given digest.
*
* @param digest identifying the connection
* @return socket or null if there is no socket for the given digest
* @throws InterruptedException
*/
public Socket getSocket(String digest) throws InterruptedException {
synchronized (this) {
long now = System.currentTimeMillis();
final long deadline = now + 5000;
while (!startupComplete && now < deadline) {
wait(deadline - now);
now = System.currentTimeMillis();
}
}
if (!startupComplete) {
throw new IllegalStateException("Startup of Socks5TestProxy failed within 5 seconds");
}
return this.connectionMap.get(digest);
}
/**
* Returns true if the local SOCKS5 proxy server is running, otherwise false.
*
* @return true if the local SOCKS5 proxy server is running, otherwise false
*/
public boolean isRunning() {
return !this.serverSocket.isClosed();
}
/**
* Implementation of a simplified SOCKS5 proxy server.
*
* @author Henning Staib
*/
class Socks5ServerProcess implements Runnable {
@Override
public void run() {
while (true) {
Socket socket = null;
try {
// TODO: Add !serverSocket.isClosed() into the while condition and remove the following lines.
if (Socks5TestProxy.this.serverSocket.isClosed()
|| Thread.currentThread().isInterrupted()) {
return;
}
// accept connection
socket = Socks5TestProxy.this.serverSocket.accept();
// initialize connection
establishConnection(socket);
synchronized (this) {
startupComplete = true;
notifyAll();
}
}
catch (SocketException e) {
/* do nothing */
LOGGER.log(Level.FINE, "Socket exception in Socks5TestProxy " + this, e);
}
catch (Exception e) {
LOGGER.log(Level.SEVERE, "exception", e);
CloseableUtil.maybeClose(socket, LOGGER);
}
}
}
/**
* Negotiates a SOCKS5 connection and stores it on success.
*
* @param socket connection to the client
* @throws SmackException if client requests a connection in an unsupported way
* @throws IOException if a network error occurred
*/
private void establishConnection(Socket socket) throws IOException, SmackException {
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
DataInputStream in = new DataInputStream(socket.getInputStream());
// first byte is version should be 5
int b = in.read();
if (b != 5) {
throw new SmackException.SmackMessageException("Only SOCKS5 supported");
}
// second byte number of authentication methods supported
b = in.read();
// read list of supported authentication methods
byte[] auth = new byte[b];
in.readFully(auth);
byte[] authMethodSelectionResponse = new byte[2];
authMethodSelectionResponse[0] = (byte) 0x05; // protocol version
// only authentication method 0, no authentication, supported
boolean noAuthMethodFound = false;
for (int i = 0; i < auth.length; i++) {
if (auth[i] == (byte) 0x00) {
noAuthMethodFound = true;
break;
}
}
if (!noAuthMethodFound) {
authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods
out.write(authMethodSelectionResponse);
out.flush();
throw new SmackException.SmackMessageException("Authentication method not supported");
}
authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method
out.write(authMethodSelectionResponse);
out.flush();
// receive connection request
byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in);
// extract digest
String responseDigest = new String(connectionRequest, 5, connectionRequest[4], StandardCharsets.UTF_8);
connectionRequest[1] = (byte) 0x00; // set return status to 0 (success)
out.write(connectionRequest);
out.flush();
// store connection
Socks5TestProxy.this.connectionMap.put(responseDigest, socket);
}
}
@Override
public void close() {
stop();