diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java index 4ce9f6c9a..bff8d6fac 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java @@ -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 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 localAddresses = new LinkedHashSet<>(4); + /** + * If set to true, then all connections are allowed and the digest is not verified. Should be set to + * false for production usage and true for (unit) testing purposes. + */ + private final boolean allowAllConnections; + /** * Private constructor. */ Socks5Proxy() { this.serverProcess = new Socks5ServerProcess(); + allowAllConnections = false; + Enumeration 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(); diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5TestProxy.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5TestProxy.java index 3c4fb236e..3d32ebd40 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5TestProxy.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5TestProxy.java @@ -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 connectionMap = new ConcurrentHashMap(); - - 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();