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.start();
214            }
215        }
216        catch (IOException e) {
217            // couldn't setup server
218            LOGGER.log(Level.SEVERE, "couldn't setup local SOCKS5 proxy on port " + getLocalSocks5ProxyPort(), e);
219        }
220    }
221
222    /**
223     * Stops the local SOCKS5 proxy server. If it is not running this method does nothing.
224     */
225    public synchronized void stop() {
226        if (!isRunning()) {
227            return;
228        }
229
230        try {
231            this.serverSocket.close();
232        }
233        catch (IOException e) {
234            // do nothing
235        }
236
237        if (this.serverThread != null && this.serverThread.isAlive()) {
238            try {
239                this.serverThread.interrupt();
240                this.serverThread.join();
241            }
242            catch (InterruptedException e) {
243                // do nothing
244            }
245        }
246        this.serverThread = null;
247        this.serverSocket = null;
248
249    }
250
251    /**
252     * Adds the given address to the list of local network addresses.
253     * <p>
254     * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request.
255     * This may be necessary if your application is running on a machine with multiple network
256     * interfaces or if you want to provide your public address in case you are behind a NAT router.
257     * <p>
258     * The order of the addresses used is determined by the order you add addresses.
259     * <p>
260     * Note that the list of addresses initially contains the address returned by
261     * <code>InetAddress.getLocalHost().getHostAddress()</code>. You can replace the list of
262     * addresses by invoking {@link #replaceLocalAddresses(Collection)}.
263     * 
264     * @param address the local network address to add
265     */
266    public void addLocalAddress(String address) {
267        if (address == null) {
268            return;
269        }
270        synchronized (localAddresses) {
271            this.localAddresses.add(address);
272        }
273    }
274
275    /**
276     * Removes the given address from the list of local network addresses. This address will then no
277     * longer be used of outgoing SOCKS5 Bytestream requests.
278     * 
279     * @param address the local network address to remove
280     * @return true if the address was removed.
281     */
282    public boolean removeLocalAddress(String address) {
283        synchronized (localAddresses) {
284            return localAddresses.remove(address);
285        }
286    }
287
288    /**
289     * Returns an set of the local network addresses that will be used for streamhost
290     * candidates of outgoing SOCKS5 Bytestream requests.
291     * 
292     * @return set of the local network addresses
293     */
294    public List<String> getLocalAddresses() {
295        synchronized (localAddresses) {
296            return new LinkedList<>(localAddresses);
297        }
298    }
299
300    /**
301     * Replaces the list of local network addresses.
302     * <p>
303     * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request and
304     * want to define their order. This may be necessary if your application is running on a machine
305     * with multiple network interfaces or if you want to provide your public address in case you
306     * are behind a NAT router.
307     * 
308     * @param addresses the new list of local network addresses
309     */
310    public void replaceLocalAddresses(Collection<String> addresses) {
311        if (addresses == null) {
312            throw new IllegalArgumentException("list must not be null");
313        }
314        synchronized (localAddresses) {
315            localAddresses.clear();
316            localAddresses.addAll(addresses);
317        }
318    }
319
320    /**
321     * Returns the port of the local SOCKS5 proxy server. If it is not running -1 will be returned.
322     * 
323     * @return the port of the local SOCKS5 proxy server or -1 if proxy is not running
324     */
325    public int getPort() {
326        if (!isRunning()) {
327            return -1;
328        }
329        return this.serverSocket.getLocalPort();
330    }
331
332    /**
333     * Returns the socket for the given digest. A socket will be returned if the given digest has
334     * been in the list of allowed transfers (see {@link #addTransfer(String)}) while the peer
335     * connected to the SOCKS5 proxy.
336     * 
337     * @param digest identifying the connection
338     * @return socket or null if there is no socket for the given digest
339     */
340    protected Socket getSocket(String digest) {
341        return this.connectionMap.get(digest);
342    }
343
344    /**
345     * Add the given digest to the list of allowed transfers. Only connections for allowed transfers
346     * are stored and can be retrieved by invoking {@link #getSocket(String)}. All connections to
347     * the local SOCKS5 proxy that don't contain an allowed digest are discarded.
348     * 
349     * @param digest to be added to the list of allowed transfers
350     */
351    protected void addTransfer(String digest) {
352        this.allowedConnections.add(digest);
353    }
354
355    /**
356     * Removes the given digest from the list of allowed transfers. After invoking this method
357     * already stored connections with the given digest will be removed.
358     * <p>
359     * The digest should be removed after establishing the SOCKS5 Bytestream is finished, an error
360     * occurred while establishing the connection or if the connection is not allowed anymore.
361     * 
362     * @param digest to be removed from the list of allowed transfers
363     */
364    protected void removeTransfer(String digest) {
365        this.allowedConnections.remove(digest);
366        this.connectionMap.remove(digest);
367    }
368
369    /**
370     * Returns <code>true</code> if the local SOCKS5 proxy server is running, otherwise
371     * <code>false</code>.
372     * 
373     * @return <code>true</code> if the local SOCKS5 proxy server is running, otherwise
374     *         <code>false</code>
375     */
376    public boolean isRunning() {
377        return this.serverSocket != null;
378    }
379
380    /**
381     * Implementation of a simplified SOCKS5 proxy server.
382     */
383    private class Socks5ServerProcess implements Runnable {
384
385        @Override
386        public void run() {
387            while (true) {
388                Socket socket = null;
389
390                try {
391
392                    if (Socks5Proxy.this.serverSocket == null || Socks5Proxy.this.serverSocket.isClosed()
393                                    || Thread.currentThread().isInterrupted()) {
394                        return;
395                    }
396
397                    // accept connection
398                    socket = Socks5Proxy.this.serverSocket.accept();
399
400                    // initialize connection
401                    establishConnection(socket);
402
403                }
404                catch (SocketException e) {
405                    /*
406                     * do nothing, if caused by closing the server socket, thread will terminate in
407                     * next loop
408                     */
409                }
410                catch (Exception e) {
411                    try {
412                        if (socket != null) {
413                            socket.close();
414                        }
415                    }
416                    catch (IOException e1) {
417                        /* do nothing */
418                    }
419                }
420            }
421
422        }
423
424        /**
425         * Negotiates a SOCKS5 connection and stores it on success.
426         * 
427         * @param socket connection to the client
428         * @throws SmackException if client requests a connection in an unsupported way
429         * @throws IOException if a network error occurred
430         */
431        private void establishConnection(Socket socket) throws SmackException, IOException {
432            DataOutputStream out = new DataOutputStream(socket.getOutputStream());
433            DataInputStream in = new DataInputStream(socket.getInputStream());
434
435            // first byte is version should be 5
436            int b = in.read();
437            if (b != 5) {
438                throw new SmackException("Only SOCKS5 supported");
439            }
440
441            // second byte number of authentication methods supported
442            b = in.read();
443
444            // read list of supported authentication methods
445            byte[] auth = new byte[b];
446            in.readFully(auth);
447
448            byte[] authMethodSelectionResponse = new byte[2];
449            authMethodSelectionResponse[0] = (byte) 0x05; // protocol version
450
451            // only authentication method 0, no authentication, supported
452            boolean noAuthMethodFound = false;
453            for (int i = 0; i < auth.length; i++) {
454                if (auth[i] == (byte) 0x00) {
455                    noAuthMethodFound = true;
456                    break;
457                }
458            }
459
460            if (!noAuthMethodFound) {
461                authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods
462                out.write(authMethodSelectionResponse);
463                out.flush();
464                throw new SmackException("Authentication method not supported");
465            }
466
467            authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method
468            out.write(authMethodSelectionResponse);
469            out.flush();
470
471            // receive connection request
472            byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in);
473
474            // extract digest
475            String responseDigest = new String(connectionRequest, 5, connectionRequest[4], StringUtils.UTF8);
476
477            // return error if digest is not allowed
478            if (!Socks5Proxy.this.allowedConnections.contains(responseDigest)) {
479                connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused)
480                out.write(connectionRequest);
481                out.flush();
482
483                throw new SmackException("Connection is not allowed");
484            }
485
486            connectionRequest[1] = (byte) 0x00; // set return status to 0 (success)
487            out.write(connectionRequest);
488            out.flush();
489
490            // store connection
491            Socks5Proxy.this.connectionMap.put(responseDigest, socket);
492        }
493
494    }
495
496}