/** * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.smackx.bytestreams.socks5; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.jivesoftware.smack.SmackConfiguration; import org.jivesoftware.smack.XMPPException; /** * The Socks5Proxy class represents a local SOCKS5 proxy server. It can be * enabled/disabled by setting the <code>localSocks5ProxyEnabled</code> flag in * the <code>smack-config.xml</code> or by invoking * {@link SmackConfiguration#setLocalSocks5ProxyEnabled(boolean)}. The proxy is * enabled by default. * <p> * The port of the local SOCKS5 proxy can be configured by setting * <code>localSocks5ProxyPort</code> in the <code>smack-config.xml</code> or by * invoking {@link SmackConfiguration#setLocalSocks5ProxyPort(int)}. Default * port is 7777. If you set the port to a negative value Smack tries to the * absolute value and all following until it finds an open port. * <p> * If your application is running on a machine with multiple network interfaces * or if you want to provide your public address in case you are behind a NAT * router, invoke {@link #addLocalAddress(String)} or * {@link #replaceLocalAddresses(List)} to modify the list of local network * addresses used for outgoing SOCKS5 Bytestream requests. * <p> * The local SOCKS5 proxy server refuses all connections except the ones that * are explicitly allowed in the process of establishing a SOCKS5 Bytestream ( * {@link Socks5BytestreamManager#establishSession(String)}). * <p> * This Implementation has the following limitations: * <ul> * <li>only supports the no-authentication authentication method</li> * <li>only supports the <code>connect</code> command and will not answer * correctly to other commands</li> * <li>only supports requests with the domain address type and will not * correctly answer to requests with other address types</li> * </ul> * (see <a href="http://tools.ietf.org/html/rfc1928">RFC 1928</a>) * * @author Henning Staib */ public class Socks5Proxy { /** * Implementation of a simplified SOCKS5 proxy server. */ private class Socks5ServerProcess implements Runnable { /** * Negotiates a SOCKS5 connection and stores it on success. * * @param socket * connection to the client * @throws XMPPException * if client requests a connection in an unsupported way * @throws IOException * if a network error occurred */ private void establishConnection(Socket socket) throws XMPPException, IOException { final DataOutputStream out = new DataOutputStream( socket.getOutputStream()); final DataInputStream in = new DataInputStream( socket.getInputStream()); // first byte is version should be 5 int b = in.read(); if (b != 5) { throw new XMPPException("Only SOCKS5 supported"); } // second byte number of authentication methods supported b = in.read(); // read list of supported authentication methods final byte[] auth = new byte[b]; in.readFully(auth); final byte[] authMethodSelectionResponse = new byte[2]; authMethodSelectionResponse[0] = (byte) 0x05; // protocol version // only authentication method 0, no authentication, supported boolean noAuthMethodFound = false; for (final byte element : auth) { if (element == (byte) 0x00) { noAuthMethodFound = true; break; } } if (!noAuthMethodFound) { authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable // methods out.write(authMethodSelectionResponse); out.flush(); throw new XMPPException("Authentication method not supported"); } authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication // method out.write(authMethodSelectionResponse); out.flush(); // receive connection request final byte[] connectionRequest = Socks5Utils .receiveSocks5Message(in); // extract digest final String responseDigest = new String(connectionRequest, 5, connectionRequest[4]); // return error if digest is not allowed if (!allowedConnections.contains(responseDigest)) { connectionRequest[1] = (byte) 0x05; // set return status to 5 // (connection refused) out.write(connectionRequest); out.flush(); throw new XMPPException("Connection is not allowed"); } connectionRequest[1] = (byte) 0x00; // set return status to 0 // (success) out.write(connectionRequest); out.flush(); // store connection connectionMap.put(responseDigest, socket); } @Override public void run() { while (true) { Socket socket = null; try { if (serverSocket.isClosed() || Thread.currentThread().isInterrupted()) { return; } // accept connection socket = serverSocket.accept(); // initialize connection establishConnection(socket); } catch (final SocketException e) { /* * do nothing, if caused by closing the server socket, * thread will terminate in next loop */ } catch (final Exception e) { try { if (socket != null) { socket.close(); } } catch (final IOException e1) { /* do nothing */ } } } } } /* SOCKS5 proxy singleton */ private static Socks5Proxy socks5Server; /** * Returns the local SOCKS5 proxy server. * * @return the local SOCKS5 proxy server */ public static synchronized Socks5Proxy getSocks5Proxy() { if (socks5Server == null) { socks5Server = new Socks5Proxy(); } if (SmackConfiguration.isLocalSocks5ProxyEnabled()) { socks5Server.start(); } return socks5Server; } /* reusable implementation of a SOCKS5 proxy server process */ private final Socks5ServerProcess serverProcess; /* thread running the SOCKS5 server process */ private Thread serverThread; /* server socket to accept SOCKS5 connections */ private ServerSocket serverSocket; /* assigns a connection to a digest */ private final Map<String, Socket> connectionMap = new ConcurrentHashMap<String, Socket>(); /* list of digests connections should be stored */ private final List<String> allowedConnections = Collections .synchronizedList(new LinkedList<String>()); private final Set<String> localAddresses = Collections .synchronizedSet(new LinkedHashSet<String>()); /** * Private constructor. */ private Socks5Proxy() { serverProcess = new Socks5ServerProcess(); // add default local address try { localAddresses.add(InetAddress.getLocalHost().getHostAddress()); } catch (final UnknownHostException e) { // do nothing } } /** * Adds the given address to the list of local network addresses. * <p> * Use this method if you want to provide multiple addresses in a SOCKS5 * Bytestream request. This may be necessary if your application is running * on a machine with multiple network interfaces or if you want to provide * your public address in case you are behind a NAT router. * <p> * The order of the addresses used is determined by the order you add * addresses. * <p> * Note that the list of addresses initially contains the address returned * by <code>InetAddress.getLocalHost().getHostAddress()</code>. You can * replace the list of addresses by invoking * {@link #replaceLocalAddresses(List)}. * * @param address * the local network address to add */ public void addLocalAddress(String address) { if (address == null) { throw new IllegalArgumentException("address may not be null"); } localAddresses.add(address); } /** * Add the given digest to the list of allowed transfers. Only connections * for allowed transfers are stored and can be retrieved by invoking * {@link #getSocket(String)}. All connections to the local SOCKS5 proxy * that don't contain an allowed digest are discarded. * * @param digest * to be added to the list of allowed transfers */ protected void addTransfer(String digest) { allowedConnections.add(digest); } /** * Returns an unmodifiable list of the local network addresses that will be * used for streamhost candidates of outgoing SOCKS5 Bytestream requests. * * @return unmodifiable list of the local network addresses */ public List<String> getLocalAddresses() { return Collections.unmodifiableList(new ArrayList<String>( localAddresses)); } /** * Returns the port of the local SOCKS5 proxy server. If it is not running * -1 will be returned. * * @return the port of the local SOCKS5 proxy server or -1 if proxy is not * running */ public int getPort() { if (!isRunning()) { return -1; } return serverSocket.getLocalPort(); } /** * Returns the socket for the given digest. A socket will be returned if the * given digest has been in the list of allowed transfers (see * {@link #addTransfer(String)}) while the peer connected to the SOCKS5 * proxy. * * @param digest * identifying the connection * @return socket or null if there is no socket for the given digest */ protected Socket getSocket(String digest) { return connectionMap.get(digest); } /** * Returns <code>true</code> if the local SOCKS5 proxy server is running, * otherwise <code>false</code>. * * @return <code>true</code> if the local SOCKS5 proxy server is running, * otherwise <code>false</code> */ public boolean isRunning() { return serverSocket != null; } /** * Removes the given address from the list of local network addresses. This * address will then no longer be used of outgoing SOCKS5 Bytestream * requests. * * @param address * the local network address to remove */ public void removeLocalAddress(String address) { localAddresses.remove(address); } /** * Removes the given digest from the list of allowed transfers. After * invoking this method already stored connections with the given digest * will be removed. * <p> * The digest should be removed after establishing the SOCKS5 Bytestream is * finished, an error occurred while establishing the connection or if the * connection is not allowed anymore. * * @param digest * to be removed from the list of allowed transfers */ protected void removeTransfer(String digest) { allowedConnections.remove(digest); connectionMap.remove(digest); } /** * Replaces the list of local network addresses. * <p> * Use this method if you want to provide multiple addresses in a SOCKS5 * Bytestream request and want to define their order. This may be necessary * if your application is running on a machine with multiple network * interfaces or if you want to provide your public address in case you are * behind a NAT router. * * @param addresses * the new list of local network addresses */ public void replaceLocalAddresses(List<String> addresses) { if (addresses == null) { throw new IllegalArgumentException("list must not be null"); } localAddresses.clear(); localAddresses.addAll(addresses); } /** * Starts the local SOCKS5 proxy server. If it is already running, this * method does nothing. */ public synchronized void start() { if (isRunning()) { return; } try { if (SmackConfiguration.getLocalSocks5ProxyPort() < 0) { final int port = Math.abs(SmackConfiguration .getLocalSocks5ProxyPort()); for (int i = 0; i < 65535 - port; i++) { try { serverSocket = new ServerSocket(port + i); break; } catch (final IOException e) { // port is used, try next one } } } else { serverSocket = new ServerSocket( SmackConfiguration.getLocalSocks5ProxyPort()); } if (serverSocket != null) { serverThread = new Thread(serverProcess); serverThread.start(); } } catch (final IOException e) { // couldn't setup server System.err.println("couldn't setup local SOCKS5 proxy on port " + SmackConfiguration.getLocalSocks5ProxyPort() + ": " + e.getMessage()); } } /** * Stops the local SOCKS5 proxy server. If it is not running this method * does nothing. */ public synchronized void stop() { if (!isRunning()) { return; } try { serverSocket.close(); } catch (final IOException e) { // do nothing } if (serverThread != null && serverThread.isAlive()) { try { serverThread.interrupt(); serverThread.join(); } catch (final InterruptedException e) { // do nothing } } serverThread = null; serverSocket = null; } }