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