blob: 664ea596d7415fec570cc9542e6e0015389b0be0 [file] [log] [blame]
Shuyi Chend7955ce2013-05-22 14:51:55 -07001/**
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 */
14package org.jivesoftware.smackx.bytestreams.socks5;
15
16import java.io.DataInputStream;
17import java.io.DataOutputStream;
18import java.io.IOException;
19import java.net.InetSocketAddress;
20import java.net.Socket;
21import java.net.SocketAddress;
22import java.util.Arrays;
23import java.util.concurrent.Callable;
24import java.util.concurrent.ExecutionException;
25import java.util.concurrent.FutureTask;
26import java.util.concurrent.TimeUnit;
27import java.util.concurrent.TimeoutException;
28
29import org.jivesoftware.smack.XMPPException;
30import 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 */
39class 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}