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