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