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