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.net.InetSocketAddress;
023import java.net.Socket;
024import java.net.SocketAddress;
025import java.nio.charset.StandardCharsets;
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.Logger;
033
034import org.jivesoftware.smack.SmackException;
035import org.jivesoftware.smack.SmackException.NoResponseException;
036import org.jivesoftware.smack.SmackException.NotConnectedException;
037import org.jivesoftware.smack.SmackException.SmackMessageException;
038import org.jivesoftware.smack.XMPPException;
039import org.jivesoftware.smack.util.Async;
040import org.jivesoftware.smack.util.CloseableUtil;
041
042import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
043
044/**
045 * The SOCKS5 client class handles establishing a connection to a SOCKS5 proxy. Connecting to a
046 * SOCKS5 proxy requires authentication. This implementation only supports the no-authentication
047 * authentication method.
048 *
049 * @author Henning Staib
050 */
051public class Socks5Client {
052
053    private static final Logger LOGGER = Logger.getLogger(Socks5Client.class.getName());
054
055    /* stream host containing network settings and name of the SOCKS5 proxy */
056    protected StreamHost streamHost;
057
058    /* SHA-1 digest identifying the SOCKS5 stream */
059    protected String digest;
060
061    /**
062     * Constructor for a SOCKS5 client.
063     *
064     * @param streamHost containing network settings of the SOCKS5 proxy
065     * @param digest identifying the SOCKS5 Bytestream
066     */
067    public Socks5Client(StreamHost streamHost, String digest) {
068        this.streamHost = streamHost;
069        this.digest = digest;
070    }
071
072    /**
073     * Returns the initialized socket that can be used to transfer data between peers via the SOCKS5
074     * proxy.
075     *
076     * @param timeout timeout to connect to SOCKS5 proxy in milliseconds
077     * @return socket the initialized socket
078     * @throws IOException if initializing the socket failed due to a network error
079     * @throws TimeoutException if connecting to SOCKS5 proxy timed out
080     * @throws InterruptedException if the current thread was interrupted while waiting
081     * @throws XMPPException if an XMPP protocol error was received.
082     * @throws SmackMessageException if there was an error.
083     * @throws NotConnectedException if the XMPP connection is not connected.
084     * @throws NoResponseException if there was no response from the remote entity.
085     */
086    public Socket getSocket(int timeout) throws IOException, InterruptedException,
087                    TimeoutException, XMPPException, SmackMessageException, NotConnectedException, NoResponseException {
088        // wrap connecting in future for timeout
089        FutureTask<Socket> futureTask = new FutureTask<>(new Callable<Socket>() {
090
091            @Override
092            public Socket call() throws IOException, SmackMessageException {
093
094                // initialize socket
095                Socket socket = new Socket();
096                SocketAddress socketAddress = new InetSocketAddress(streamHost.getAddress().asInetAddress(),
097                                streamHost.getPort());
098                socket.connect(socketAddress);
099
100                // initialize connection to SOCKS5 proxy
101                try {
102                    establish(socket);
103                }
104                catch (SmackMessageException e) {
105                    if (!socket.isClosed()) {
106                        CloseableUtil.maybeClose(socket, LOGGER);
107                    }
108                    throw e;
109                }
110
111                return socket;
112            }
113
114        });
115        Async.go(futureTask, "SOCKS5 client connecting to " + streamHost);
116
117        // get connection to initiator with timeout
118        try {
119            return futureTask.get(timeout, TimeUnit.MILLISECONDS);
120        }
121        catch (ExecutionException e) {
122            throw new IOException("ExecutionException while SOCKS5 client attempting to connect to " + streamHost, e);
123        }
124
125    }
126
127    /**
128     * Initializes the connection to the SOCKS5 proxy by negotiating authentication method and
129     * requesting a stream for the given digest. Currently only the no-authentication method is
130     * supported by the Socks5Client.
131     *
132     * @param socket connected to a SOCKS5 proxy
133     * @throws IOException if an I/O error occurred.
134     * @throws SmackMessageException if there was an error.
135     */
136    protected void establish(Socket socket) throws IOException, SmackMessageException {
137
138        byte[] connectionRequest;
139        byte[] connectionResponse;
140        /*
141         * use DataInputStream/DataOutputStream to assure read and write is completed in a single
142         * statement
143         */
144        DataInputStream in = new DataInputStream(socket.getInputStream());
145        DataOutputStream out = new DataOutputStream(socket.getOutputStream());
146
147        // authentication negotiation
148        byte[] cmd = new byte[3];
149
150        cmd[0] = (byte) 0x05; // protocol version 5
151        cmd[1] = (byte) 0x01; // number of authentication methods supported
152        cmd[2] = (byte) 0x00; // authentication method: no-authentication required
153
154        out.write(cmd);
155        out.flush();
156
157        byte[] response = new byte[2];
158        in.readFully(response);
159
160        // check if server responded with correct version and no-authentication method
161        if (response[0] != (byte) 0x05 || response[1] != (byte) 0x00) {
162            throw new SmackException.SmackMessageException("Remote SOCKS5 server responded with unexpected version: " + response[0] + ' ' + response[1] + ". Should be 0x05 0x00.");
163        }
164
165        // request SOCKS5 connection with given address/digest
166        connectionRequest = createSocks5ConnectRequest();
167        out.write(connectionRequest);
168        out.flush();
169
170        // receive response
171        connectionResponse = Socks5Utils.receiveSocks5Message(in);
172
173        // verify response
174        connectionRequest[1] = (byte) 0x00; // set expected return status to 0
175        if (!Arrays.equals(connectionRequest, connectionResponse)) {
176            throw new SmackException.SmackMessageException(
177                            "Connection request does not equal connection response. Response: "
178                                            + Arrays.toString(connectionResponse) + ". Request: "
179                                            + Arrays.toString(connectionRequest));
180        }
181    }
182
183    /**
184     * Returns a SOCKS5 connection request message. It contains the command "connect", the address
185     * type "domain" and the digest as address.
186     * <p>
187     * (see <a href="http://tools.ietf.org/html/rfc1928">RFC1928</a>)
188     *
189     * @return SOCKS5 connection request message
190     */
191    private byte[] createSocks5ConnectRequest() {
192        byte[] addr = digest.getBytes(StandardCharsets.UTF_8);
193
194        byte[] data = new byte[7 + addr.length];
195        data[0] = (byte) 0x05; // version (SOCKS5)
196        data[1] = (byte) 0x01; // command (1 - connect)
197        data[2] = (byte) 0x00; // reserved byte (always 0)
198        data[3] = (byte) 0x03; // address type (3 - domain name)
199        data[4] = (byte) addr.length; // address length
200        System.arraycopy(addr, 0, data, 5, addr.length); // address
201        data[data.length - 2] = (byte) 0; // address port (2 bytes always 0)
202        data[data.length - 1] = (byte) 0;
203
204        return data;
205    }
206
207}