001/**
002 *
003 * Copyright the original author or authors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.bytestreams.socks5;
018
019import java.io.DataInputStream;
020import java.io.DataOutputStream;
021import java.io.IOException;
022import java.io.UnsupportedEncodingException;
023import java.net.InetSocketAddress;
024import java.net.Socket;
025import java.net.SocketAddress;
026import java.util.Arrays;
027import java.util.concurrent.Callable;
028import java.util.concurrent.ExecutionException;
029import java.util.concurrent.FutureTask;
030import java.util.concurrent.TimeUnit;
031import java.util.concurrent.TimeoutException;
032import java.util.logging.Level;
033import java.util.logging.Logger;
034
035import org.jivesoftware.smack.SmackException;
036import org.jivesoftware.smack.XMPPException;
037import org.jivesoftware.smack.util.StringUtils;
038import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
039
040/**
041 * The SOCKS5 client class handles establishing a connection to a SOCKS5 proxy. Connecting to a
042 * SOCKS5 proxy requires authentication. This implementation only supports the no-authentication
043 * authentication method.
044 *
045 * @author Henning Staib
046 */
047public class Socks5Client {
048
049    private static final Logger LOGGER = Logger.getLogger(Socks5Client.class.getName());
050
051    /* stream host containing network settings and name of the SOCKS5 proxy */
052    protected StreamHost streamHost;
053
054    /* SHA-1 digest identifying the SOCKS5 stream */
055    protected String digest;
056
057    /**
058     * Constructor for a SOCKS5 client.
059     *
060     * @param streamHost containing network settings of the SOCKS5 proxy
061     * @param digest identifying the SOCKS5 Bytestream
062     */
063    public Socks5Client(StreamHost streamHost, String digest) {
064        this.streamHost = streamHost;
065        this.digest = digest;
066    }
067
068    /**
069     * Returns the initialized socket that can be used to transfer data between peers via the SOCKS5
070     * proxy.
071     *
072     * @param timeout timeout to connect to SOCKS5 proxy in milliseconds
073     * @return socket the initialized socket
074     * @throws IOException if initializing the socket failed due to a network error
075     * @throws TimeoutException if connecting to SOCKS5 proxy timed out
076     * @throws InterruptedException if the current thread was interrupted while waiting
077     * @throws SmackException if the connection to the SOCKS5 proxy failed
078     * @throws XMPPException
079     */
080    public Socket getSocket(int timeout) throws IOException, InterruptedException,
081                    TimeoutException, SmackException, XMPPException {
082        // wrap connecting in future for timeout
083        FutureTask<Socket> futureTask = new FutureTask<>(new Callable<Socket>() {
084
085            @Override
086            public Socket call() throws IOException, SmackException {
087
088                // initialize socket
089                Socket socket = new Socket();
090                SocketAddress socketAddress = new InetSocketAddress(streamHost.getAddress(),
091                                streamHost.getPort());
092                socket.connect(socketAddress);
093
094                // initialize connection to SOCKS5 proxy
095                try {
096                    establish(socket);
097                }
098                catch (SmackException e) {
099                    if (!socket.isClosed()) {
100                        try {
101                            socket.close();
102                        } catch (IOException e2) {
103                            LOGGER.log(Level.WARNING, "Could not close SOCKS5 socket", e2);
104                        }
105                    }
106                    throw e;
107                }
108
109                return socket;
110            }
111
112        });
113        Thread executor = new Thread(futureTask);
114        executor.start();
115
116        // get connection to initiator with timeout
117        try {
118            return futureTask.get(timeout, TimeUnit.MILLISECONDS);
119        }
120        catch (ExecutionException e) {
121            Throwable cause = e.getCause();
122            if (cause != null) {
123                // case exceptions to comply with method signature
124                if (cause instanceof IOException) {
125                    throw (IOException) cause;
126                }
127                if (cause instanceof SmackException) {
128                    throw (SmackException) cause;
129                }
130            }
131
132            // throw generic Smack exception if unexpected exception was thrown
133            throw new SmackException("Error while connecting to SOCKS5 proxy", e);
134        }
135
136    }
137
138    /**
139     * Initializes the connection to the SOCKS5 proxy by negotiating authentication method and
140     * requesting a stream for the given digest. Currently only the no-authentication method is
141     * supported by the Socks5Client.
142     *
143     * @param socket connected to a SOCKS5 proxy
144     * @throws SmackException
145     * @throws IOException
146     */
147    protected void establish(Socket socket) throws SmackException, IOException {
148
149        byte[] connectionRequest;
150        byte[] connectionResponse;
151        /*
152         * use DataInputStream/DataOutpuStream to assure read and write is completed in a single
153         * statement
154         */
155        DataInputStream in = new DataInputStream(socket.getInputStream());
156        DataOutputStream out = new DataOutputStream(socket.getOutputStream());
157
158        // authentication negotiation
159        byte[] cmd = new byte[3];
160
161        cmd[0] = (byte) 0x05; // protocol version 5
162        cmd[1] = (byte) 0x01; // number of authentication methods supported
163        cmd[2] = (byte) 0x00; // authentication method: no-authentication required
164
165        out.write(cmd);
166        out.flush();
167
168        byte[] response = new byte[2];
169        in.readFully(response);
170
171        // check if server responded with correct version and no-authentication method
172        if (response[0] != (byte) 0x05 || response[1] != (byte) 0x00) {
173            throw new SmackException("Remote SOCKS5 server responded with unexpected version: " + response[0] + ' ' + response[1] + ". Should be 0x05 0x00.");
174        }
175
176        // request SOCKS5 connection with given address/digest
177        connectionRequest = createSocks5ConnectRequest();
178        out.write(connectionRequest);
179        out.flush();
180
181        // receive response
182        connectionResponse = Socks5Utils.receiveSocks5Message(in);
183
184        // verify response
185        connectionRequest[1] = (byte) 0x00; // set expected return status to 0
186        if (!Arrays.equals(connectionRequest, connectionResponse)) {
187            throw new SmackException(
188                            "Connection request does not equal connection response. Response: "
189                                            + Arrays.toString(connectionResponse) + ". Request: "
190                                            + Arrays.toString(connectionRequest));
191        }
192    }
193
194    /**
195     * Returns a SOCKS5 connection request message. It contains the command "connect", the address
196     * type "domain" and the digest as address.
197     * <p>
198     * (see <a href="http://tools.ietf.org/html/rfc1928">RFC1928</a>)
199     *
200     * @return SOCKS5 connection request message
201     */
202    private byte[] createSocks5ConnectRequest() {
203        byte[] addr;
204        try {
205            addr = digest.getBytes(StringUtils.UTF8);
206        }
207        catch (UnsupportedEncodingException e) {
208            throw new AssertionError(e);
209        }
210
211        byte[] data = new byte[7 + addr.length];
212        data[0] = (byte) 0x05; // version (SOCKS5)
213        data[1] = (byte) 0x01; // command (1 - connect)
214        data[2] = (byte) 0x00; // reserved byte (always 0)
215        data[3] = (byte) 0x03; // address type (3 - domain name)
216        data[4] = (byte) addr.length; // address length
217        System.arraycopy(addr, 0, data, 5, addr.length); // address
218        data[data.length - 2] = (byte) 0; // address port (2 bytes always 0)
219        data[data.length - 1] = (byte) 0;
220
221        return data;
222    }
223
224}