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.start(); 215 } 216 } 217 catch (IOException e) { 218 // couldn't setup server 219 LOGGER.log(Level.SEVERE, "couldn't setup local SOCKS5 proxy on port " + getLocalSocks5ProxyPort(), e); 220 } 221 } 222 223 /** 224 * Stops the local SOCKS5 proxy server. If it is not running this method does nothing. 225 */ 226 public synchronized void stop() { 227 if (!isRunning()) { 228 return; 229 } 230 231 try { 232 this.serverSocket.close(); 233 } 234 catch (IOException e) { 235 // do nothing 236 } 237 238 if (this.serverThread != null && this.serverThread.isAlive()) { 239 try { 240 this.serverThread.interrupt(); 241 this.serverThread.join(); 242 } 243 catch (InterruptedException e) { 244 // do nothing 245 } 246 } 247 this.serverThread = null; 248 this.serverSocket = null; 249 250 } 251 252 /** 253 * Adds the given address to the list of local network addresses. 254 * <p> 255 * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request. 256 * This may be necessary if your application is running on a machine with multiple network 257 * interfaces or if you want to provide your public address in case you are behind a NAT router. 258 * <p> 259 * The order of the addresses used is determined by the order you add addresses. 260 * <p> 261 * Note that the list of addresses initially contains the address returned by 262 * <code>InetAddress.getLocalHost().getHostAddress()</code>. You can replace the list of 263 * addresses by invoking {@link #replaceLocalAddresses(Collection)}. 264 * 265 * @param address the local network address to add 266 */ 267 public void addLocalAddress(String address) { 268 if (address == null) { 269 return; 270 } 271 synchronized (localAddresses) { 272 this.localAddresses.add(address); 273 } 274 } 275 276 /** 277 * Removes the given address from the list of local network addresses. This address will then no 278 * longer be used of outgoing SOCKS5 Bytestream requests. 279 * 280 * @param address the local network address to remove 281 * @return true if the address was removed. 282 */ 283 public boolean removeLocalAddress(String address) { 284 synchronized (localAddresses) { 285 return localAddresses.remove(address); 286 } 287 } 288 289 /** 290 * Returns an set of the local network addresses that will be used for streamhost 291 * candidates of outgoing SOCKS5 Bytestream requests. 292 * 293 * @return set of the local network addresses 294 */ 295 public List<String> getLocalAddresses() { 296 synchronized (localAddresses) { 297 return new LinkedList<>(localAddresses); 298 } 299 } 300 301 /** 302 * Replaces the list of local network addresses. 303 * <p> 304 * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request and 305 * want to define their order. This may be necessary if your application is running on a machine 306 * with multiple network interfaces or if you want to provide your public address in case you 307 * are behind a NAT router. 308 * 309 * @param addresses the new list of local network addresses 310 */ 311 public void replaceLocalAddresses(Collection<String> addresses) { 312 if (addresses == null) { 313 throw new IllegalArgumentException("list must not be null"); 314 } 315 synchronized (localAddresses) { 316 localAddresses.clear(); 317 localAddresses.addAll(addresses); 318 } 319 } 320 321 /** 322 * Returns the port of the local SOCKS5 proxy server. If it is not running -1 will be returned. 323 * 324 * @return the port of the local SOCKS5 proxy server or -1 if proxy is not running 325 */ 326 public int getPort() { 327 if (!isRunning()) { 328 return -1; 329 } 330 return this.serverSocket.getLocalPort(); 331 } 332 333 /** 334 * Returns the socket for the given digest. A socket will be returned if the given digest has 335 * been in the list of allowed transfers (see {@link #addTransfer(String)}) while the peer 336 * connected to the SOCKS5 proxy. 337 * 338 * @param digest identifying the connection 339 * @return socket or null if there is no socket for the given digest 340 */ 341 protected Socket getSocket(String digest) { 342 return this.connectionMap.get(digest); 343 } 344 345 /** 346 * Add the given digest to the list of allowed transfers. Only connections for allowed transfers 347 * are stored and can be retrieved by invoking {@link #getSocket(String)}. All connections to 348 * the local SOCKS5 proxy that don't contain an allowed digest are discarded. 349 * 350 * @param digest to be added to the list of allowed transfers 351 */ 352 public void addTransfer(String digest) { 353 this.allowedConnections.add(digest); 354 } 355 356 /** 357 * Removes the given digest from the list of allowed transfers. After invoking this method 358 * already stored connections with the given digest will be removed. 359 * <p> 360 * The digest should be removed after establishing the SOCKS5 Bytestream is finished, an error 361 * occurred while establishing the connection or if the connection is not allowed anymore. 362 * 363 * @param digest to be removed from the list of allowed transfers 364 */ 365 protected void removeTransfer(String digest) { 366 this.allowedConnections.remove(digest); 367 this.connectionMap.remove(digest); 368 } 369 370 /** 371 * Returns <code>true</code> if the local SOCKS5 proxy server is running, otherwise 372 * <code>false</code>. 373 * 374 * @return <code>true</code> if the local SOCKS5 proxy server is running, otherwise 375 * <code>false</code> 376 */ 377 public boolean isRunning() { 378 return this.serverSocket != null; 379 } 380 381 /** 382 * Implementation of a simplified SOCKS5 proxy server. 383 */ 384 private class Socks5ServerProcess implements Runnable { 385 386 @Override 387 public void run() { 388 while (true) { 389 Socket socket = null; 390 391 try { 392 393 if (Socks5Proxy.this.serverSocket == null || Socks5Proxy.this.serverSocket.isClosed() 394 || Thread.currentThread().isInterrupted()) { 395 return; 396 } 397 398 // accept connection 399 socket = Socks5Proxy.this.serverSocket.accept(); 400 401 // initialize connection 402 establishConnection(socket); 403 404 } 405 catch (SocketException e) { 406 /* 407 * do nothing, if caused by closing the server socket, thread will terminate in 408 * next loop 409 */ 410 } 411 catch (Exception e) { 412 try { 413 if (socket != null) { 414 socket.close(); 415 } 416 } 417 catch (IOException e1) { 418 /* do nothing */ 419 } 420 } 421 } 422 423 } 424 425 /** 426 * Negotiates a SOCKS5 connection and stores it on success. 427 * 428 * @param socket connection to the client 429 * @throws SmackException if client requests a connection in an unsupported way 430 * @throws IOException if a network error occurred 431 */ 432 private void establishConnection(Socket socket) throws SmackException, IOException { 433 DataOutputStream out = new DataOutputStream(socket.getOutputStream()); 434 DataInputStream in = new DataInputStream(socket.getInputStream()); 435 436 // first byte is version should be 5 437 int b = in.read(); 438 if (b != 5) { 439 throw new SmackException("Only SOCKS5 supported"); 440 } 441 442 // second byte number of authentication methods supported 443 b = in.read(); 444 445 // read list of supported authentication methods 446 byte[] auth = new byte[b]; 447 in.readFully(auth); 448 449 byte[] authMethodSelectionResponse = new byte[2]; 450 authMethodSelectionResponse[0] = (byte) 0x05; // protocol version 451 452 // only authentication method 0, no authentication, supported 453 boolean noAuthMethodFound = false; 454 for (int i = 0; i < auth.length; i++) { 455 if (auth[i] == (byte) 0x00) { 456 noAuthMethodFound = true; 457 break; 458 } 459 } 460 461 if (!noAuthMethodFound) { 462 authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods 463 out.write(authMethodSelectionResponse); 464 out.flush(); 465 throw new SmackException("Authentication method not supported"); 466 } 467 468 authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method 469 out.write(authMethodSelectionResponse); 470 out.flush(); 471 472 // receive connection request 473 byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in); 474 475 // extract digest 476 String responseDigest = new String(connectionRequest, 5, connectionRequest[4], StringUtils.UTF8); 477 478 // return error if digest is not allowed 479 if (!Socks5Proxy.this.allowedConnections.contains(responseDigest)) { 480 connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused) 481 out.write(connectionRequest); 482 out.flush(); 483 484 throw new SmackException("Connection is not allowed"); 485 } 486 487 connectionRequest[1] = (byte) 0x00; // set return status to 0 (success) 488 out.write(connectionRequest); 489 out.flush(); 490 491 // store connection 492 Socks5Proxy.this.connectionMap.put(responseDigest, socket); 493 } 494 495 } 496 497}