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.ArrayList; 030import java.util.Collection; 031import java.util.Collections; 032import java.util.Enumeration; 033import java.util.HashSet; 034import java.util.LinkedHashSet; 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 ArrayList<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 @SuppressWarnings("this-escape") 153 protected Socks5Proxy(ServerSocket serverSocket) { 154 this.serverProcess = new Socks5ServerProcess(); 155 this.serverSocket = serverSocket; 156 157 allowAllConnections = true; 158 159 startServerThread(); 160 } 161 162 163 164 /** 165 * Returns true if the local Socks5 proxy should be started. Default is true. 166 * 167 * @return if the local Socks5 proxy should be started 168 */ 169 public static boolean isLocalSocks5ProxyEnabled() { 170 return localSocks5ProxyEnabled; 171 } 172 173 /** 174 * Sets if the local Socks5 proxy should be started. Default is true. 175 * 176 * @param localSocks5ProxyEnabled if the local Socks5 proxy should be started 177 */ 178 public static void setLocalSocks5ProxyEnabled(boolean localSocks5ProxyEnabled) { 179 Socks5Proxy.localSocks5ProxyEnabled = localSocks5ProxyEnabled; 180 } 181 182 private static void checkLocalSocks5ProxyPortArgument(int port) { 183 if (Math.abs(port) > 65535) { 184 throw new IllegalArgumentException("Local SOCKS5 proxy port must be within (-65535,65535)"); 185 } 186 } 187 188 public static int getDefaultLocalSocks5ProxyPort() { 189 return DEFAULT_LOCAL_SOCKS5_PROXY_PORT; 190 } 191 192 public static void setDefaultLocalSocsk5ProxyPort(int defaultLocalSocks5ProxyPort) { 193 checkLocalSocks5ProxyPortArgument(defaultLocalSocks5ProxyPort); 194 DEFAULT_LOCAL_SOCKS5_PROXY_PORT = defaultLocalSocks5ProxyPort; 195 } 196 197 /** 198 * Return the port of the local Socks5 proxy. Default is 7777. 199 * 200 * @return the port of the local Socks5 proxy 201 */ 202 public int getLocalSocks5ProxyPort() { 203 return localSocks5ProxyPort; 204 } 205 206 /** 207 * Sets the port of the local Socks5 proxy. Default is 7777. If you set the port to a negative 208 * value Smack tries the absolute value and all following until it finds an open port. 209 * 210 * @param localSocks5ProxyPort the port of the local Socks5 proxy to set 211 */ 212 public void setLocalSocks5ProxyPort(int localSocks5ProxyPort) { 213 checkLocalSocks5ProxyPortArgument(localSocks5ProxyPort); 214 this.localSocks5ProxyPort = localSocks5ProxyPort; 215 } 216 217 /** 218 * Returns the local SOCKS5 proxy server. 219 * 220 * @return the local SOCKS5 proxy server 221 */ 222 public static synchronized Socks5Proxy getSocks5Proxy() { 223 if (socks5Server == null) { 224 socks5Server = new Socks5Proxy(); 225 } 226 if (isLocalSocks5ProxyEnabled()) { 227 socks5Server.start(); 228 } 229 return socks5Server; 230 } 231 232 /** 233 * Starts the local SOCKS5 proxy server. If it is already running, this method does nothing. 234 * 235 * @return the server socket. 236 */ 237 public synchronized ServerSocket start() { 238 if (isRunning()) { 239 return this.serverSocket; 240 } 241 try { 242 if (getLocalSocks5ProxyPort() < 0) { 243 int port = Math.abs(getLocalSocks5ProxyPort()); 244 for (int i = 0; i < 65535 - port; i++) { 245 try { 246 this.serverSocket = new ServerSocket(port + i); 247 break; 248 } 249 catch (IOException e) { 250 // port is used, try next one 251 } 252 } 253 } 254 else { 255 this.serverSocket = new ServerSocket(getLocalSocks5ProxyPort()); 256 } 257 258 if (this.serverSocket != null) { 259 startServerThread(); 260 } 261 } 262 catch (IOException e) { 263 // couldn't setup server 264 LOGGER.log(Level.SEVERE, "couldn't setup local SOCKS5 proxy on port " + getLocalSocks5ProxyPort(), e); 265 } 266 267 return this.serverSocket; 268 } 269 270 private synchronized void startServerThread() { 271 this.serverThread = new Thread(this.serverProcess); 272 this.serverThread.setName("Smack Local SOCKS5 Proxy [" + this.serverSocket + ']'); 273 this.serverThread.setDaemon(true); 274 275 RUNNING_PROXIES.add(this); 276 this.serverThread.start(); 277 } 278 279 /** 280 * Stops the local SOCKS5 proxy server. If it is not running this method does nothing. 281 */ 282 public synchronized void stop() { 283 if (!isRunning()) { 284 return; 285 } 286 287 RUNNING_PROXIES.remove(this); 288 289 CloseableUtil.maybeClose(this.serverSocket, LOGGER); 290 291 if (this.serverThread != null && this.serverThread.isAlive()) { 292 try { 293 this.serverThread.interrupt(); 294 this.serverThread.join(); 295 } 296 catch (InterruptedException e) { 297 LOGGER.log(Level.WARNING, "SOCKS5 server thread termination was interrupted", e); 298 } 299 } 300 this.serverThread = null; 301 this.serverSocket = null; 302 } 303 304 /** 305 * Adds the given address to the list of local network addresses. 306 * <p> 307 * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request. 308 * This may be necessary if your application is running on a machine with multiple network 309 * interfaces or if you want to provide your public address in case you are behind a NAT router. 310 * <p> 311 * The order of the addresses used is determined by the order you add addresses. 312 * <p> 313 * Note that the list of addresses initially contains the address returned by 314 * <code>InetAddress.getLocalHost().getHostAddress()</code>. You can replace the list of 315 * addresses by invoking {@link #replaceLocalAddresses(Collection)}. 316 * 317 * @param address the local network address to add 318 */ 319 public void addLocalAddress(InetAddress address) { 320 if (address == null) { 321 return; 322 } 323 synchronized (localAddresses) { 324 this.localAddresses.add(address); 325 } 326 } 327 328 /** 329 * Removes the given address from the list of local network addresses. This address will then no 330 * longer be used of outgoing SOCKS5 Bytestream requests. 331 * 332 * @param address the local network address to remove 333 * @return true if the address was removed. 334 */ 335 public boolean removeLocalAddress(InetAddress address) { 336 synchronized (localAddresses) { 337 return localAddresses.remove(address); 338 } 339 } 340 341 /** 342 * Returns an set of the local network addresses that will be used for streamhost 343 * candidates of outgoing SOCKS5 Bytestream requests. 344 * 345 * @return set of the local network addresses 346 */ 347 public List<InetAddress> getLocalAddresses() { 348 synchronized (localAddresses) { 349 return new ArrayList<>(localAddresses); 350 } 351 } 352 353 /** 354 * Replaces the list of local network addresses. 355 * <p> 356 * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request and 357 * want to define their order. This may be necessary if your application is running on a machine 358 * with multiple network interfaces or if you want to provide your public address in case you 359 * are behind a NAT router. 360 * 361 * @param addresses the new list of local network addresses 362 */ 363 public void replaceLocalAddresses(Collection<? extends InetAddress> addresses) { 364 if (addresses == null) { 365 throw new IllegalArgumentException("list must not be null"); 366 } 367 synchronized (localAddresses) { 368 localAddresses.clear(); 369 localAddresses.addAll(addresses); 370 } 371 } 372 373 /** 374 * Returns the port of the local SOCKS5 proxy server. If it is not running -1 will be returned. 375 * 376 * @return the port of the local SOCKS5 proxy server or -1 if proxy is not running 377 */ 378 public int getPort() { 379 if (!isRunning()) { 380 return -1; 381 } 382 return this.serverSocket.getLocalPort(); 383 } 384 385 /** 386 * Returns the socket for the given digest. A socket will be returned if the given digest has 387 * been in the list of allowed transfers (see {@link #addTransfer(String)}) while the peer 388 * connected to the SOCKS5 proxy. 389 * 390 * @param digest identifying the connection 391 * @return socket or null if there is no socket for the given digest 392 */ 393 protected Socket getSocket(String digest) { 394 return this.connectionMap.get(digest); 395 } 396 397 /** 398 * Add the given digest to the list of allowed transfers. Only connections for allowed transfers 399 * are stored and can be retrieved by invoking {@link #getSocket(String)}. All connections to 400 * the local SOCKS5 proxy that don't contain an allowed digest are discarded. 401 * 402 * @param digest to be added to the list of allowed transfers 403 */ 404 public void addTransfer(String digest) { 405 this.allowedConnections.add(digest); 406 } 407 408 /** 409 * Removes the given digest from the list of allowed transfers. After invoking this method 410 * already stored connections with the given digest will be removed. 411 * <p> 412 * The digest should be removed after establishing the SOCKS5 Bytestream is finished, an error 413 * occurred while establishing the connection or if the connection is not allowed anymore. 414 * 415 * @param digest to be removed from the list of allowed transfers 416 */ 417 protected void removeTransfer(String digest) { 418 this.allowedConnections.remove(digest); 419 this.connectionMap.remove(digest); 420 } 421 422 /** 423 * Returns <code>true</code> if the local SOCKS5 proxy server is running, otherwise 424 * <code>false</code>. 425 * 426 * @return <code>true</code> if the local SOCKS5 proxy server is running, otherwise 427 * <code>false</code> 428 */ 429 public boolean isRunning() { 430 return this.serverSocket != null; 431 } 432 433 /** 434 * Implementation of a simplified SOCKS5 proxy server. 435 */ 436 private final class Socks5ServerProcess implements Runnable { 437 438 @Override 439 public void run() { 440 while (true) { 441 ServerSocket serverSocket = Socks5Proxy.this.serverSocket; 442 if (serverSocket == null || serverSocket.isClosed() || Thread.currentThread().isInterrupted()) { 443 return; 444 } 445 446 // accept connection 447 Socket socket = null; 448 try { 449 socket = serverSocket.accept(); 450 // initialize connection 451 establishConnection(socket); 452 } catch (SmackException | IOException e) { 453 // Do nothing, if caused by closing the server socket, thread will terminate in next loop. 454 LOGGER.log(Level.FINE, "Exception while " + Socks5Proxy.this + " was handling connection", e); 455 CloseableUtil.maybeClose(socket, LOGGER); 456 } 457 } 458 } 459 460 /** 461 * Negotiates a SOCKS5 connection and stores it on success. 462 * 463 * @param socket connection to the client 464 * @throws SmackException if client requests a connection in an unsupported way 465 * @throws IOException if a network error occurred 466 */ 467 private void establishConnection(Socket socket) throws SmackException, IOException { 468 DataOutputStream out = new DataOutputStream(socket.getOutputStream()); 469 DataInputStream in = new DataInputStream(socket.getInputStream()); 470 471 // first byte is version should be 5 472 int b = in.read(); 473 if (b != 5) { 474 throw new SmackException.SmackMessageException("Only SOCKS5 supported: Peer send " + b + " but we expect 5"); 475 } 476 477 // second byte number of authentication methods supported 478 b = in.read(); 479 480 // read list of supported authentication methods 481 byte[] auth = new byte[b]; 482 in.readFully(auth); 483 484 byte[] authMethodSelectionResponse = new byte[2]; 485 authMethodSelectionResponse[0] = (byte) 0x05; // protocol version 486 487 // only authentication method 0, no authentication, supported 488 boolean noAuthMethodFound = false; 489 for (int i = 0; i < auth.length; i++) { 490 if (auth[i] == (byte) 0x00) { 491 noAuthMethodFound = true; 492 break; 493 } 494 } 495 496 if (!noAuthMethodFound) { 497 authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods 498 out.write(authMethodSelectionResponse); 499 out.flush(); 500 throw new SmackException.SmackMessageException("Authentication method not supported"); 501 } 502 503 authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method 504 out.write(authMethodSelectionResponse); 505 out.flush(); 506 507 // receive connection request 508 byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in); 509 510 // extract digest 511 String responseDigest = new String(connectionRequest, 5, connectionRequest[4], StandardCharsets.UTF_8); 512 513 // return error if digest is not allowed 514 if (!allowAllConnections && !Socks5Proxy.this.allowedConnections.contains(responseDigest)) { 515 connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused) 516 out.write(connectionRequest); 517 out.flush(); 518 519 throw new SmackException.SmackMessageException( 520 "Connection with digest '" + responseDigest + "' is not allowed"); 521 } 522 523 // Store the connection before we send the return status. 524 Socks5Proxy.this.connectionMap.put(responseDigest, socket); 525 526 connectionRequest[1] = (byte) 0x00; // set return status to 0 (success) 527 out.write(connectionRequest); 528 out.flush(); 529 } 530 531 } 532 533 public static Socket getSocketForDigest(String digest) { 534 for (Socks5Proxy socks5Proxy : RUNNING_PROXIES) { 535 Socket socket = socks5Proxy.getSocket(digest); 536 if (socket != null) { 537 return socket; 538 } 539 } 540 return null; 541 } 542 543 static List<Socks5Proxy> getRunningProxies() { 544 return RUNNING_PROXIES; 545 } 546}