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.InetAddress; 023import java.net.ServerSocket; 024import java.net.Socket; 025import java.net.SocketException; 026import java.net.UnknownHostException; 027import java.util.ArrayList; 028import java.util.Collections; 029import java.util.LinkedHashSet; 030import java.util.LinkedList; 031import java.util.List; 032import java.util.Map; 033import java.util.Set; 034import java.util.concurrent.ConcurrentHashMap; 035import java.util.logging.Level; 036import java.util.logging.Logger; 037 038import org.jivesoftware.smack.SmackException; 039 040/** 041 * The Socks5Proxy class represents a local SOCKS5 proxy server. It can be enabled/disabled by 042 * invoking {@link #setLocalSocks5ProxyEnabled(boolean)}. The proxy is enabled by default. 043 * <p> 044 * The port of the local SOCKS5 proxy can be configured by invoking 045 * {@link #setLocalSocks5ProxyPort(int)}. Default port is 7777. If you set the port to a negative 046 * value Smack tries to the absolute value and all following until it finds an open port. 047 * <p> 048 * If your application is running on a machine with multiple network interfaces or if you want to 049 * provide your public address in case you are behind a NAT router, invoke 050 * {@link #addLocalAddress(String)} or {@link #replaceLocalAddresses(List)} to modify the list of 051 * local network addresses used for outgoing SOCKS5 Bytestream requests. 052 * <p> 053 * The local SOCKS5 proxy server refuses all connections except the ones that are explicitly allowed 054 * in the process of establishing a SOCKS5 Bytestream ( 055 * {@link Socks5BytestreamManager#establishSession(String)}). 056 * <p> 057 * This Implementation has the following limitations: 058 * <ul> 059 * <li>only supports the no-authentication authentication method</li> 060 * <li>only supports the <code>connect</code> command and will not answer correctly to other 061 * commands</li> 062 * <li>only supports requests with the domain address type and will not correctly answer to requests 063 * with other address types</li> 064 * </ul> 065 * (see <a href="http://tools.ietf.org/html/rfc1928">RFC 1928</a>) 066 * 067 * @author Henning Staib 068 */ 069public class Socks5Proxy { 070 private static final Logger LOGGER = Logger.getLogger(Socks5Proxy.class.getName()); 071 072 /* SOCKS5 proxy singleton */ 073 private static Socks5Proxy socks5Server; 074 075 private static boolean localSocks5ProxyEnabled = true; 076 077 /** 078 * The port of the local Socks5 Proxy. If this value is negative, the next ports will be tried 079 * until a unused is found. 080 */ 081 private static int localSocks5ProxyPort = -7777; 082 083 /* reusable implementation of a SOCKS5 proxy server process */ 084 private Socks5ServerProcess serverProcess; 085 086 /* thread running the SOCKS5 server process */ 087 private Thread serverThread; 088 089 /* server socket to accept SOCKS5 connections */ 090 private ServerSocket serverSocket; 091 092 /* assigns a connection to a digest */ 093 private final Map<String, Socket> connectionMap = new ConcurrentHashMap<String, Socket>(); 094 095 /* list of digests connections should be stored */ 096 private final List<String> allowedConnections = Collections.synchronizedList(new LinkedList<String>()); 097 098 private final Set<String> localAddresses = Collections.synchronizedSet(new LinkedHashSet<String>()); 099 100 /** 101 * Private constructor. 102 */ 103 private Socks5Proxy() { 104 this.serverProcess = new Socks5ServerProcess(); 105 106 // add default local address 107 try { 108 this.localAddresses.add(InetAddress.getLocalHost().getHostAddress()); 109 } 110 catch (UnknownHostException e) { 111 // do nothing 112 } 113 114 } 115 116 /** 117 * Returns true if the local Socks5 proxy should be started. Default is true. 118 * 119 * @return if the local Socks5 proxy should be started 120 */ 121 public static boolean isLocalSocks5ProxyEnabled() { 122 return localSocks5ProxyEnabled; 123 } 124 125 /** 126 * Sets if the local Socks5 proxy should be started. Default is true. 127 * 128 * @param localSocks5ProxyEnabled if the local Socks5 proxy should be started 129 */ 130 public static void setLocalSocks5ProxyEnabled(boolean localSocks5ProxyEnabled) { 131 Socks5Proxy.localSocks5ProxyEnabled = localSocks5ProxyEnabled; 132 } 133 134 /** 135 * Return the port of the local Socks5 proxy. Default is 7777. 136 * 137 * @return the port of the local Socks5 proxy 138 */ 139 public static int getLocalSocks5ProxyPort() { 140 return localSocks5ProxyPort; 141 } 142 143 /** 144 * Sets the port of the local Socks5 proxy. Default is 7777. If you set the port to a negative 145 * value Smack tries the absolute value and all following until it finds an open port. 146 * 147 * @param localSocks5ProxyPort the port of the local Socks5 proxy to set 148 */ 149 public static void setLocalSocks5ProxyPort(int localSocks5ProxyPort) { 150 if (Math.abs(localSocks5ProxyPort) > 65535) { 151 throw new IllegalArgumentException("localSocks5ProxyPort must be within (-65535,65535)"); 152 } 153 Socks5Proxy.localSocks5ProxyPort = localSocks5ProxyPort; 154 } 155 156 /** 157 * Returns the local SOCKS5 proxy server. 158 * 159 * @return the local SOCKS5 proxy server 160 */ 161 public static synchronized Socks5Proxy getSocks5Proxy() { 162 if (socks5Server == null) { 163 socks5Server = new Socks5Proxy(); 164 } 165 if (isLocalSocks5ProxyEnabled()) { 166 socks5Server.start(); 167 } 168 return socks5Server; 169 } 170 171 /** 172 * Starts the local SOCKS5 proxy server. If it is already running, this method does nothing. 173 */ 174 public synchronized void start() { 175 if (isRunning()) { 176 return; 177 } 178 try { 179 if (getLocalSocks5ProxyPort() < 0) { 180 int port = Math.abs(getLocalSocks5ProxyPort()); 181 for (int i = 0; i < 65535 - port; i++) { 182 try { 183 this.serverSocket = new ServerSocket(port + i); 184 break; 185 } 186 catch (IOException e) { 187 // port is used, try next one 188 } 189 } 190 } 191 else { 192 this.serverSocket = new ServerSocket(getLocalSocks5ProxyPort()); 193 } 194 195 if (this.serverSocket != null) { 196 this.serverThread = new Thread(this.serverProcess); 197 this.serverThread.start(); 198 } 199 } 200 catch (IOException e) { 201 // couldn't setup server 202 LOGGER.log(Level.SEVERE, "couldn't setup local SOCKS5 proxy on port " + getLocalSocks5ProxyPort(), e); 203 } 204 } 205 206 /** 207 * Stops the local SOCKS5 proxy server. If it is not running this method does nothing. 208 */ 209 public synchronized void stop() { 210 if (!isRunning()) { 211 return; 212 } 213 214 try { 215 this.serverSocket.close(); 216 } 217 catch (IOException e) { 218 // do nothing 219 } 220 221 if (this.serverThread != null && this.serverThread.isAlive()) { 222 try { 223 this.serverThread.interrupt(); 224 this.serverThread.join(); 225 } 226 catch (InterruptedException e) { 227 // do nothing 228 } 229 } 230 this.serverThread = null; 231 this.serverSocket = null; 232 233 } 234 235 /** 236 * Adds the given address to the list of local network addresses. 237 * <p> 238 * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request. 239 * This may be necessary if your application is running on a machine with multiple network 240 * interfaces or if you want to provide your public address in case you are behind a NAT router. 241 * <p> 242 * The order of the addresses used is determined by the order you add addresses. 243 * <p> 244 * Note that the list of addresses initially contains the address returned by 245 * <code>InetAddress.getLocalHost().getHostAddress()</code>. You can replace the list of 246 * addresses by invoking {@link #replaceLocalAddresses(List)}. 247 * 248 * @param address the local network address to add 249 */ 250 public void addLocalAddress(String address) { 251 if (address == null) { 252 throw new IllegalArgumentException("address may not be null"); 253 } 254 this.localAddresses.add(address); 255 } 256 257 /** 258 * Removes the given address from the list of local network addresses. This address will then no 259 * longer be used of outgoing SOCKS5 Bytestream requests. 260 * 261 * @param address the local network address to remove 262 */ 263 public void removeLocalAddress(String address) { 264 this.localAddresses.remove(address); 265 } 266 267 /** 268 * Returns an unmodifiable list of the local network addresses that will be used for streamhost 269 * candidates of outgoing SOCKS5 Bytestream requests. 270 * 271 * @return unmodifiable list of the local network addresses 272 */ 273 public List<String> getLocalAddresses() { 274 return Collections.unmodifiableList(new ArrayList<String>(this.localAddresses)); 275 } 276 277 /** 278 * Replaces the list of local network addresses. 279 * <p> 280 * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request and 281 * want to define their order. This may be necessary if your application is running on a machine 282 * with multiple network interfaces or if you want to provide your public address in case you 283 * are behind a NAT router. 284 * 285 * @param addresses the new list of local network addresses 286 */ 287 public void replaceLocalAddresses(List<String> addresses) { 288 if (addresses == null) { 289 throw new IllegalArgumentException("list must not be null"); 290 } 291 this.localAddresses.clear(); 292 this.localAddresses.addAll(addresses); 293 294 } 295 296 /** 297 * Returns the port of the local SOCKS5 proxy server. If it is not running -1 will be returned. 298 * 299 * @return the port of the local SOCKS5 proxy server or -1 if proxy is not running 300 */ 301 public int getPort() { 302 if (!isRunning()) { 303 return -1; 304 } 305 return this.serverSocket.getLocalPort(); 306 } 307 308 /** 309 * Returns the socket for the given digest. A socket will be returned if the given digest has 310 * been in the list of allowed transfers (see {@link #addTransfer(String)}) while the peer 311 * connected to the SOCKS5 proxy. 312 * 313 * @param digest identifying the connection 314 * @return socket or null if there is no socket for the given digest 315 */ 316 protected Socket getSocket(String digest) { 317 return this.connectionMap.get(digest); 318 } 319 320 /** 321 * Add the given digest to the list of allowed transfers. Only connections for allowed transfers 322 * are stored and can be retrieved by invoking {@link #getSocket(String)}. All connections to 323 * the local SOCKS5 proxy that don't contain an allowed digest are discarded. 324 * 325 * @param digest to be added to the list of allowed transfers 326 */ 327 protected void addTransfer(String digest) { 328 this.allowedConnections.add(digest); 329 } 330 331 /** 332 * Removes the given digest from the list of allowed transfers. After invoking this method 333 * already stored connections with the given digest will be removed. 334 * <p> 335 * The digest should be removed after establishing the SOCKS5 Bytestream is finished, an error 336 * occurred while establishing the connection or if the connection is not allowed anymore. 337 * 338 * @param digest to be removed from the list of allowed transfers 339 */ 340 protected void removeTransfer(String digest) { 341 this.allowedConnections.remove(digest); 342 this.connectionMap.remove(digest); 343 } 344 345 /** 346 * Returns <code>true</code> if the local SOCKS5 proxy server is running, otherwise 347 * <code>false</code>. 348 * 349 * @return <code>true</code> if the local SOCKS5 proxy server is running, otherwise 350 * <code>false</code> 351 */ 352 public boolean isRunning() { 353 return this.serverSocket != null; 354 } 355 356 /** 357 * Implementation of a simplified SOCKS5 proxy server. 358 */ 359 private class Socks5ServerProcess implements Runnable { 360 361 public void run() { 362 while (true) { 363 Socket socket = null; 364 365 try { 366 367 if (Socks5Proxy.this.serverSocket.isClosed() 368 || Thread.currentThread().isInterrupted()) { 369 return; 370 } 371 372 // accept connection 373 socket = Socks5Proxy.this.serverSocket.accept(); 374 375 // initialize connection 376 establishConnection(socket); 377 378 } 379 catch (SocketException e) { 380 /* 381 * do nothing, if caused by closing the server socket, thread will terminate in 382 * next loop 383 */ 384 } 385 catch (Exception e) { 386 try { 387 if (socket != null) { 388 socket.close(); 389 } 390 } 391 catch (IOException e1) { 392 /* do nothing */ 393 } 394 } 395 } 396 397 } 398 399 /** 400 * Negotiates a SOCKS5 connection and stores it on success. 401 * 402 * @param socket connection to the client 403 * @throws SmackException if client requests a connection in an unsupported way 404 * @throws IOException if a network error occurred 405 */ 406 private void establishConnection(Socket socket) throws SmackException, IOException { 407 DataOutputStream out = new DataOutputStream(socket.getOutputStream()); 408 DataInputStream in = new DataInputStream(socket.getInputStream()); 409 410 // first byte is version should be 5 411 int b = in.read(); 412 if (b != 5) { 413 throw new SmackException("Only SOCKS5 supported"); 414 } 415 416 // second byte number of authentication methods supported 417 b = in.read(); 418 419 // read list of supported authentication methods 420 byte[] auth = new byte[b]; 421 in.readFully(auth); 422 423 byte[] authMethodSelectionResponse = new byte[2]; 424 authMethodSelectionResponse[0] = (byte) 0x05; // protocol version 425 426 // only authentication method 0, no authentication, supported 427 boolean noAuthMethodFound = false; 428 for (int i = 0; i < auth.length; i++) { 429 if (auth[i] == (byte) 0x00) { 430 noAuthMethodFound = true; 431 break; 432 } 433 } 434 435 if (!noAuthMethodFound) { 436 authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods 437 out.write(authMethodSelectionResponse); 438 out.flush(); 439 throw new SmackException("Authentication method not supported"); 440 } 441 442 authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method 443 out.write(authMethodSelectionResponse); 444 out.flush(); 445 446 // receive connection request 447 byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in); 448 449 // extract digest 450 String responseDigest = new String(connectionRequest, 5, connectionRequest[4]); 451 452 // return error if digest is not allowed 453 if (!Socks5Proxy.this.allowedConnections.contains(responseDigest)) { 454 connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused) 455 out.write(connectionRequest); 456 out.flush(); 457 458 throw new SmackException("Connection is not allowed"); 459 } 460 461 connectionRequest[1] = (byte) 0x00; // set return status to 0 (success) 462 out.write(connectionRequest); 463 out.flush(); 464 465 // store connection 466 Socks5Proxy.this.connectionMap.put(responseDigest, socket); 467 } 468 469 } 470 471}