Socks5Proxy.java

  1. /**
  2.  *
  3.  * Copyright the original author or authors
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smackx.bytestreams.socks5;

  18. import java.io.DataInputStream;
  19. import java.io.DataOutputStream;
  20. import java.io.IOException;
  21. import java.net.InetAddress;
  22. import java.net.NetworkInterface;
  23. import java.net.ServerSocket;
  24. import java.net.Socket;
  25. import java.net.SocketException;
  26. import java.util.Collection;
  27. import java.util.Collections;
  28. import java.util.Enumeration;
  29. import java.util.HashSet;
  30. import java.util.LinkedHashSet;
  31. import java.util.LinkedList;
  32. import java.util.List;
  33. import java.util.Map;
  34. import java.util.Set;
  35. import java.util.concurrent.ConcurrentHashMap;
  36. import java.util.logging.Level;
  37. import java.util.logging.Logger;

  38. import org.jivesoftware.smack.SmackException;

  39. /**
  40.  * The Socks5Proxy class represents a local SOCKS5 proxy server. It can be enabled/disabled by
  41.  * invoking {@link #setLocalSocks5ProxyEnabled(boolean)}. The proxy is enabled by default.
  42.  * <p>
  43.  * The port of the local SOCKS5 proxy can be configured by invoking
  44.  * {@link #setLocalSocks5ProxyPort(int)}. Default port is 7777. If you set the port to a negative
  45.  * value Smack tries to the absolute value and all following until it finds an open port.
  46.  * <p>
  47.  * If your application is running on a machine with multiple network interfaces or if you want to
  48.  * provide your public address in case you are behind a NAT router, invoke
  49.  * {@link #addLocalAddress(String)} or {@link #replaceLocalAddresses(Collection)} to modify the list of
  50.  * local network addresses used for outgoing SOCKS5 Bytestream requests.
  51.  * <p>
  52.  * The local SOCKS5 proxy server refuses all connections except the ones that are explicitly allowed
  53.  * in the process of establishing a SOCKS5 Bytestream (
  54.  * {@link Socks5BytestreamManager#establishSession(org.jxmpp.jid.Jid)}).
  55.  * <p>
  56.  * This Implementation has the following limitations:
  57.  * <ul>
  58.  * <li>only supports the no-authentication authentication method</li>
  59.  * <li>only supports the <code>connect</code> command and will not answer correctly to other
  60.  * commands</li>
  61.  * <li>only supports requests with the domain address type and will not correctly answer to requests
  62.  * with other address types</li>
  63.  * </ul>
  64.  * (see <a href="http://tools.ietf.org/html/rfc1928">RFC 1928</a>)
  65.  *
  66.  * @author Henning Staib
  67.  */
  68. public class Socks5Proxy {
  69.     private static final Logger LOGGER = Logger.getLogger(Socks5Proxy.class.getName());
  70.    
  71.     /* SOCKS5 proxy singleton */
  72.     private static Socks5Proxy socks5Server;

  73.     private static boolean localSocks5ProxyEnabled = true;

  74.     /**
  75.      * The port of the local Socks5 Proxy. If this value is negative, the next ports will be tried
  76.      * until a unused is found.
  77.      */
  78.     private static int localSocks5ProxyPort = -7777;

  79.     /* reusable implementation of a SOCKS5 proxy server process */
  80.     private Socks5ServerProcess serverProcess;

  81.     /* thread running the SOCKS5 server process */
  82.     private Thread serverThread;

  83.     /* server socket to accept SOCKS5 connections */
  84.     private ServerSocket serverSocket;

  85.     /* assigns a connection to a digest */
  86.     private final Map<String, Socket> connectionMap = new ConcurrentHashMap<String, Socket>();

  87.     /* list of digests connections should be stored */
  88.     private final List<String> allowedConnections = Collections.synchronizedList(new LinkedList<String>());

  89.     private final Set<String> localAddresses = new LinkedHashSet<String>(4);

  90.     /**
  91.      * Private constructor.
  92.      */
  93.     private Socks5Proxy() {
  94.         this.serverProcess = new Socks5ServerProcess();

  95.         Enumeration<NetworkInterface> networkInterfaces;
  96.         try {
  97.             networkInterfaces = NetworkInterface.getNetworkInterfaces();
  98.         } catch (SocketException e) {
  99.             throw new IllegalStateException(e);
  100.         }
  101.         Set<String> localHostAddresses = new HashSet<String>();
  102.         for (NetworkInterface networkInterface : Collections.list(networkInterfaces)) {
  103.             // We can't use NetworkInterface.getInterfaceAddresses here, which
  104.             // would return a List instead the deprecated Enumeration, because
  105.             // it's Android API 9 and Smack currently uses 8. Change that when
  106.             // we raise Smack's minimum Android API.
  107.             Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
  108.             for (InetAddress address : Collections.list(inetAddresses)) {
  109.                 localHostAddresses.add(address.getHostAddress());
  110.             }
  111.         }
  112.         if (localHostAddresses.isEmpty()) {
  113.             throw new IllegalStateException("Could not determine any local host address");
  114.         }
  115.         replaceLocalAddresses(localHostAddresses);
  116.     }

  117.    /**
  118.     * Returns true if the local Socks5 proxy should be started. Default is true.
  119.     *
  120.     * @return if the local Socks5 proxy should be started
  121.     */
  122.    public static boolean isLocalSocks5ProxyEnabled() {
  123.        return localSocks5ProxyEnabled;
  124.    }

  125.    /**
  126.     * Sets if the local Socks5 proxy should be started. Default is true.
  127.     *
  128.     * @param localSocks5ProxyEnabled if the local Socks5 proxy should be started
  129.     */
  130.    public static void setLocalSocks5ProxyEnabled(boolean localSocks5ProxyEnabled) {
  131.        Socks5Proxy.localSocks5ProxyEnabled = localSocks5ProxyEnabled;
  132.    }

  133.    /**
  134.     * Return the port of the local Socks5 proxy. Default is 7777.
  135.     *
  136.     * @return the port of the local Socks5 proxy
  137.     */
  138.    public static int getLocalSocks5ProxyPort() {
  139.        return localSocks5ProxyPort;
  140.    }

  141.    /**
  142.     * Sets the port of the local Socks5 proxy. Default is 7777. If you set the port to a negative
  143.     * value Smack tries the absolute value and all following until it finds an open port.
  144.     *
  145.     * @param localSocks5ProxyPort the port of the local Socks5 proxy to set
  146.     */
  147.    public static void setLocalSocks5ProxyPort(int localSocks5ProxyPort) {
  148.        if (Math.abs(localSocks5ProxyPort) > 65535) {
  149.            throw new IllegalArgumentException("localSocks5ProxyPort must be within (-65535,65535)");
  150.        }
  151.        Socks5Proxy.localSocks5ProxyPort = localSocks5ProxyPort;
  152.    }

  153.     /**
  154.      * Returns the local SOCKS5 proxy server.
  155.      *
  156.      * @return the local SOCKS5 proxy server
  157.      */
  158.     public static synchronized Socks5Proxy getSocks5Proxy() {
  159.         if (socks5Server == null) {
  160.             socks5Server = new Socks5Proxy();
  161.         }
  162.         if (isLocalSocks5ProxyEnabled()) {
  163.             socks5Server.start();
  164.         }
  165.         return socks5Server;
  166.     }

  167.     /**
  168.      * Starts the local SOCKS5 proxy server. If it is already running, this method does nothing.
  169.      */
  170.     public synchronized void start() {
  171.         if (isRunning()) {
  172.             return;
  173.         }
  174.         try {
  175.             if (getLocalSocks5ProxyPort() < 0) {
  176.                 int port = Math.abs(getLocalSocks5ProxyPort());
  177.                 for (int i = 0; i < 65535 - port; i++) {
  178.                     try {
  179.                         this.serverSocket = new ServerSocket(port + i);
  180.                         break;
  181.                     }
  182.                     catch (IOException e) {
  183.                         // port is used, try next one
  184.                     }
  185.                 }
  186.             }
  187.             else {
  188.                 this.serverSocket = new ServerSocket(getLocalSocks5ProxyPort());
  189.             }

  190.             if (this.serverSocket != null) {
  191.                 this.serverThread = new Thread(this.serverProcess);
  192.                 this.serverThread.start();
  193.             }
  194.         }
  195.         catch (IOException e) {
  196.             // couldn't setup server
  197.             LOGGER.log(Level.SEVERE, "couldn't setup local SOCKS5 proxy on port " + getLocalSocks5ProxyPort(), e);
  198.         }
  199.     }

  200.     /**
  201.      * Stops the local SOCKS5 proxy server. If it is not running this method does nothing.
  202.      */
  203.     public synchronized void stop() {
  204.         if (!isRunning()) {
  205.             return;
  206.         }

  207.         try {
  208.             this.serverSocket.close();
  209.         }
  210.         catch (IOException e) {
  211.             // do nothing
  212.         }

  213.         if (this.serverThread != null && this.serverThread.isAlive()) {
  214.             try {
  215.                 this.serverThread.interrupt();
  216.                 this.serverThread.join();
  217.             }
  218.             catch (InterruptedException e) {
  219.                 // do nothing
  220.             }
  221.         }
  222.         this.serverThread = null;
  223.         this.serverSocket = null;

  224.     }

  225.     /**
  226.      * Adds the given address to the list of local network addresses.
  227.      * <p>
  228.      * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request.
  229.      * This may be necessary if your application is running on a machine with multiple network
  230.      * interfaces or if you want to provide your public address in case you are behind a NAT router.
  231.      * <p>
  232.      * The order of the addresses used is determined by the order you add addresses.
  233.      * <p>
  234.      * Note that the list of addresses initially contains the address returned by
  235.      * <code>InetAddress.getLocalHost().getHostAddress()</code>. You can replace the list of
  236.      * addresses by invoking {@link #replaceLocalAddresses(Collection)}.
  237.      *
  238.      * @param address the local network address to add
  239.      */
  240.     public void addLocalAddress(String address) {
  241.         if (address == null) {
  242.             return;
  243.         }
  244.         synchronized (localAddresses) {
  245.             this.localAddresses.add(address);
  246.         }
  247.     }

  248.     /**
  249.      * Removes the given address from the list of local network addresses. This address will then no
  250.      * longer be used of outgoing SOCKS5 Bytestream requests.
  251.      *
  252.      * @param address the local network address to remove
  253.      * @return true if the address was removed.
  254.      */
  255.     public boolean removeLocalAddress(String address) {
  256.         synchronized(localAddresses) {
  257.             return localAddresses.remove(address);
  258.         }
  259.     }

  260.     /**
  261.      * Returns an set of the local network addresses that will be used for streamhost
  262.      * candidates of outgoing SOCKS5 Bytestream requests.
  263.      *
  264.      * @return set of the local network addresses
  265.      */
  266.     public List<String> getLocalAddresses() {
  267.         synchronized (localAddresses) {
  268.             return new LinkedList<String>(localAddresses);
  269.         }
  270.     }

  271.     /**
  272.      * Replaces the list of local network addresses.
  273.      * <p>
  274.      * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request and
  275.      * want to define their order. This may be necessary if your application is running on a machine
  276.      * with multiple network interfaces or if you want to provide your public address in case you
  277.      * are behind a NAT router.
  278.      *
  279.      * @param addresses the new list of local network addresses
  280.      */
  281.     public void replaceLocalAddresses(Collection<String> addresses) {
  282.         if (addresses == null) {
  283.             throw new IllegalArgumentException("list must not be null");
  284.         }
  285.         synchronized(localAddresses) {
  286.             localAddresses.clear();
  287.             localAddresses.addAll(addresses);
  288.         }
  289.     }

  290.     /**
  291.      * Returns the port of the local SOCKS5 proxy server. If it is not running -1 will be returned.
  292.      *
  293.      * @return the port of the local SOCKS5 proxy server or -1 if proxy is not running
  294.      */
  295.     public int getPort() {
  296.         if (!isRunning()) {
  297.             return -1;
  298.         }
  299.         return this.serverSocket.getLocalPort();
  300.     }

  301.     /**
  302.      * Returns the socket for the given digest. A socket will be returned if the given digest has
  303.      * been in the list of allowed transfers (see {@link #addTransfer(String)}) while the peer
  304.      * connected to the SOCKS5 proxy.
  305.      *
  306.      * @param digest identifying the connection
  307.      * @return socket or null if there is no socket for the given digest
  308.      */
  309.     protected Socket getSocket(String digest) {
  310.         return this.connectionMap.get(digest);
  311.     }

  312.     /**
  313.      * Add the given digest to the list of allowed transfers. Only connections for allowed transfers
  314.      * are stored and can be retrieved by invoking {@link #getSocket(String)}. All connections to
  315.      * the local SOCKS5 proxy that don't contain an allowed digest are discarded.
  316.      *
  317.      * @param digest to be added to the list of allowed transfers
  318.      */
  319.     protected void addTransfer(String digest) {
  320.         this.allowedConnections.add(digest);
  321.     }

  322.     /**
  323.      * Removes the given digest from the list of allowed transfers. After invoking this method
  324.      * already stored connections with the given digest will be removed.
  325.      * <p>
  326.      * The digest should be removed after establishing the SOCKS5 Bytestream is finished, an error
  327.      * occurred while establishing the connection or if the connection is not allowed anymore.
  328.      *
  329.      * @param digest to be removed from the list of allowed transfers
  330.      */
  331.     protected void removeTransfer(String digest) {
  332.         this.allowedConnections.remove(digest);
  333.         this.connectionMap.remove(digest);
  334.     }

  335.     /**
  336.      * Returns <code>true</code> if the local SOCKS5 proxy server is running, otherwise
  337.      * <code>false</code>.
  338.      *
  339.      * @return <code>true</code> if the local SOCKS5 proxy server is running, otherwise
  340.      *         <code>false</code>
  341.      */
  342.     public boolean isRunning() {
  343.         return this.serverSocket != null;
  344.     }

  345.     /**
  346.      * Implementation of a simplified SOCKS5 proxy server.
  347.      */
  348.     private class Socks5ServerProcess implements Runnable {

  349.         public void run() {
  350.             while (true) {
  351.                 Socket socket = null;

  352.                 try {

  353.                     if (Socks5Proxy.this.serverSocket.isClosed()
  354.                                     || Thread.currentThread().isInterrupted()) {
  355.                         return;
  356.                     }

  357.                     // accept connection
  358.                     socket = Socks5Proxy.this.serverSocket.accept();

  359.                     // initialize connection
  360.                     establishConnection(socket);

  361.                 }
  362.                 catch (SocketException e) {
  363.                     /*
  364.                      * do nothing, if caused by closing the server socket, thread will terminate in
  365.                      * next loop
  366.                      */
  367.                 }
  368.                 catch (Exception e) {
  369.                     try {
  370.                         if (socket != null) {
  371.                             socket.close();
  372.                         }
  373.                     }
  374.                     catch (IOException e1) {
  375.                         /* do nothing */
  376.                     }
  377.                 }
  378.             }

  379.         }

  380.         /**
  381.          * Negotiates a SOCKS5 connection and stores it on success.
  382.          *
  383.          * @param socket connection to the client
  384.          * @throws SmackException if client requests a connection in an unsupported way
  385.          * @throws IOException if a network error occurred
  386.          */
  387.         private void establishConnection(Socket socket) throws SmackException, IOException {
  388.             DataOutputStream out = new DataOutputStream(socket.getOutputStream());
  389.             DataInputStream in = new DataInputStream(socket.getInputStream());

  390.             // first byte is version should be 5
  391.             int b = in.read();
  392.             if (b != 5) {
  393.                 throw new SmackException("Only SOCKS5 supported");
  394.             }

  395.             // second byte number of authentication methods supported
  396.             b = in.read();

  397.             // read list of supported authentication methods
  398.             byte[] auth = new byte[b];
  399.             in.readFully(auth);

  400.             byte[] authMethodSelectionResponse = new byte[2];
  401.             authMethodSelectionResponse[0] = (byte) 0x05; // protocol version

  402.             // only authentication method 0, no authentication, supported
  403.             boolean noAuthMethodFound = false;
  404.             for (int i = 0; i < auth.length; i++) {
  405.                 if (auth[i] == (byte) 0x00) {
  406.                     noAuthMethodFound = true;
  407.                     break;
  408.                 }
  409.             }

  410.             if (!noAuthMethodFound) {
  411.                 authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods
  412.                 out.write(authMethodSelectionResponse);
  413.                 out.flush();
  414.                 throw new SmackException("Authentication method not supported");
  415.             }

  416.             authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method
  417.             out.write(authMethodSelectionResponse);
  418.             out.flush();

  419.             // receive connection request
  420.             byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in);

  421.             // extract digest
  422.             String responseDigest = new String(connectionRequest, 5, connectionRequest[4]);

  423.             // return error if digest is not allowed
  424.             if (!Socks5Proxy.this.allowedConnections.contains(responseDigest)) {
  425.                 connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused)
  426.                 out.write(connectionRequest);
  427.                 out.flush();

  428.                 throw new SmackException("Connection is not allowed");
  429.             }

  430.             connectionRequest[1] = (byte) 0x00; // set return status to 0 (success)
  431.             out.write(connectionRequest);
  432.             out.flush();

  433.             // store connection
  434.             Socks5Proxy.this.connectionMap.put(responseDigest, socket);
  435.         }

  436.     }

  437. }