Home | History | Annotate | Download | only in socks5
      1 /**
      2  * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
      3  * you may not use this file except in compliance with the License.
      4  * You may obtain a copy of the License at
      5  *
      6  *     http://www.apache.org/licenses/LICENSE-2.0
      7  *
      8  * Unless required by applicable law or agreed to in writing, software
      9  * distributed under the License is distributed on an "AS IS" BASIS,
     10  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     11  * See the License for the specific language governing permissions and
     12  * limitations under the License.
     13  */
     14 package org.jivesoftware.smackx.bytestreams.socks5;
     15 
     16 import java.io.DataInputStream;
     17 import java.io.DataOutputStream;
     18 import java.io.IOException;
     19 import java.net.InetAddress;
     20 import java.net.ServerSocket;
     21 import java.net.Socket;
     22 import java.net.SocketException;
     23 import java.net.UnknownHostException;
     24 import java.util.ArrayList;
     25 import java.util.Collections;
     26 import java.util.LinkedHashSet;
     27 import java.util.LinkedList;
     28 import java.util.List;
     29 import java.util.Map;
     30 import java.util.Set;
     31 import java.util.concurrent.ConcurrentHashMap;
     32 
     33 import org.jivesoftware.smack.SmackConfiguration;
     34 import org.jivesoftware.smack.XMPPException;
     35 
     36 /**
     37  * The Socks5Proxy class represents a local SOCKS5 proxy server. It can be enabled/disabled by
     38  * setting the <code>localSocks5ProxyEnabled</code> flag in the <code>smack-config.xml</code> or by
     39  * invoking {@link SmackConfiguration#setLocalSocks5ProxyEnabled(boolean)}. The proxy is enabled by
     40  * default.
     41  * <p>
     42  * The port of the local SOCKS5 proxy can be configured by setting <code>localSocks5ProxyPort</code>
     43  * in the <code>smack-config.xml</code> or by invoking
     44  * {@link SmackConfiguration#setLocalSocks5ProxyPort(int)}. Default port is 7777. If you set the
     45  * port to a negative value Smack tries to the absolute value and all following until it finds an
     46  * open port.
     47  * <p>
     48  * If your application is running on a machine with multiple network interfaces or if you want to
     49  * provide your public address in case you are behind a NAT router, invoke
     50  * {@link #addLocalAddress(String)} or {@link #replaceLocalAddresses(List)} to modify the list of
     51  * local network addresses used for outgoing SOCKS5 Bytestream requests.
     52  * <p>
     53  * The local SOCKS5 proxy server refuses all connections except the ones that are explicitly allowed
     54  * in the process of establishing a SOCKS5 Bytestream (
     55  * {@link Socks5BytestreamManager#establishSession(String)}).
     56  * <p>
     57  * This Implementation has the following limitations:
     58  * <ul>
     59  * <li>only supports the no-authentication authentication method</li>
     60  * <li>only supports the <code>connect</code> command and will not answer correctly to other
     61  * commands</li>
     62  * <li>only supports requests with the domain address type and will not correctly answer to requests
     63  * with other address types</li>
     64  * </ul>
     65  * (see <a href="http://tools.ietf.org/html/rfc1928">RFC 1928</a>)
     66  *
     67  * @author Henning Staib
     68  */
     69 public class Socks5Proxy {
     70 
     71     /* SOCKS5 proxy singleton */
     72     private static Socks5Proxy socks5Server;
     73 
     74     /* reusable implementation of a SOCKS5 proxy server process */
     75     private Socks5ServerProcess serverProcess;
     76 
     77     /* thread running the SOCKS5 server process */
     78     private Thread serverThread;
     79 
     80     /* server socket to accept SOCKS5 connections */
     81     private ServerSocket serverSocket;
     82 
     83     /* assigns a connection to a digest */
     84     private final Map<String, Socket> connectionMap = new ConcurrentHashMap<String, Socket>();
     85 
     86     /* list of digests connections should be stored */
     87     private final List<String> allowedConnections = Collections.synchronizedList(new LinkedList<String>());
     88 
     89     private final Set<String> localAddresses = Collections.synchronizedSet(new LinkedHashSet<String>());
     90 
     91     /**
     92      * Private constructor.
     93      */
     94     private Socks5Proxy() {
     95         this.serverProcess = new Socks5ServerProcess();
     96 
     97         // add default local address
     98         try {
     99             this.localAddresses.add(InetAddress.getLocalHost().getHostAddress());
    100         }
    101         catch (UnknownHostException e) {
    102             // do nothing
    103         }
    104 
    105     }
    106 
    107     /**
    108      * Returns the local SOCKS5 proxy server.
    109      *
    110      * @return the local SOCKS5 proxy server
    111      */
    112     public static synchronized Socks5Proxy getSocks5Proxy() {
    113         if (socks5Server == null) {
    114             socks5Server = new Socks5Proxy();
    115         }
    116         if (SmackConfiguration.isLocalSocks5ProxyEnabled()) {
    117             socks5Server.start();
    118         }
    119         return socks5Server;
    120     }
    121 
    122     /**
    123      * Starts the local SOCKS5 proxy server. If it is already running, this method does nothing.
    124      */
    125     public synchronized void start() {
    126         if (isRunning()) {
    127             return;
    128         }
    129         try {
    130             if (SmackConfiguration.getLocalSocks5ProxyPort() < 0) {
    131                 int port = Math.abs(SmackConfiguration.getLocalSocks5ProxyPort());
    132                 for (int i = 0; i < 65535 - port; i++) {
    133                     try {
    134                         this.serverSocket = new ServerSocket(port + i);
    135                         break;
    136                     }
    137                     catch (IOException e) {
    138                         // port is used, try next one
    139                     }
    140                 }
    141             }
    142             else {
    143                 this.serverSocket = new ServerSocket(SmackConfiguration.getLocalSocks5ProxyPort());
    144             }
    145 
    146             if (this.serverSocket != null) {
    147                 this.serverThread = new Thread(this.serverProcess);
    148                 this.serverThread.start();
    149             }
    150         }
    151         catch (IOException e) {
    152             // couldn't setup server
    153             System.err.println("couldn't setup local SOCKS5 proxy on port "
    154                             + SmackConfiguration.getLocalSocks5ProxyPort() + ": " + e.getMessage());
    155         }
    156     }
    157 
    158     /**
    159      * Stops the local SOCKS5 proxy server. If it is not running this method does nothing.
    160      */
    161     public synchronized void stop() {
    162         if (!isRunning()) {
    163             return;
    164         }
    165 
    166         try {
    167             this.serverSocket.close();
    168         }
    169         catch (IOException e) {
    170             // do nothing
    171         }
    172 
    173         if (this.serverThread != null && this.serverThread.isAlive()) {
    174             try {
    175                 this.serverThread.interrupt();
    176                 this.serverThread.join();
    177             }
    178             catch (InterruptedException e) {
    179                 // do nothing
    180             }
    181         }
    182         this.serverThread = null;
    183         this.serverSocket = null;
    184 
    185     }
    186 
    187     /**
    188      * Adds the given address to the list of local network addresses.
    189      * <p>
    190      * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request.
    191      * This may be necessary if your application is running on a machine with multiple network
    192      * interfaces or if you want to provide your public address in case you are behind a NAT router.
    193      * <p>
    194      * The order of the addresses used is determined by the order you add addresses.
    195      * <p>
    196      * Note that the list of addresses initially contains the address returned by
    197      * <code>InetAddress.getLocalHost().getHostAddress()</code>. You can replace the list of
    198      * addresses by invoking {@link #replaceLocalAddresses(List)}.
    199      *
    200      * @param address the local network address to add
    201      */
    202     public void addLocalAddress(String address) {
    203         if (address == null) {
    204             throw new IllegalArgumentException("address may not be null");
    205         }
    206         this.localAddresses.add(address);
    207     }
    208 
    209     /**
    210      * Removes the given address from the list of local network addresses. This address will then no
    211      * longer be used of outgoing SOCKS5 Bytestream requests.
    212      *
    213      * @param address the local network address to remove
    214      */
    215     public void removeLocalAddress(String address) {
    216         this.localAddresses.remove(address);
    217     }
    218 
    219     /**
    220      * Returns an unmodifiable list of the local network addresses that will be used for streamhost
    221      * candidates of outgoing SOCKS5 Bytestream requests.
    222      *
    223      * @return unmodifiable list of the local network addresses
    224      */
    225     public List<String> getLocalAddresses() {
    226         return Collections.unmodifiableList(new ArrayList<String>(this.localAddresses));
    227     }
    228 
    229     /**
    230      * Replaces the list of local network addresses.
    231      * <p>
    232      * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request and
    233      * want to define their order. This may be necessary if your application is running on a machine
    234      * with multiple network interfaces or if you want to provide your public address in case you
    235      * are behind a NAT router.
    236      *
    237      * @param addresses the new list of local network addresses
    238      */
    239     public void replaceLocalAddresses(List<String> addresses) {
    240         if (addresses == null) {
    241             throw new IllegalArgumentException("list must not be null");
    242         }
    243         this.localAddresses.clear();
    244         this.localAddresses.addAll(addresses);
    245 
    246     }
    247 
    248     /**
    249      * Returns the port of the local SOCKS5 proxy server. If it is not running -1 will be returned.
    250      *
    251      * @return the port of the local SOCKS5 proxy server or -1 if proxy is not running
    252      */
    253     public int getPort() {
    254         if (!isRunning()) {
    255             return -1;
    256         }
    257         return this.serverSocket.getLocalPort();
    258     }
    259 
    260     /**
    261      * Returns the socket for the given digest. A socket will be returned if the given digest has
    262      * been in the list of allowed transfers (see {@link #addTransfer(String)}) while the peer
    263      * connected to the SOCKS5 proxy.
    264      *
    265      * @param digest identifying the connection
    266      * @return socket or null if there is no socket for the given digest
    267      */
    268     protected Socket getSocket(String digest) {
    269         return this.connectionMap.get(digest);
    270     }
    271 
    272     /**
    273      * Add the given digest to the list of allowed transfers. Only connections for allowed transfers
    274      * are stored and can be retrieved by invoking {@link #getSocket(String)}. All connections to
    275      * the local SOCKS5 proxy that don't contain an allowed digest are discarded.
    276      *
    277      * @param digest to be added to the list of allowed transfers
    278      */
    279     protected void addTransfer(String digest) {
    280         this.allowedConnections.add(digest);
    281     }
    282 
    283     /**
    284      * Removes the given digest from the list of allowed transfers. After invoking this method
    285      * already stored connections with the given digest will be removed.
    286      * <p>
    287      * The digest should be removed after establishing the SOCKS5 Bytestream is finished, an error
    288      * occurred while establishing the connection or if the connection is not allowed anymore.
    289      *
    290      * @param digest to be removed from the list of allowed transfers
    291      */
    292     protected void removeTransfer(String digest) {
    293         this.allowedConnections.remove(digest);
    294         this.connectionMap.remove(digest);
    295     }
    296 
    297     /**
    298      * Returns <code>true</code> if the local SOCKS5 proxy server is running, otherwise
    299      * <code>false</code>.
    300      *
    301      * @return <code>true</code> if the local SOCKS5 proxy server is running, otherwise
    302      *         <code>false</code>
    303      */
    304     public boolean isRunning() {
    305         return this.serverSocket != null;
    306     }
    307 
    308     /**
    309      * Implementation of a simplified SOCKS5 proxy server.
    310      */
    311     private class Socks5ServerProcess implements Runnable {
    312 
    313         public void run() {
    314             while (true) {
    315                 Socket socket = null;
    316 
    317                 try {
    318 
    319                     if (Socks5Proxy.this.serverSocket.isClosed()
    320                                     || Thread.currentThread().isInterrupted()) {
    321                         return;
    322                     }
    323 
    324                     // accept connection
    325                     socket = Socks5Proxy.this.serverSocket.accept();
    326 
    327                     // initialize connection
    328                     establishConnection(socket);
    329 
    330                 }
    331                 catch (SocketException e) {
    332                     /*
    333                      * do nothing, if caused by closing the server socket, thread will terminate in
    334                      * next loop
    335                      */
    336                 }
    337                 catch (Exception e) {
    338                     try {
    339                         if (socket != null) {
    340                             socket.close();
    341                         }
    342                     }
    343                     catch (IOException e1) {
    344                         /* do nothing */
    345                     }
    346                 }
    347             }
    348 
    349         }
    350 
    351         /**
    352          * Negotiates a SOCKS5 connection and stores it on success.
    353          *
    354          * @param socket connection to the client
    355          * @throws XMPPException if client requests a connection in an unsupported way
    356          * @throws IOException if a network error occurred
    357          */
    358         private void establishConnection(Socket socket) throws XMPPException, IOException {
    359             DataOutputStream out = new DataOutputStream(socket.getOutputStream());
    360             DataInputStream in = new DataInputStream(socket.getInputStream());
    361 
    362             // first byte is version should be 5
    363             int b = in.read();
    364             if (b != 5) {
    365                 throw new XMPPException("Only SOCKS5 supported");
    366             }
    367 
    368             // second byte number of authentication methods supported
    369             b = in.read();
    370 
    371             // read list of supported authentication methods
    372             byte[] auth = new byte[b];
    373             in.readFully(auth);
    374 
    375             byte[] authMethodSelectionResponse = new byte[2];
    376             authMethodSelectionResponse[0] = (byte) 0x05; // protocol version
    377 
    378             // only authentication method 0, no authentication, supported
    379             boolean noAuthMethodFound = false;
    380             for (int i = 0; i < auth.length; i++) {
    381                 if (auth[i] == (byte) 0x00) {
    382                     noAuthMethodFound = true;
    383                     break;
    384                 }
    385             }
    386 
    387             if (!noAuthMethodFound) {
    388                 authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods
    389                 out.write(authMethodSelectionResponse);
    390                 out.flush();
    391                 throw new XMPPException("Authentication method not supported");
    392             }
    393 
    394             authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method
    395             out.write(authMethodSelectionResponse);
    396             out.flush();
    397 
    398             // receive connection request
    399             byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in);
    400 
    401             // extract digest
    402             String responseDigest = new String(connectionRequest, 5, connectionRequest[4]);
    403 
    404             // return error if digest is not allowed
    405             if (!Socks5Proxy.this.allowedConnections.contains(responseDigest)) {
    406                 connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused)
    407                 out.write(connectionRequest);
    408                 out.flush();
    409 
    410                 throw new XMPPException("Connection is not allowed");
    411             }
    412 
    413             connectionRequest[1] = (byte) 0x00; // set return status to 0 (success)
    414             out.write(connectionRequest);
    415             out.flush();
    416 
    417             // store connection
    418             Socks5Proxy.this.connectionMap.put(responseDigest, socket);
    419         }
    420 
    421     }
    422 
    423 }
    424