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}