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