1 /** 2 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); 3 * you may not use this file except in compliance with the License. 4 * You may obtain a copy of the License at 5 * 6 * http://www.apache.org/licenses/LICENSE-2.0 7 * 8 * Unless required by applicable law or agreed to in writing, software 9 * distributed under the License is distributed on an "AS IS" BASIS, 10 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 * See the License for the specific language governing permissions and 12 * limitations under the License. 13 */ 14 package org.jivesoftware.smackx.bytestreams.socks5; 15 16 import java.io.DataInputStream; 17 import java.io.DataOutputStream; 18 import java.io.IOException; 19 import java.net.InetAddress; 20 import java.net.ServerSocket; 21 import java.net.Socket; 22 import java.net.SocketException; 23 import java.net.UnknownHostException; 24 import java.util.ArrayList; 25 import java.util.Collections; 26 import java.util.LinkedHashSet; 27 import java.util.LinkedList; 28 import java.util.List; 29 import java.util.Map; 30 import java.util.Set; 31 import java.util.concurrent.ConcurrentHashMap; 32 33 import org.jivesoftware.smack.SmackConfiguration; 34 import org.jivesoftware.smack.XMPPException; 35 36 /** 37 * The Socks5Proxy class represents a local SOCKS5 proxy server. It can be enabled/disabled by 38 * setting the <code>localSocks5ProxyEnabled</code> flag in the <code>smack-config.xml</code> or by 39 * invoking {@link SmackConfiguration#setLocalSocks5ProxyEnabled(boolean)}. The proxy is enabled by 40 * default. 41 * <p> 42 * The port of the local SOCKS5 proxy can be configured by setting <code>localSocks5ProxyPort</code> 43 * in the <code>smack-config.xml</code> or by invoking 44 * {@link SmackConfiguration#setLocalSocks5ProxyPort(int)}. Default port is 7777. If you set the 45 * port to a negative value Smack tries to the absolute value and all following until it finds an 46 * open port. 47 * <p> 48 * If your application is running on a machine with multiple network interfaces or if you want to 49 * provide your public address in case you are behind a NAT router, invoke 50 * {@link #addLocalAddress(String)} or {@link #replaceLocalAddresses(List)} to modify the list of 51 * local network addresses used for outgoing SOCKS5 Bytestream requests. 52 * <p> 53 * The local SOCKS5 proxy server refuses all connections except the ones that are explicitly allowed 54 * in the process of establishing a SOCKS5 Bytestream ( 55 * {@link Socks5BytestreamManager#establishSession(String)}). 56 * <p> 57 * This Implementation has the following limitations: 58 * <ul> 59 * <li>only supports the no-authentication authentication method</li> 60 * <li>only supports the <code>connect</code> command and will not answer correctly to other 61 * commands</li> 62 * <li>only supports requests with the domain address type and will not correctly answer to requests 63 * with other address types</li> 64 * </ul> 65 * (see <a href="http://tools.ietf.org/html/rfc1928">RFC 1928</a>) 66 * 67 * @author Henning Staib 68 */ 69 public class Socks5Proxy { 70 71 /* SOCKS5 proxy singleton */ 72 private static Socks5Proxy socks5Server; 73 74 /* reusable implementation of a SOCKS5 proxy server process */ 75 private Socks5ServerProcess serverProcess; 76 77 /* thread running the SOCKS5 server process */ 78 private Thread serverThread; 79 80 /* server socket to accept SOCKS5 connections */ 81 private ServerSocket serverSocket; 82 83 /* assigns a connection to a digest */ 84 private final Map<String, Socket> connectionMap = new ConcurrentHashMap<String, Socket>(); 85 86 /* list of digests connections should be stored */ 87 private final List<String> allowedConnections = Collections.synchronizedList(new LinkedList<String>()); 88 89 private final Set<String> localAddresses = Collections.synchronizedSet(new LinkedHashSet<String>()); 90 91 /** 92 * Private constructor. 93 */ 94 private Socks5Proxy() { 95 this.serverProcess = new Socks5ServerProcess(); 96 97 // add default local address 98 try { 99 this.localAddresses.add(InetAddress.getLocalHost().getHostAddress()); 100 } 101 catch (UnknownHostException e) { 102 // do nothing 103 } 104 105 } 106 107 /** 108 * Returns the local SOCKS5 proxy server. 109 * 110 * @return the local SOCKS5 proxy server 111 */ 112 public static synchronized Socks5Proxy getSocks5Proxy() { 113 if (socks5Server == null) { 114 socks5Server = new Socks5Proxy(); 115 } 116 if (SmackConfiguration.isLocalSocks5ProxyEnabled()) { 117 socks5Server.start(); 118 } 119 return socks5Server; 120 } 121 122 /** 123 * Starts the local SOCKS5 proxy server. If it is already running, this method does nothing. 124 */ 125 public synchronized void start() { 126 if (isRunning()) { 127 return; 128 } 129 try { 130 if (SmackConfiguration.getLocalSocks5ProxyPort() < 0) { 131 int port = Math.abs(SmackConfiguration.getLocalSocks5ProxyPort()); 132 for (int i = 0; i < 65535 - port; i++) { 133 try { 134 this.serverSocket = new ServerSocket(port + i); 135 break; 136 } 137 catch (IOException e) { 138 // port is used, try next one 139 } 140 } 141 } 142 else { 143 this.serverSocket = new ServerSocket(SmackConfiguration.getLocalSocks5ProxyPort()); 144 } 145 146 if (this.serverSocket != null) { 147 this.serverThread = new Thread(this.serverProcess); 148 this.serverThread.start(); 149 } 150 } 151 catch (IOException e) { 152 // couldn't setup server 153 System.err.println("couldn't setup local SOCKS5 proxy on port " 154 + SmackConfiguration.getLocalSocks5ProxyPort() + ": " + e.getMessage()); 155 } 156 } 157 158 /** 159 * Stops the local SOCKS5 proxy server. If it is not running this method does nothing. 160 */ 161 public synchronized void stop() { 162 if (!isRunning()) { 163 return; 164 } 165 166 try { 167 this.serverSocket.close(); 168 } 169 catch (IOException e) { 170 // do nothing 171 } 172 173 if (this.serverThread != null && this.serverThread.isAlive()) { 174 try { 175 this.serverThread.interrupt(); 176 this.serverThread.join(); 177 } 178 catch (InterruptedException e) { 179 // do nothing 180 } 181 } 182 this.serverThread = null; 183 this.serverSocket = null; 184 185 } 186 187 /** 188 * Adds the given address to the list of local network addresses. 189 * <p> 190 * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request. 191 * This may be necessary if your application is running on a machine with multiple network 192 * interfaces or if you want to provide your public address in case you are behind a NAT router. 193 * <p> 194 * The order of the addresses used is determined by the order you add addresses. 195 * <p> 196 * Note that the list of addresses initially contains the address returned by 197 * <code>InetAddress.getLocalHost().getHostAddress()</code>. You can replace the list of 198 * addresses by invoking {@link #replaceLocalAddresses(List)}. 199 * 200 * @param address the local network address to add 201 */ 202 public void addLocalAddress(String address) { 203 if (address == null) { 204 throw new IllegalArgumentException("address may not be null"); 205 } 206 this.localAddresses.add(address); 207 } 208 209 /** 210 * Removes the given address from the list of local network addresses. This address will then no 211 * longer be used of outgoing SOCKS5 Bytestream requests. 212 * 213 * @param address the local network address to remove 214 */ 215 public void removeLocalAddress(String address) { 216 this.localAddresses.remove(address); 217 } 218 219 /** 220 * Returns an unmodifiable list of the local network addresses that will be used for streamhost 221 * candidates of outgoing SOCKS5 Bytestream requests. 222 * 223 * @return unmodifiable list of the local network addresses 224 */ 225 public List<String> getLocalAddresses() { 226 return Collections.unmodifiableList(new ArrayList<String>(this.localAddresses)); 227 } 228 229 /** 230 * Replaces the list of local network addresses. 231 * <p> 232 * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request and 233 * want to define their order. This may be necessary if your application is running on a machine 234 * with multiple network interfaces or if you want to provide your public address in case you 235 * are behind a NAT router. 236 * 237 * @param addresses the new list of local network addresses 238 */ 239 public void replaceLocalAddresses(List<String> addresses) { 240 if (addresses == null) { 241 throw new IllegalArgumentException("list must not be null"); 242 } 243 this.localAddresses.clear(); 244 this.localAddresses.addAll(addresses); 245 246 } 247 248 /** 249 * Returns the port of the local SOCKS5 proxy server. If it is not running -1 will be returned. 250 * 251 * @return the port of the local SOCKS5 proxy server or -1 if proxy is not running 252 */ 253 public int getPort() { 254 if (!isRunning()) { 255 return -1; 256 } 257 return this.serverSocket.getLocalPort(); 258 } 259 260 /** 261 * Returns the socket for the given digest. A socket will be returned if the given digest has 262 * been in the list of allowed transfers (see {@link #addTransfer(String)}) while the peer 263 * connected to the SOCKS5 proxy. 264 * 265 * @param digest identifying the connection 266 * @return socket or null if there is no socket for the given digest 267 */ 268 protected Socket getSocket(String digest) { 269 return this.connectionMap.get(digest); 270 } 271 272 /** 273 * Add the given digest to the list of allowed transfers. Only connections for allowed transfers 274 * are stored and can be retrieved by invoking {@link #getSocket(String)}. All connections to 275 * the local SOCKS5 proxy that don't contain an allowed digest are discarded. 276 * 277 * @param digest to be added to the list of allowed transfers 278 */ 279 protected void addTransfer(String digest) { 280 this.allowedConnections.add(digest); 281 } 282 283 /** 284 * Removes the given digest from the list of allowed transfers. After invoking this method 285 * already stored connections with the given digest will be removed. 286 * <p> 287 * The digest should be removed after establishing the SOCKS5 Bytestream is finished, an error 288 * occurred while establishing the connection or if the connection is not allowed anymore. 289 * 290 * @param digest to be removed from the list of allowed transfers 291 */ 292 protected void removeTransfer(String digest) { 293 this.allowedConnections.remove(digest); 294 this.connectionMap.remove(digest); 295 } 296 297 /** 298 * Returns <code>true</code> if the local SOCKS5 proxy server is running, otherwise 299 * <code>false</code>. 300 * 301 * @return <code>true</code> if the local SOCKS5 proxy server is running, otherwise 302 * <code>false</code> 303 */ 304 public boolean isRunning() { 305 return this.serverSocket != null; 306 } 307 308 /** 309 * Implementation of a simplified SOCKS5 proxy server. 310 */ 311 private class Socks5ServerProcess implements Runnable { 312 313 public void run() { 314 while (true) { 315 Socket socket = null; 316 317 try { 318 319 if (Socks5Proxy.this.serverSocket.isClosed() 320 || Thread.currentThread().isInterrupted()) { 321 return; 322 } 323 324 // accept connection 325 socket = Socks5Proxy.this.serverSocket.accept(); 326 327 // initialize connection 328 establishConnection(socket); 329 330 } 331 catch (SocketException e) { 332 /* 333 * do nothing, if caused by closing the server socket, thread will terminate in 334 * next loop 335 */ 336 } 337 catch (Exception e) { 338 try { 339 if (socket != null) { 340 socket.close(); 341 } 342 } 343 catch (IOException e1) { 344 /* do nothing */ 345 } 346 } 347 } 348 349 } 350 351 /** 352 * Negotiates a SOCKS5 connection and stores it on success. 353 * 354 * @param socket connection to the client 355 * @throws XMPPException if client requests a connection in an unsupported way 356 * @throws IOException if a network error occurred 357 */ 358 private void establishConnection(Socket socket) throws XMPPException, IOException { 359 DataOutputStream out = new DataOutputStream(socket.getOutputStream()); 360 DataInputStream in = new DataInputStream(socket.getInputStream()); 361 362 // first byte is version should be 5 363 int b = in.read(); 364 if (b != 5) { 365 throw new XMPPException("Only SOCKS5 supported"); 366 } 367 368 // second byte number of authentication methods supported 369 b = in.read(); 370 371 // read list of supported authentication methods 372 byte[] auth = new byte[b]; 373 in.readFully(auth); 374 375 byte[] authMethodSelectionResponse = new byte[2]; 376 authMethodSelectionResponse[0] = (byte) 0x05; // protocol version 377 378 // only authentication method 0, no authentication, supported 379 boolean noAuthMethodFound = false; 380 for (int i = 0; i < auth.length; i++) { 381 if (auth[i] == (byte) 0x00) { 382 noAuthMethodFound = true; 383 break; 384 } 385 } 386 387 if (!noAuthMethodFound) { 388 authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods 389 out.write(authMethodSelectionResponse); 390 out.flush(); 391 throw new XMPPException("Authentication method not supported"); 392 } 393 394 authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method 395 out.write(authMethodSelectionResponse); 396 out.flush(); 397 398 // receive connection request 399 byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in); 400 401 // extract digest 402 String responseDigest = new String(connectionRequest, 5, connectionRequest[4]); 403 404 // return error if digest is not allowed 405 if (!Socks5Proxy.this.allowedConnections.contains(responseDigest)) { 406 connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused) 407 out.write(connectionRequest); 408 out.flush(); 409 410 throw new XMPPException("Connection is not allowed"); 411 } 412 413 connectionRequest[1] = (byte) 0x00; // set return status to 0 (success) 414 out.write(connectionRequest); 415 out.flush(); 416 417 // store connection 418 Socks5Proxy.this.connectionMap.put(responseDigest, socket); 419 } 420 421 } 422 423 } 424