blob: 11ef7a9c59c24a9583b9f53f2635d462bff5e410 [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.InetAddress;
20import java.net.ServerSocket;
21import java.net.Socket;
22import java.net.SocketException;
23import java.net.UnknownHostException;
24import java.util.ArrayList;
25import java.util.Collections;
26import java.util.LinkedHashSet;
27import java.util.LinkedList;
28import java.util.List;
29import java.util.Map;
30import java.util.Set;
31import java.util.concurrent.ConcurrentHashMap;
32
33import org.jivesoftware.smack.SmackConfiguration;
34import org.jivesoftware.smack.XMPPException;
35
36/**
37 * The Socks5Proxy class represents a local SOCKS5 proxy server. It can be enabled/disabled by
38 * setting the <code>localSocks5ProxyEnabled</code> flag in the <code>smack-config.xml</code> or by
39 * invoking {@link SmackConfiguration#setLocalSocks5ProxyEnabled(boolean)}. The proxy is enabled by
40 * default.
41 * <p>
42 * The port of the local SOCKS5 proxy can be configured by setting <code>localSocks5ProxyPort</code>
43 * in the <code>smack-config.xml</code> or by invoking
44 * {@link SmackConfiguration#setLocalSocks5ProxyPort(int)}. Default port is 7777. If you set the
45 * port to a negative value Smack tries to the absolute value and all following until it finds an
46 * open port.
47 * <p>
48 * If your application is running on a machine with multiple network interfaces or if you want to
49 * provide your public address in case you are behind a NAT router, invoke
50 * {@link #addLocalAddress(String)} or {@link #replaceLocalAddresses(List)} to modify the list of
51 * local network addresses used for outgoing SOCKS5 Bytestream requests.
52 * <p>
53 * The local SOCKS5 proxy server refuses all connections except the ones that are explicitly allowed
54 * in the process of establishing a SOCKS5 Bytestream (
55 * {@link Socks5BytestreamManager#establishSession(String)}).
56 * <p>
57 * This Implementation has the following limitations:
58 * <ul>
59 * <li>only supports the no-authentication authentication method</li>
60 * <li>only supports the <code>connect</code> command and will not answer correctly to other
61 * commands</li>
62 * <li>only supports requests with the domain address type and will not correctly answer to requests
63 * with other address types</li>
64 * </ul>
65 * (see <a href="http://tools.ietf.org/html/rfc1928">RFC 1928</a>)
66 *
67 * @author Henning Staib
68 */
69public class Socks5Proxy {
70
71 /* SOCKS5 proxy singleton */
72 private static Socks5Proxy socks5Server;
73
74 /* reusable implementation of a SOCKS5 proxy server process */
75 private Socks5ServerProcess serverProcess;
76
77 /* thread running the SOCKS5 server process */
78 private Thread serverThread;
79
80 /* server socket to accept SOCKS5 connections */
81 private ServerSocket serverSocket;
82
83 /* assigns a connection to a digest */
84 private final Map<String, Socket> connectionMap = new ConcurrentHashMap<String, Socket>();
85
86 /* list of digests connections should be stored */
87 private final List<String> allowedConnections = Collections.synchronizedList(new LinkedList<String>());
88
89 private final Set<String> localAddresses = Collections.synchronizedSet(new LinkedHashSet<String>());
90
91 /**
92 * Private constructor.
93 */
94 private Socks5Proxy() {
95 this.serverProcess = new Socks5ServerProcess();
96
97 // add default local address
98 try {
99 this.localAddresses.add(InetAddress.getLocalHost().getHostAddress());
100 }
101 catch (UnknownHostException e) {
102 // do nothing
103 }
104
105 }
106
107 /**
108 * Returns the local SOCKS5 proxy server.
109 *
110 * @return the local SOCKS5 proxy server
111 */
112 public static synchronized Socks5Proxy getSocks5Proxy() {
113 if (socks5Server == null) {
114 socks5Server = new Socks5Proxy();
115 }
116 if (SmackConfiguration.isLocalSocks5ProxyEnabled()) {
117 socks5Server.start();
118 }
119 return socks5Server;
120 }
121
122 /**
123 * Starts the local SOCKS5 proxy server. If it is already running, this method does nothing.
124 */
125 public synchronized void start() {
126 if (isRunning()) {
127 return;
128 }
129 try {
130 if (SmackConfiguration.getLocalSocks5ProxyPort() < 0) {
131 int port = Math.abs(SmackConfiguration.getLocalSocks5ProxyPort());
132 for (int i = 0; i < 65535 - port; i++) {
133 try {
134 this.serverSocket = new ServerSocket(port + i);
135 break;
136 }
137 catch (IOException e) {
138 // port is used, try next one
139 }
140 }
141 }
142 else {
143 this.serverSocket = new ServerSocket(SmackConfiguration.getLocalSocks5ProxyPort());
144 }
145
146 if (this.serverSocket != null) {
147 this.serverThread = new Thread(this.serverProcess);
148 this.serverThread.start();
149 }
150 }
151 catch (IOException e) {
152 // couldn't setup server
153 System.err.println("couldn't setup local SOCKS5 proxy on port "
154 + SmackConfiguration.getLocalSocks5ProxyPort() + ": " + e.getMessage());
155 }
156 }
157
158 /**
159 * Stops the local SOCKS5 proxy server. If it is not running this method does nothing.
160 */
161 public synchronized void stop() {
162 if (!isRunning()) {
163 return;
164 }
165
166 try {
167 this.serverSocket.close();
168 }
169 catch (IOException e) {
170 // do nothing
171 }
172
173 if (this.serverThread != null && this.serverThread.isAlive()) {
174 try {
175 this.serverThread.interrupt();
176 this.serverThread.join();
177 }
178 catch (InterruptedException e) {
179 // do nothing
180 }
181 }
182 this.serverThread = null;
183 this.serverSocket = null;
184
185 }
186
187 /**
188 * Adds the given address to the list of local network addresses.
189 * <p>
190 * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request.
191 * This may be necessary if your application is running on a machine with multiple network
192 * interfaces or if you want to provide your public address in case you are behind a NAT router.
193 * <p>
194 * The order of the addresses used is determined by the order you add addresses.
195 * <p>
196 * Note that the list of addresses initially contains the address returned by
197 * <code>InetAddress.getLocalHost().getHostAddress()</code>. You can replace the list of
198 * addresses by invoking {@link #replaceLocalAddresses(List)}.
199 *
200 * @param address the local network address to add
201 */
202 public void addLocalAddress(String address) {
203 if (address == null) {
204 throw new IllegalArgumentException("address may not be null");
205 }
206 this.localAddresses.add(address);
207 }
208
209 /**
210 * Removes the given address from the list of local network addresses. This address will then no
211 * longer be used of outgoing SOCKS5 Bytestream requests.
212 *
213 * @param address the local network address to remove
214 */
215 public void removeLocalAddress(String address) {
216 this.localAddresses.remove(address);
217 }
218
219 /**
220 * Returns an unmodifiable list of the local network addresses that will be used for streamhost
221 * candidates of outgoing SOCKS5 Bytestream requests.
222 *
223 * @return unmodifiable list of the local network addresses
224 */
225 public List<String> getLocalAddresses() {
226 return Collections.unmodifiableList(new ArrayList<String>(this.localAddresses));
227 }
228
229 /**
230 * Replaces the list of local network addresses.
231 * <p>
232 * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request and
233 * want to define their order. This may be necessary if your application is running on a machine
234 * with multiple network interfaces or if you want to provide your public address in case you
235 * are behind a NAT router.
236 *
237 * @param addresses the new list of local network addresses
238 */
239 public void replaceLocalAddresses(List<String> addresses) {
240 if (addresses == null) {
241 throw new IllegalArgumentException("list must not be null");
242 }
243 this.localAddresses.clear();
244 this.localAddresses.addAll(addresses);
245
246 }
247
248 /**
249 * Returns the port of the local SOCKS5 proxy server. If it is not running -1 will be returned.
250 *
251 * @return the port of the local SOCKS5 proxy server or -1 if proxy is not running
252 */
253 public int getPort() {
254 if (!isRunning()) {
255 return -1;
256 }
257 return this.serverSocket.getLocalPort();
258 }
259
260 /**
261 * Returns the socket for the given digest. A socket will be returned if the given digest has
262 * been in the list of allowed transfers (see {@link #addTransfer(String)}) while the peer
263 * connected to the SOCKS5 proxy.
264 *
265 * @param digest identifying the connection
266 * @return socket or null if there is no socket for the given digest
267 */
268 protected Socket getSocket(String digest) {
269 return this.connectionMap.get(digest);
270 }
271
272 /**
273 * Add the given digest to the list of allowed transfers. Only connections for allowed transfers
274 * are stored and can be retrieved by invoking {@link #getSocket(String)}. All connections to
275 * the local SOCKS5 proxy that don't contain an allowed digest are discarded.
276 *
277 * @param digest to be added to the list of allowed transfers
278 */
279 protected void addTransfer(String digest) {
280 this.allowedConnections.add(digest);
281 }
282
283 /**
284 * Removes the given digest from the list of allowed transfers. After invoking this method
285 * already stored connections with the given digest will be removed.
286 * <p>
287 * The digest should be removed after establishing the SOCKS5 Bytestream is finished, an error
288 * occurred while establishing the connection or if the connection is not allowed anymore.
289 *
290 * @param digest to be removed from the list of allowed transfers
291 */
292 protected void removeTransfer(String digest) {
293 this.allowedConnections.remove(digest);
294 this.connectionMap.remove(digest);
295 }
296
297 /**
298 * Returns <code>true</code> if the local SOCKS5 proxy server is running, otherwise
299 * <code>false</code>.
300 *
301 * @return <code>true</code> if the local SOCKS5 proxy server is running, otherwise
302 * <code>false</code>
303 */
304 public boolean isRunning() {
305 return this.serverSocket != null;
306 }
307
308 /**
309 * Implementation of a simplified SOCKS5 proxy server.
310 */
311 private class Socks5ServerProcess implements Runnable {
312
313 public void run() {
314 while (true) {
315 Socket socket = null;
316
317 try {
318
319 if (Socks5Proxy.this.serverSocket.isClosed()
320 || Thread.currentThread().isInterrupted()) {
321 return;
322 }
323
324 // accept connection
325 socket = Socks5Proxy.this.serverSocket.accept();
326
327 // initialize connection
328 establishConnection(socket);
329
330 }
331 catch (SocketException e) {
332 /*
333 * do nothing, if caused by closing the server socket, thread will terminate in
334 * next loop
335 */
336 }
337 catch (Exception e) {
338 try {
339 if (socket != null) {
340 socket.close();
341 }
342 }
343 catch (IOException e1) {
344 /* do nothing */
345 }
346 }
347 }
348
349 }
350
351 /**
352 * Negotiates a SOCKS5 connection and stores it on success.
353 *
354 * @param socket connection to the client
355 * @throws XMPPException if client requests a connection in an unsupported way
356 * @throws IOException if a network error occurred
357 */
358 private void establishConnection(Socket socket) throws XMPPException, IOException {
359 DataOutputStream out = new DataOutputStream(socket.getOutputStream());
360 DataInputStream in = new DataInputStream(socket.getInputStream());
361
362 // first byte is version should be 5
363 int b = in.read();
364 if (b != 5) {
365 throw new XMPPException("Only SOCKS5 supported");
366 }
367
368 // second byte number of authentication methods supported
369 b = in.read();
370
371 // read list of supported authentication methods
372 byte[] auth = new byte[b];
373 in.readFully(auth);
374
375 byte[] authMethodSelectionResponse = new byte[2];
376 authMethodSelectionResponse[0] = (byte) 0x05; // protocol version
377
378 // only authentication method 0, no authentication, supported
379 boolean noAuthMethodFound = false;
380 for (int i = 0; i < auth.length; i++) {
381 if (auth[i] == (byte) 0x00) {
382 noAuthMethodFound = true;
383 break;
384 }
385 }
386
387 if (!noAuthMethodFound) {
388 authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods
389 out.write(authMethodSelectionResponse);
390 out.flush();
391 throw new XMPPException("Authentication method not supported");
392 }
393
394 authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method
395 out.write(authMethodSelectionResponse);
396 out.flush();
397
398 // receive connection request
399 byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in);
400
401 // extract digest
402 String responseDigest = new String(connectionRequest, 5, connectionRequest[4]);
403
404 // return error if digest is not allowed
405 if (!Socks5Proxy.this.allowedConnections.contains(responseDigest)) {
406 connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused)
407 out.write(connectionRequest);
408 out.flush();
409
410 throw new XMPPException("Connection is not allowed");
411 }
412
413 connectionRequest[1] = (byte) 0x00; // set return status to 0 (success)
414 out.write(connectionRequest);
415 out.flush();
416
417 // store connection
418 Socks5Proxy.this.connectionMap.put(responseDigest, socket);
419 }
420
421 }
422
423}