Socks5Client.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.InetSocketAddress;
  22. import java.net.Socket;
  23. import java.net.SocketAddress;
  24. import java.nio.charset.StandardCharsets;
  25. import java.util.Arrays;
  26. import java.util.concurrent.Callable;
  27. import java.util.concurrent.ExecutionException;
  28. import java.util.concurrent.FutureTask;
  29. import java.util.concurrent.TimeUnit;
  30. import java.util.concurrent.TimeoutException;
  31. import java.util.logging.Logger;

  32. import org.jivesoftware.smack.SmackException;
  33. import org.jivesoftware.smack.SmackException.NoResponseException;
  34. import org.jivesoftware.smack.SmackException.NotConnectedException;
  35. import org.jivesoftware.smack.SmackException.SmackMessageException;
  36. import org.jivesoftware.smack.XMPPException;
  37. import org.jivesoftware.smack.util.Async;
  38. import org.jivesoftware.smack.util.CloseableUtil;

  39. import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;

  40. /**
  41.  * The SOCKS5 client class handles establishing a connection to a SOCKS5 proxy. Connecting to a
  42.  * SOCKS5 proxy requires authentication. This implementation only supports the no-authentication
  43.  * authentication method.
  44.  *
  45.  * @author Henning Staib
  46.  */
  47. public class Socks5Client {

  48.     private static final Logger LOGGER = Logger.getLogger(Socks5Client.class.getName());

  49.     /* stream host containing network settings and name of the SOCKS5 proxy */
  50.     protected StreamHost streamHost;

  51.     /* SHA-1 digest identifying the SOCKS5 stream */
  52.     protected String digest;

  53.     /**
  54.      * Constructor for a SOCKS5 client.
  55.      *
  56.      * @param streamHost containing network settings of the SOCKS5 proxy
  57.      * @param digest identifying the SOCKS5 Bytestream
  58.      */
  59.     public Socks5Client(StreamHost streamHost, String digest) {
  60.         this.streamHost = streamHost;
  61.         this.digest = digest;
  62.     }

  63.     /**
  64.      * Returns the initialized socket that can be used to transfer data between peers via the SOCKS5
  65.      * proxy.
  66.      *
  67.      * @param timeout timeout to connect to SOCKS5 proxy in milliseconds
  68.      * @return socket the initialized socket
  69.      * @throws IOException if initializing the socket failed due to a network error
  70.      * @throws TimeoutException if connecting to SOCKS5 proxy timed out
  71.      * @throws InterruptedException if the current thread was interrupted while waiting
  72.      * @throws XMPPException if an XMPP protocol error was received.
  73.      * @throws SmackMessageException if there was an error.
  74.      * @throws NotConnectedException if the XMPP connection is not connected.
  75.      * @throws NoResponseException if there was no response from the remote entity.
  76.      */
  77.     public Socket getSocket(int timeout) throws IOException, InterruptedException,
  78.                     TimeoutException, XMPPException, SmackMessageException, NotConnectedException, NoResponseException {
  79.         // wrap connecting in future for timeout
  80.         FutureTask<Socket> futureTask = new FutureTask<>(new Callable<Socket>() {

  81.             @Override
  82.             public Socket call() throws IOException, SmackMessageException {

  83.                 // initialize socket
  84.                 Socket socket = new Socket();
  85.                 SocketAddress socketAddress = new InetSocketAddress(streamHost.getAddress().asInetAddress(),
  86.                                 streamHost.getPort());
  87.                 socket.connect(socketAddress);

  88.                 // initialize connection to SOCKS5 proxy
  89.                 try {
  90.                     establish(socket);
  91.                 }
  92.                 catch (SmackMessageException e) {
  93.                     if (!socket.isClosed()) {
  94.                         CloseableUtil.maybeClose(socket, LOGGER);
  95.                     }
  96.                     throw e;
  97.                 }

  98.                 return socket;
  99.             }

  100.         });
  101.         Async.go(futureTask, "SOCKS5 client connecting to " + streamHost);

  102.         // get connection to initiator with timeout
  103.         try {
  104.             return futureTask.get(timeout, TimeUnit.MILLISECONDS);
  105.         }
  106.         catch (ExecutionException e) {
  107.             throw new IOException("ExecutionException while SOCKS5 client attempting to connect to " + streamHost, e);
  108.         }

  109.     }

  110.     /**
  111.      * Initializes the connection to the SOCKS5 proxy by negotiating authentication method and
  112.      * requesting a stream for the given digest. Currently only the no-authentication method is
  113.      * supported by the Socks5Client.
  114.      *
  115.      * @param socket connected to a SOCKS5 proxy
  116.      * @throws IOException if an I/O error occurred.
  117.      * @throws SmackMessageException if there was an error.
  118.      */
  119.     protected void establish(Socket socket) throws IOException, SmackMessageException {

  120.         byte[] connectionRequest;
  121.         byte[] connectionResponse;
  122.         /*
  123.          * use DataInputStream/DataOutputStream to assure read and write is completed in a single
  124.          * statement
  125.          */
  126.         DataInputStream in = new DataInputStream(socket.getInputStream());
  127.         DataOutputStream out = new DataOutputStream(socket.getOutputStream());

  128.         // authentication negotiation
  129.         byte[] cmd = new byte[3];

  130.         cmd[0] = (byte) 0x05; // protocol version 5
  131.         cmd[1] = (byte) 0x01; // number of authentication methods supported
  132.         cmd[2] = (byte) 0x00; // authentication method: no-authentication required

  133.         out.write(cmd);
  134.         out.flush();

  135.         byte[] response = new byte[2];
  136.         in.readFully(response);

  137.         // check if server responded with correct version and no-authentication method
  138.         if (response[0] != (byte) 0x05 || response[1] != (byte) 0x00) {
  139.             throw new SmackException.SmackMessageException("Remote SOCKS5 server responded with unexpected version: " + response[0] + ' ' + response[1] + ". Should be 0x05 0x00.");
  140.         }

  141.         // request SOCKS5 connection with given address/digest
  142.         connectionRequest = createSocks5ConnectRequest();
  143.         out.write(connectionRequest);
  144.         out.flush();

  145.         // receive response
  146.         connectionResponse = Socks5Utils.receiveSocks5Message(in);

  147.         // verify response
  148.         connectionRequest[1] = (byte) 0x00; // set expected return status to 0
  149.         if (!Arrays.equals(connectionRequest, connectionResponse)) {
  150.             throw new SmackException.SmackMessageException(
  151.                             "Connection request does not equal connection response. Response: "
  152.                                             + Arrays.toString(connectionResponse) + ". Request: "
  153.                                             + Arrays.toString(connectionRequest));
  154.         }
  155.     }

  156.     /**
  157.      * Returns a SOCKS5 connection request message. It contains the command "connect", the address
  158.      * type "domain" and the digest as address.
  159.      * <p>
  160.      * (see <a href="http://tools.ietf.org/html/rfc1928">RFC1928</a>)
  161.      *
  162.      * @return SOCKS5 connection request message
  163.      */
  164.     private byte[] createSocks5ConnectRequest() {
  165.         byte[] addr = digest.getBytes(StandardCharsets.UTF_8);

  166.         byte[] data = new byte[7 + addr.length];
  167.         data[0] = (byte) 0x05; // version (SOCKS5)
  168.         data[1] = (byte) 0x01; // command (1 - connect)
  169.         data[2] = (byte) 0x00; // reserved byte (always 0)
  170.         data[3] = (byte) 0x03; // address type (3 - domain name)
  171.         data[4] = (byte) addr.length; // address length
  172.         System.arraycopy(addr, 0, data, 5, addr.length); // address
  173.         data[data.length - 2] = (byte) 0; // address port (2 bytes always 0)
  174.         data[data.length - 1] = (byte) 0;

  175.         return data;
  176.     }

  177. }