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