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