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