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