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.Collection;
030import java.util.Collections;
031import java.util.Enumeration;
032import java.util.HashSet;
033import java.util.LinkedHashSet;
034import java.util.LinkedList;
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 LinkedList<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    protected Socks5Proxy(ServerSocket serverSocket) {
153        this.serverProcess = new Socks5ServerProcess();
154        this.serverSocket = serverSocket;
155
156        allowAllConnections = true;
157
158        startServerThread();
159    }
160
161
162
163   /**
164    * Returns true if the local Socks5 proxy should be started. Default is true.
165    *
166    * @return if the local Socks5 proxy should be started
167    */
168   public static boolean isLocalSocks5ProxyEnabled() {
169       return localSocks5ProxyEnabled;
170   }
171
172   /**
173    * Sets if the local Socks5 proxy should be started. Default is true.
174    *
175    * @param localSocks5ProxyEnabled if the local Socks5 proxy should be started
176    */
177   public static void setLocalSocks5ProxyEnabled(boolean localSocks5ProxyEnabled) {
178       Socks5Proxy.localSocks5ProxyEnabled = localSocks5ProxyEnabled;
179   }
180
181   private static void checkLocalSocks5ProxyPortArgument(int port) {
182       if (Math.abs(port) > 65535) {
183           throw new IllegalArgumentException("Local SOCKS5 proxy port must be within (-65535,65535)");
184       }
185   }
186
187   public static int getDefaultLocalSocks5ProxyPort() {
188       return DEFAULT_LOCAL_SOCKS5_PROXY_PORT;
189   }
190
191   public static void setDefaultLocalSocsk5ProxyPort(int defaultLocalSocks5ProxyPort) {
192       checkLocalSocks5ProxyPortArgument(defaultLocalSocks5ProxyPort);
193       DEFAULT_LOCAL_SOCKS5_PROXY_PORT = defaultLocalSocks5ProxyPort;
194   }
195
196   /**
197    * Return the port of the local Socks5 proxy. Default is 7777.
198    *
199    * @return the port of the local Socks5 proxy
200    */
201   public int getLocalSocks5ProxyPort() {
202       return localSocks5ProxyPort;
203   }
204
205   /**
206    * Sets the port of the local Socks5 proxy. Default is 7777. If you set the port to a negative
207    * value Smack tries the absolute value and all following until it finds an open port.
208    *
209    * @param localSocks5ProxyPort the port of the local Socks5 proxy to set
210    */
211   public void setLocalSocks5ProxyPort(int localSocks5ProxyPort) {
212       checkLocalSocks5ProxyPortArgument(localSocks5ProxyPort);
213       this.localSocks5ProxyPort = localSocks5ProxyPort;
214   }
215
216    /**
217     * Returns the local SOCKS5 proxy server.
218     *
219     * @return the local SOCKS5 proxy server
220     */
221    public static synchronized Socks5Proxy getSocks5Proxy() {
222        if (socks5Server == null) {
223            socks5Server = new Socks5Proxy();
224        }
225        if (isLocalSocks5ProxyEnabled()) {
226            socks5Server.start();
227        }
228        return socks5Server;
229    }
230
231    /**
232     * Starts the local SOCKS5 proxy server. If it is already running, this method does nothing.
233     *
234     * @return the server socket.
235     */
236    public synchronized ServerSocket start() {
237        if (isRunning()) {
238            return this.serverSocket;
239        }
240        try {
241            if (getLocalSocks5ProxyPort() < 0) {
242                int port = Math.abs(getLocalSocks5ProxyPort());
243                for (int i = 0; i < 65535 - port; i++) {
244                    try {
245                        this.serverSocket = new ServerSocket(port + i);
246                        break;
247                    }
248                    catch (IOException e) {
249                        // port is used, try next one
250                    }
251                }
252            }
253            else {
254                this.serverSocket = new ServerSocket(getLocalSocks5ProxyPort());
255            }
256
257            if (this.serverSocket != null) {
258                startServerThread();
259            }
260        }
261        catch (IOException e) {
262            // couldn't setup server
263            LOGGER.log(Level.SEVERE, "couldn't setup local SOCKS5 proxy on port " + getLocalSocks5ProxyPort(), e);
264        }
265
266        return this.serverSocket;
267    }
268
269    private synchronized void startServerThread() {
270        this.serverThread = new Thread(this.serverProcess);
271        this.serverThread.setName("Smack Local SOCKS5 Proxy [" + this.serverSocket + ']');
272        this.serverThread.setDaemon(true);
273
274        RUNNING_PROXIES.add(this);
275        this.serverThread.start();
276    }
277
278    /**
279     * Stops the local SOCKS5 proxy server. If it is not running this method does nothing.
280     */
281    public synchronized void stop() {
282        if (!isRunning()) {
283            return;
284        }
285
286        RUNNING_PROXIES.remove(this);
287
288        CloseableUtil.maybeClose(this.serverSocket, LOGGER);
289
290        if (this.serverThread != null && this.serverThread.isAlive()) {
291            try {
292                this.serverThread.interrupt();
293                this.serverThread.join();
294            }
295            catch (InterruptedException e) {
296                LOGGER.log(Level.WARNING, "SOCKS5 server thread termination was interrupted", e);
297            }
298        }
299        this.serverThread = null;
300        this.serverSocket = null;
301    }
302
303    /**
304     * Adds the given address to the list of local network addresses.
305     * <p>
306     * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request.
307     * This may be necessary if your application is running on a machine with multiple network
308     * interfaces or if you want to provide your public address in case you are behind a NAT router.
309     * <p>
310     * The order of the addresses used is determined by the order you add addresses.
311     * <p>
312     * Note that the list of addresses initially contains the address returned by
313     * <code>InetAddress.getLocalHost().getHostAddress()</code>. You can replace the list of
314     * addresses by invoking {@link #replaceLocalAddresses(Collection)}.
315     *
316     * @param address the local network address to add
317     */
318    public void addLocalAddress(InetAddress address) {
319        if (address == null) {
320            return;
321        }
322        synchronized (localAddresses) {
323            this.localAddresses.add(address);
324        }
325    }
326
327    /**
328     * Removes the given address from the list of local network addresses. This address will then no
329     * longer be used of outgoing SOCKS5 Bytestream requests.
330     *
331     * @param address the local network address to remove
332     * @return true if the address was removed.
333     */
334    public boolean removeLocalAddress(InetAddress address) {
335        synchronized (localAddresses) {
336            return localAddresses.remove(address);
337        }
338    }
339
340    /**
341     * Returns an set of the local network addresses that will be used for streamhost
342     * candidates of outgoing SOCKS5 Bytestream requests.
343     *
344     * @return set of the local network addresses
345     */
346    public List<InetAddress> getLocalAddresses() {
347        synchronized (localAddresses) {
348            return new LinkedList<>(localAddresses);
349        }
350    }
351
352    /**
353     * Replaces the list of local network addresses.
354     * <p>
355     * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request and
356     * want to define their order. This may be necessary if your application is running on a machine
357     * with multiple network interfaces or if you want to provide your public address in case you
358     * are behind a NAT router.
359     *
360     * @param addresses the new list of local network addresses
361     */
362    public void replaceLocalAddresses(Collection<? extends InetAddress> addresses) {
363        if (addresses == null) {
364            throw new IllegalArgumentException("list must not be null");
365        }
366        synchronized (localAddresses) {
367            localAddresses.clear();
368            localAddresses.addAll(addresses);
369        }
370    }
371
372    /**
373     * Returns the port of the local SOCKS5 proxy server. If it is not running -1 will be returned.
374     *
375     * @return the port of the local SOCKS5 proxy server or -1 if proxy is not running
376     */
377    public int getPort() {
378        if (!isRunning()) {
379            return -1;
380        }
381        return this.serverSocket.getLocalPort();
382    }
383
384    /**
385     * Returns the socket for the given digest. A socket will be returned if the given digest has
386     * been in the list of allowed transfers (see {@link #addTransfer(String)}) while the peer
387     * connected to the SOCKS5 proxy.
388     *
389     * @param digest identifying the connection
390     * @return socket or null if there is no socket for the given digest
391     */
392    protected Socket getSocket(String digest) {
393        return this.connectionMap.get(digest);
394    }
395
396    /**
397     * Add the given digest to the list of allowed transfers. Only connections for allowed transfers
398     * are stored and can be retrieved by invoking {@link #getSocket(String)}. All connections to
399     * the local SOCKS5 proxy that don't contain an allowed digest are discarded.
400     *
401     * @param digest to be added to the list of allowed transfers
402     */
403    public void addTransfer(String digest) {
404        this.allowedConnections.add(digest);
405    }
406
407    /**
408     * Removes the given digest from the list of allowed transfers. After invoking this method
409     * already stored connections with the given digest will be removed.
410     * <p>
411     * The digest should be removed after establishing the SOCKS5 Bytestream is finished, an error
412     * occurred while establishing the connection or if the connection is not allowed anymore.
413     *
414     * @param digest to be removed from the list of allowed transfers
415     */
416    protected void removeTransfer(String digest) {
417        this.allowedConnections.remove(digest);
418        this.connectionMap.remove(digest);
419    }
420
421    /**
422     * Returns <code>true</code> if the local SOCKS5 proxy server is running, otherwise
423     * <code>false</code>.
424     *
425     * @return <code>true</code> if the local SOCKS5 proxy server is running, otherwise
426     *         <code>false</code>
427     */
428    public boolean isRunning() {
429        return this.serverSocket != null;
430    }
431
432    /**
433     * Implementation of a simplified SOCKS5 proxy server.
434     */
435    private class Socks5ServerProcess implements Runnable {
436
437        @Override
438        public void run() {
439            while (true) {
440                ServerSocket serverSocket = Socks5Proxy.this.serverSocket;
441                if (serverSocket == null || serverSocket.isClosed() || Thread.currentThread().isInterrupted()) {
442                    return;
443                }
444
445                // accept connection
446                Socket socket = null;
447                try {
448                    socket = serverSocket.accept();
449                    // initialize connection
450                    establishConnection(socket);
451                } catch (SmackException | IOException e) {
452                    // Do nothing, if caused by closing the server socket, thread will terminate in next loop.
453                    LOGGER.log(Level.FINE, "Exception while " + Socks5Proxy.this + " was handling connection", e);
454                    CloseableUtil.maybeClose(socket, LOGGER);
455                }
456            }
457        }
458
459        /**
460         * Negotiates a SOCKS5 connection and stores it on success.
461         *
462         * @param socket connection to the client
463         * @throws SmackException if client requests a connection in an unsupported way
464         * @throws IOException if a network error occurred
465         */
466        private void establishConnection(Socket socket) throws SmackException, IOException {
467            DataOutputStream out = new DataOutputStream(socket.getOutputStream());
468            DataInputStream in = new DataInputStream(socket.getInputStream());
469
470            // first byte is version should be 5
471            int b = in.read();
472            if (b != 5) {
473                throw new SmackException.SmackMessageException("Only SOCKS5 supported: Peer send " + b + " but we expect 5");
474            }
475
476            // second byte number of authentication methods supported
477            b = in.read();
478
479            // read list of supported authentication methods
480            byte[] auth = new byte[b];
481            in.readFully(auth);
482
483            byte[] authMethodSelectionResponse = new byte[2];
484            authMethodSelectionResponse[0] = (byte) 0x05; // protocol version
485
486            // only authentication method 0, no authentication, supported
487            boolean noAuthMethodFound = false;
488            for (int i = 0; i < auth.length; i++) {
489                if (auth[i] == (byte) 0x00) {
490                    noAuthMethodFound = true;
491                    break;
492                }
493            }
494
495            if (!noAuthMethodFound) {
496                authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods
497                out.write(authMethodSelectionResponse);
498                out.flush();
499                throw new SmackException.SmackMessageException("Authentication method not supported");
500            }
501
502            authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method
503            out.write(authMethodSelectionResponse);
504            out.flush();
505
506            // receive connection request
507            byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in);
508
509            // extract digest
510            String responseDigest = new String(connectionRequest, 5, connectionRequest[4], StandardCharsets.UTF_8);
511
512            // return error if digest is not allowed
513            if (!allowAllConnections && !Socks5Proxy.this.allowedConnections.contains(responseDigest)) {
514                connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused)
515                out.write(connectionRequest);
516                out.flush();
517
518                throw new SmackException.SmackMessageException(
519                                "Connection with digest '" + responseDigest + "' is not allowed");
520            }
521
522            // Store the connection before we send the return status.
523            Socks5Proxy.this.connectionMap.put(responseDigest, socket);
524
525            connectionRequest[1] = (byte) 0x00; // set return status to 0 (success)
526            out.write(connectionRequest);
527            out.flush();
528        }
529
530    }
531
532    public static Socket getSocketForDigest(String digest) {
533        for (Socks5Proxy socks5Proxy : RUNNING_PROXIES) {
534            Socket socket = socks5Proxy.getSocket(digest);
535            if (socket != null) {
536                return socket;
537            }
538        }
539        return null;
540    }
541
542    static List<Socks5Proxy> getRunningProxies() {
543        return RUNNING_PROXIES;
544    }
545}