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