Socks5Proxy.java
- /**
- *
- * Copyright the original author or authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- 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.InterfaceAddress;
- import java.net.NetworkInterface;
- import java.net.ServerSocket;
- import java.net.Socket;
- import java.net.SocketException;
- import java.nio.charset.StandardCharsets;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.Enumeration;
- import java.util.HashSet;
- import java.util.LinkedHashSet;
- import java.util.LinkedList;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import java.util.concurrent.ConcurrentHashMap;
- import java.util.concurrent.CopyOnWriteArrayList;
- import java.util.logging.Level;
- import java.util.logging.Logger;
- import org.jivesoftware.smack.SmackException;
- import org.jivesoftware.smack.util.CloseableUtil;
- /**
- * The Socks5Proxy class represents a local SOCKS5 proxy server. It can be enabled/disabled by
- * invoking {@link #setLocalSocks5ProxyEnabled(boolean)}. The proxy is enabled by default.
- * <p>
- * The port of the local SOCKS5 proxy can be configured by invoking
- * {@link #setLocalSocks5ProxyPort(int)}. Default port is 7777. If you set the port to a negative
- * value Smack tries to the absolute value and all following until it finds an open port.
- * <p>
- * If your application is running on a machine with multiple network interfaces or if you want to
- * provide your public address in case you are behind a NAT router, invoke
- * {@link #addLocalAddress(InetAddress)} or {@link #replaceLocalAddresses(Collection)} to modify the list of
- * local network addresses used for outgoing SOCKS5 Bytestream requests.
- * <p>
- * The local SOCKS5 proxy server refuses all connections except the ones that are explicitly allowed
- * in the process of establishing a SOCKS5 Bytestream (
- * {@link Socks5BytestreamManager#establishSession(org.jxmpp.jid.Jid)}).
- * <p>
- * This Implementation has the following limitations:
- * <ul>
- * <li>only supports the no-authentication authentication method</li>
- * <li>only supports the <code>connect</code> command and will not answer correctly to other
- * commands</li>
- * <li>only supports requests with the domain address type and will not correctly answer to requests
- * with other address types</li>
- * </ul>
- * (see <a href="http://tools.ietf.org/html/rfc1928">RFC 1928</a>)
- *
- * @author Henning Staib
- */
- public class Socks5Proxy {
- private static final Logger LOGGER = Logger.getLogger(Socks5Proxy.class.getName());
- private static final List<Socks5Proxy> RUNNING_PROXIES = new CopyOnWriteArrayList<>();
- /* SOCKS5 proxy singleton */
- private static Socks5Proxy socks5Server;
- private static boolean localSocks5ProxyEnabled = true;
- /**
- * The default port of the local Socks5 Proxy. If this value is negative, the next ports will be tried
- * until a unused is found.
- */
- private static int DEFAULT_LOCAL_SOCKS5_PROXY_PORT = -7777;
- /**
- * The port of the local Socks5 Proxy. If this value is negative, the next ports will be tried
- * until a unused is found.
- */
- private int localSocks5ProxyPort = -7777;
- /* reusable implementation of a SOCKS5 proxy server process */
- private final Socks5ServerProcess serverProcess;
- /* thread running the SOCKS5 server process */
- private Thread serverThread;
- /* server socket to accept SOCKS5 connections */
- private ServerSocket serverSocket;
- /* assigns a connection to a digest */
- private final Map<String, Socket> connectionMap = new ConcurrentHashMap<>();
- /* list of digests connections should be stored */
- private final List<String> allowedConnections = Collections.synchronizedList(new LinkedList<String>());
- 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();
- } catch (SocketException e) {
- throw new IllegalStateException(e);
- }
- Set<InetAddress> localAddresses = new HashSet<>();
- for (NetworkInterface networkInterface : Collections.list(networkInterfaces)) {
- List<InterfaceAddress> interfaceAddresses = networkInterface.getInterfaceAddresses();
- for (InterfaceAddress interfaceAddress : interfaceAddresses) {
- localAddresses.add(interfaceAddress.getAddress());
- }
- }
- if (localAddresses.isEmpty()) {
- throw new IllegalStateException("Could not determine any local internet address");
- }
- 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.
- *
- * @return if the local Socks5 proxy should be started
- */
- public static boolean isLocalSocks5ProxyEnabled() {
- return localSocks5ProxyEnabled;
- }
- /**
- * Sets if the local Socks5 proxy should be started. Default is true.
- *
- * @param localSocks5ProxyEnabled if the local Socks5 proxy should be started
- */
- public static void setLocalSocks5ProxyEnabled(boolean localSocks5ProxyEnabled) {
- Socks5Proxy.localSocks5ProxyEnabled = localSocks5ProxyEnabled;
- }
- private static void checkLocalSocks5ProxyPortArgument(int port) {
- if (Math.abs(port) > 65535) {
- throw new IllegalArgumentException("Local SOCKS5 proxy port must be within (-65535,65535)");
- }
- }
- public static int getDefaultLocalSocks5ProxyPort() {
- return DEFAULT_LOCAL_SOCKS5_PROXY_PORT;
- }
- public static void setDefaultLocalSocsk5ProxyPort(int defaultLocalSocks5ProxyPort) {
- checkLocalSocks5ProxyPortArgument(defaultLocalSocks5ProxyPort);
- DEFAULT_LOCAL_SOCKS5_PROXY_PORT = defaultLocalSocks5ProxyPort;
- }
- /**
- * Return the port of the local Socks5 proxy. Default is 7777.
- *
- * @return the port of the local Socks5 proxy
- */
- public int getLocalSocks5ProxyPort() {
- return localSocks5ProxyPort;
- }
- /**
- * Sets the port of the local Socks5 proxy. Default is 7777. If you set the port to a negative
- * value Smack tries the absolute value and all following until it finds an open port.
- *
- * @param localSocks5ProxyPort the port of the local Socks5 proxy to set
- */
- public void setLocalSocks5ProxyPort(int localSocks5ProxyPort) {
- checkLocalSocks5ProxyPortArgument(localSocks5ProxyPort);
- this.localSocks5ProxyPort = localSocks5ProxyPort;
- }
- /**
- * Returns the local SOCKS5 proxy server.
- *
- * @return the local SOCKS5 proxy server
- */
- public static synchronized Socks5Proxy getSocks5Proxy() {
- if (socks5Server == null) {
- socks5Server = new Socks5Proxy();
- }
- if (isLocalSocks5ProxyEnabled()) {
- socks5Server.start();
- }
- return socks5Server;
- }
- /**
- * Starts the local SOCKS5 proxy server. If it is already running, this method does nothing.
- *
- * @return the server socket.
- */
- public synchronized ServerSocket start() {
- if (isRunning()) {
- return this.serverSocket;
- }
- try {
- if (getLocalSocks5ProxyPort() < 0) {
- int port = Math.abs(getLocalSocks5ProxyPort());
- for (int i = 0; i < 65535 - port; i++) {
- try {
- this.serverSocket = new ServerSocket(port + i);
- break;
- }
- catch (IOException e) {
- // port is used, try next one
- }
- }
- }
- else {
- this.serverSocket = new ServerSocket(getLocalSocks5ProxyPort());
- }
- if (this.serverSocket != null) {
- startServerThread();
- }
- }
- catch (IOException e) {
- // couldn't setup server
- LOGGER.log(Level.SEVERE, "couldn't setup local SOCKS5 proxy on port " + getLocalSocks5ProxyPort(), e);
- }
- 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.
- */
- public synchronized void stop() {
- if (!isRunning()) {
- return;
- }
- RUNNING_PROXIES.remove(this);
- CloseableUtil.maybeClose(this.serverSocket, LOGGER);
- if (this.serverThread != null && this.serverThread.isAlive()) {
- try {
- this.serverThread.interrupt();
- this.serverThread.join();
- }
- catch (InterruptedException e) {
- LOGGER.log(Level.WARNING, "SOCKS5 server thread termination was interrupted", e);
- }
- }
- this.serverThread = null;
- this.serverSocket = null;
- }
- /**
- * Adds the given address to the list of local network addresses.
- * <p>
- * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request.
- * This may be necessary if your application is running on a machine with multiple network
- * interfaces or if you want to provide your public address in case you are behind a NAT router.
- * <p>
- * The order of the addresses used is determined by the order you add addresses.
- * <p>
- * Note that the list of addresses initially contains the address returned by
- * <code>InetAddress.getLocalHost().getHostAddress()</code>. You can replace the list of
- * addresses by invoking {@link #replaceLocalAddresses(Collection)}.
- *
- * @param address the local network address to add
- */
- public void addLocalAddress(InetAddress address) {
- if (address == null) {
- return;
- }
- synchronized (localAddresses) {
- this.localAddresses.add(address);
- }
- }
- /**
- * Removes the given address from the list of local network addresses. This address will then no
- * longer be used of outgoing SOCKS5 Bytestream requests.
- *
- * @param address the local network address to remove
- * @return true if the address was removed.
- */
- public boolean removeLocalAddress(InetAddress address) {
- synchronized (localAddresses) {
- return localAddresses.remove(address);
- }
- }
- /**
- * Returns an set of the local network addresses that will be used for streamhost
- * candidates of outgoing SOCKS5 Bytestream requests.
- *
- * @return set of the local network addresses
- */
- public List<InetAddress> getLocalAddresses() {
- synchronized (localAddresses) {
- return new LinkedList<>(localAddresses);
- }
- }
- /**
- * Replaces the list of local network addresses.
- * <p>
- * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request and
- * want to define their order. This may be necessary if your application is running on a machine
- * with multiple network interfaces or if you want to provide your public address in case you
- * are behind a NAT router.
- *
- * @param addresses the new list of local network addresses
- */
- public void replaceLocalAddresses(Collection<? extends InetAddress> addresses) {
- if (addresses == null) {
- throw new IllegalArgumentException("list must not be null");
- }
- synchronized (localAddresses) {
- localAddresses.clear();
- localAddresses.addAll(addresses);
- }
- }
- /**
- * 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. A socket will be returned if the given digest has
- * been in the list of allowed transfers (see {@link #addTransfer(String)}) while the peer
- * connected to the SOCKS5 proxy.
- *
- * @param digest identifying the connection
- * @return socket or null if there is no socket for the given digest
- */
- protected Socket getSocket(String digest) {
- return this.connectionMap.get(digest);
- }
- /**
- * Add the given digest to the list of allowed transfers. Only connections for allowed transfers
- * are stored and can be retrieved by invoking {@link #getSocket(String)}. All connections to
- * the local SOCKS5 proxy that don't contain an allowed digest are discarded.
- *
- * @param digest to be added to the list of allowed transfers
- */
- public void addTransfer(String digest) {
- this.allowedConnections.add(digest);
- }
- /**
- * Removes the given digest from the list of allowed transfers. After invoking this method
- * already stored connections with the given digest will be removed.
- * <p>
- * The digest should be removed after establishing the SOCKS5 Bytestream is finished, an error
- * occurred while establishing the connection or if the connection is not allowed anymore.
- *
- * @param digest to be removed from the list of allowed transfers
- */
- protected void removeTransfer(String digest) {
- this.allowedConnections.remove(digest);
- this.connectionMap.remove(digest);
- }
- /**
- * Returns <code>true</code> if the local SOCKS5 proxy server is running, otherwise
- * <code>false</code>.
- *
- * @return <code>true</code> if the local SOCKS5 proxy server is running, otherwise
- * <code>false</code>
- */
- public boolean isRunning() {
- return this.serverSocket != null;
- }
- /**
- * Implementation of a simplified SOCKS5 proxy server.
- */
- private class Socks5ServerProcess implements Runnable {
- @Override
- public void run() {
- while (true) {
- ServerSocket serverSocket = Socks5Proxy.this.serverSocket;
- if (serverSocket == null || serverSocket.isClosed() || Thread.currentThread().isInterrupted()) {
- return;
- }
- // accept connection
- Socket socket = null;
- try {
- socket = serverSocket.accept();
- // initialize connection
- establishConnection(socket);
- } catch (SmackException | IOException e) {
- // Do nothing, if caused by closing the server socket, thread will terminate in next loop.
- LOGGER.log(Level.FINE, "Exception while " + Socks5Proxy.this + " was handling connection", 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 SmackException, IOException {
- 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: Peer send " + b + " but we expect 5");
- }
- // 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);
- // return error if digest is not allowed
- if (!allowAllConnections && !Socks5Proxy.this.allowedConnections.contains(responseDigest)) {
- connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused)
- out.write(connectionRequest);
- out.flush();
- throw new SmackException.SmackMessageException(
- "Connection with digest '" + responseDigest + "' is not allowed");
- }
- // Store the connection before we send the return status.
- Socks5Proxy.this.connectionMap.put(responseDigest, socket);
- connectionRequest[1] = (byte) 0x00; // set return status to 0 (success)
- out.write(connectionRequest);
- out.flush();
- }
- }
- public static Socket getSocketForDigest(String digest) {
- for (Socks5Proxy socks5Proxy : RUNNING_PROXIES) {
- Socket socket = socks5Proxy.getSocket(digest);
- if (socket != null) {
- return socket;
- }
- }
- return null;
- }
- static List<Socks5Proxy> getRunningProxies() {
- return RUNNING_PROXIES;
- }
- }