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