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.InetSocketAddress;
     20 import java.net.Socket;
     21 import java.net.SocketAddress;
     22 import java.util.Arrays;
     23 import java.util.concurrent.Callable;
     24 import java.util.concurrent.ExecutionException;
     25 import java.util.concurrent.FutureTask;
     26 import java.util.concurrent.TimeUnit;
     27 import java.util.concurrent.TimeoutException;
     28 
     29 import org.jivesoftware.smack.XMPPException;
     30 import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
     31 
     32 /**
     33  * The SOCKS5 client class handles establishing a connection to a SOCKS5 proxy. Connecting to a
     34  * SOCKS5 proxy requires authentication. This implementation only supports the no-authentication
     35  * authentication method.
     36  *
     37  * @author Henning Staib
     38  */
     39 class Socks5Client {
     40 
     41     /* stream host containing network settings and name of the SOCKS5 proxy */
     42     protected StreamHost streamHost;
     43 
     44     /* SHA-1 digest identifying the SOCKS5 stream */
     45     protected String digest;
     46 
     47     /**
     48      * Constructor for a SOCKS5 client.
     49      *
     50      * @param streamHost containing network settings of the SOCKS5 proxy
     51      * @param digest identifying the SOCKS5 Bytestream
     52      */
     53     public Socks5Client(StreamHost streamHost, String digest) {
     54         this.streamHost = streamHost;
     55         this.digest = digest;
     56     }
     57 
     58     /**
     59      * Returns the initialized socket that can be used to transfer data between peers via the SOCKS5
     60      * proxy.
     61      *
     62      * @param timeout timeout to connect to SOCKS5 proxy in milliseconds
     63      * @return socket the initialized socket
     64      * @throws IOException if initializing the socket failed due to a network error
     65      * @throws XMPPException if establishing connection to SOCKS5 proxy failed
     66      * @throws TimeoutException if connecting to SOCKS5 proxy timed out
     67      * @throws InterruptedException if the current thread was interrupted while waiting
     68      */
     69     public Socket getSocket(int timeout) throws IOException, XMPPException, InterruptedException,
     70                     TimeoutException {
     71 
     72         // wrap connecting in future for timeout
     73         FutureTask<Socket> futureTask = new FutureTask<Socket>(new Callable<Socket>() {
     74 
     75             public Socket call() throws Exception {
     76 
     77                 // initialize socket
     78                 Socket socket = new Socket();
     79                 SocketAddress socketAddress = new InetSocketAddress(streamHost.getAddress(),
     80                                 streamHost.getPort());
     81                 socket.connect(socketAddress);
     82 
     83                 // initialize connection to SOCKS5 proxy
     84                 if (!establish(socket)) {
     85 
     86                     // initialization failed, close socket
     87                     socket.close();
     88                     throw new XMPPException("establishing connection to SOCKS5 proxy failed");
     89 
     90                 }
     91 
     92                 return socket;
     93             }
     94 
     95         });
     96         Thread executor = new Thread(futureTask);
     97         executor.start();
     98 
     99         // get connection to initiator with timeout
    100         try {
    101             return futureTask.get(timeout, TimeUnit.MILLISECONDS);
    102         }
    103         catch (ExecutionException e) {
    104             Throwable cause = e.getCause();
    105             if (cause != null) {
    106                 // case exceptions to comply with method signature
    107                 if (cause instanceof IOException) {
    108                     throw (IOException) cause;
    109                 }
    110                 if (cause instanceof XMPPException) {
    111                     throw (XMPPException) cause;
    112                 }
    113             }
    114 
    115             // throw generic IO exception if unexpected exception was thrown
    116             throw new IOException("Error while connection to SOCKS5 proxy");
    117         }
    118 
    119     }
    120 
    121     /**
    122      * Initializes the connection to the SOCKS5 proxy by negotiating authentication method and
    123      * requesting a stream for the given digest. Currently only the no-authentication method is
    124      * supported by the Socks5Client.
    125      * <p>
    126      * Returns <code>true</code> if a stream could be established, otherwise <code>false</code>. If
    127      * <code>false</code> is returned the given Socket should be closed.
    128      *
    129      * @param socket connected to a SOCKS5 proxy
    130      * @return <code>true</code> if if a stream could be established, otherwise <code>false</code>.
    131      *         If <code>false</code> is returned the given Socket should be closed.
    132      * @throws IOException if a network error occurred
    133      */
    134     protected boolean establish(Socket socket) throws IOException {
    135 
    136         /*
    137          * use DataInputStream/DataOutpuStream to assure read and write is completed in a single
    138          * statement
    139          */
    140         DataInputStream in = new DataInputStream(socket.getInputStream());
    141         DataOutputStream out = new DataOutputStream(socket.getOutputStream());
    142 
    143         // authentication negotiation
    144         byte[] cmd = new byte[3];
    145 
    146         cmd[0] = (byte) 0x05; // protocol version 5
    147         cmd[1] = (byte) 0x01; // number of authentication methods supported
    148         cmd[2] = (byte) 0x00; // authentication method: no-authentication required
    149 
    150         out.write(cmd);
    151         out.flush();
    152 
    153         byte[] response = new byte[2];
    154         in.readFully(response);
    155 
    156         // check if server responded with correct version and no-authentication method
    157         if (response[0] != (byte) 0x05 || response[1] != (byte) 0x00) {
    158             return false;
    159         }
    160 
    161         // request SOCKS5 connection with given address/digest
    162         byte[] connectionRequest = createSocks5ConnectRequest();
    163         out.write(connectionRequest);
    164         out.flush();
    165 
    166         // receive response
    167         byte[] connectionResponse;
    168         try {
    169             connectionResponse = Socks5Utils.receiveSocks5Message(in);
    170         }
    171         catch (XMPPException e) {
    172             return false; // server answered in an unsupported way
    173         }
    174 
    175         // verify response
    176         connectionRequest[1] = (byte) 0x00; // set expected return status to 0
    177         return Arrays.equals(connectionRequest, connectionResponse);
    178     }
    179 
    180     /**
    181      * Returns a SOCKS5 connection request message. It contains the command "connect", the address
    182      * type "domain" and the digest as address.
    183      * <p>
    184      * (see <a href="http://tools.ietf.org/html/rfc1928">RFC1928</a>)
    185      *
    186      * @return SOCKS5 connection request message
    187      */
    188     private byte[] createSocks5ConnectRequest() {
    189         byte addr[] = this.digest.getBytes();
    190 
    191         byte[] data = new byte[7 + addr.length];
    192         data[0] = (byte) 0x05; // version (SOCKS5)
    193         data[1] = (byte) 0x01; // command (1 - connect)
    194         data[2] = (byte) 0x00; // reserved byte (always 0)
    195         data[3] = (byte) 0x03; // address type (3 - domain name)
    196         data[4] = (byte) addr.length; // address length
    197         System.arraycopy(addr, 0, data, 5, addr.length); // address
    198         data[data.length - 2] = (byte) 0; // address port (2 bytes always 0)
    199         data[data.length - 1] = (byte) 0;
    200 
    201         return data;
    202     }
    203 
    204 }
    205