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