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}