1/**
2 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
3 * you may not use this file except in compliance with the License.
4 * You may obtain a copy of the License at
5 *
6 *     http://www.apache.org/licenses/LICENSE-2.0
7 *
8 * Unless required by applicable law or agreed to in writing, software
9 * distributed under the License is distributed on an "AS IS" BASIS,
10 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 * See the License for the specific language governing permissions and
12 * limitations under the License.
13 */
14package org.jivesoftware.smackx.bytestreams.socks5;
15
16import java.io.DataInputStream;
17import java.io.DataOutputStream;
18import java.io.IOException;
19import java.net.InetAddress;
20import java.net.ServerSocket;
21import java.net.Socket;
22import java.net.SocketException;
23import java.net.UnknownHostException;
24import java.util.ArrayList;
25import java.util.Collections;
26import java.util.LinkedHashSet;
27import java.util.LinkedList;
28import java.util.List;
29import java.util.Map;
30import java.util.Set;
31import java.util.concurrent.ConcurrentHashMap;
32
33import org.jivesoftware.smack.SmackConfiguration;
34import org.jivesoftware.smack.XMPPException;
35
36/**
37 * The Socks5Proxy class represents a local SOCKS5 proxy server. It can be enabled/disabled by
38 * setting the <code>localSocks5ProxyEnabled</code> flag in the <code>smack-config.xml</code> or by
39 * invoking {@link SmackConfiguration#setLocalSocks5ProxyEnabled(boolean)}. The proxy is enabled by
40 * default.
41 * <p>
42 * The port of the local SOCKS5 proxy can be configured by setting <code>localSocks5ProxyPort</code>
43 * in the <code>smack-config.xml</code> or by invoking
44 * {@link SmackConfiguration#setLocalSocks5ProxyPort(int)}. Default port is 7777. If you set the
45 * port to a negative value Smack tries to the absolute value and all following until it finds an
46 * open port.
47 * <p>
48 * If your application is running on a machine with multiple network interfaces or if you want to
49 * provide your public address in case you are behind a NAT router, invoke
50 * {@link #addLocalAddress(String)} or {@link #replaceLocalAddresses(List)} to modify the list of
51 * local network addresses used for outgoing SOCKS5 Bytestream requests.
52 * <p>
53 * The local SOCKS5 proxy server refuses all connections except the ones that are explicitly allowed
54 * in the process of establishing a SOCKS5 Bytestream (
55 * {@link Socks5BytestreamManager#establishSession(String)}).
56 * <p>
57 * This Implementation has the following limitations:
58 * <ul>
59 * <li>only supports the no-authentication authentication method</li>
60 * <li>only supports the <code>connect</code> command and will not answer correctly to other
61 * commands</li>
62 * <li>only supports requests with the domain address type and will not correctly answer to requests
63 * with other address types</li>
64 * </ul>
65 * (see <a href="http://tools.ietf.org/html/rfc1928">RFC 1928</a>)
66 *
67 * @author Henning Staib
68 */
69public class Socks5Proxy {
70
71    /* SOCKS5 proxy singleton */
72    private static Socks5Proxy socks5Server;
73
74    /* reusable implementation of a SOCKS5 proxy server process */
75    private Socks5ServerProcess serverProcess;
76
77    /* thread running the SOCKS5 server process */
78    private Thread serverThread;
79
80    /* server socket to accept SOCKS5 connections */
81    private ServerSocket serverSocket;
82
83    /* assigns a connection to a digest */
84    private final Map<String, Socket> connectionMap = new ConcurrentHashMap<String, Socket>();
85
86    /* list of digests connections should be stored */
87    private final List<String> allowedConnections = Collections.synchronizedList(new LinkedList<String>());
88
89    private final Set<String> localAddresses = Collections.synchronizedSet(new LinkedHashSet<String>());
90
91    /**
92     * Private constructor.
93     */
94    private Socks5Proxy() {
95        this.serverProcess = new Socks5ServerProcess();
96
97        // add default local address
98        try {
99            this.localAddresses.add(InetAddress.getLocalHost().getHostAddress());
100        }
101        catch (UnknownHostException e) {
102            // do nothing
103        }
104
105    }
106
107    /**
108     * Returns the local SOCKS5 proxy server.
109     *
110     * @return the local SOCKS5 proxy server
111     */
112    public static synchronized Socks5Proxy getSocks5Proxy() {
113        if (socks5Server == null) {
114            socks5Server = new Socks5Proxy();
115        }
116        if (SmackConfiguration.isLocalSocks5ProxyEnabled()) {
117            socks5Server.start();
118        }
119        return socks5Server;
120    }
121
122    /**
123     * Starts the local SOCKS5 proxy server. If it is already running, this method does nothing.
124     */
125    public synchronized void start() {
126        if (isRunning()) {
127            return;
128        }
129        try {
130            if (SmackConfiguration.getLocalSocks5ProxyPort() < 0) {
131                int port = Math.abs(SmackConfiguration.getLocalSocks5ProxyPort());
132                for (int i = 0; i < 65535 - port; i++) {
133                    try {
134                        this.serverSocket = new ServerSocket(port + i);
135                        break;
136                    }
137                    catch (IOException e) {
138                        // port is used, try next one
139                    }
140                }
141            }
142            else {
143                this.serverSocket = new ServerSocket(SmackConfiguration.getLocalSocks5ProxyPort());
144            }
145
146            if (this.serverSocket != null) {
147                this.serverThread = new Thread(this.serverProcess);
148                this.serverThread.start();
149            }
150        }
151        catch (IOException e) {
152            // couldn't setup server
153            System.err.println("couldn't setup local SOCKS5 proxy on port "
154                            + SmackConfiguration.getLocalSocks5ProxyPort() + ": " + e.getMessage());
155        }
156    }
157
158    /**
159     * Stops the local SOCKS5 proxy server. If it is not running this method does nothing.
160     */
161    public synchronized void stop() {
162        if (!isRunning()) {
163            return;
164        }
165
166        try {
167            this.serverSocket.close();
168        }
169        catch (IOException e) {
170            // do nothing
171        }
172
173        if (this.serverThread != null && this.serverThread.isAlive()) {
174            try {
175                this.serverThread.interrupt();
176                this.serverThread.join();
177            }
178            catch (InterruptedException e) {
179                // do nothing
180            }
181        }
182        this.serverThread = null;
183        this.serverSocket = null;
184
185    }
186
187    /**
188     * Adds the given address to the list of local network addresses.
189     * <p>
190     * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request.
191     * This may be necessary if your application is running on a machine with multiple network
192     * interfaces or if you want to provide your public address in case you are behind a NAT router.
193     * <p>
194     * The order of the addresses used is determined by the order you add addresses.
195     * <p>
196     * Note that the list of addresses initially contains the address returned by
197     * <code>InetAddress.getLocalHost().getHostAddress()</code>. You can replace the list of
198     * addresses by invoking {@link #replaceLocalAddresses(List)}.
199     *
200     * @param address the local network address to add
201     */
202    public void addLocalAddress(String address) {
203        if (address == null) {
204            throw new IllegalArgumentException("address may not be null");
205        }
206        this.localAddresses.add(address);
207    }
208
209    /**
210     * Removes the given address from the list of local network addresses. This address will then no
211     * longer be used of outgoing SOCKS5 Bytestream requests.
212     *
213     * @param address the local network address to remove
214     */
215    public void removeLocalAddress(String address) {
216        this.localAddresses.remove(address);
217    }
218
219    /**
220     * Returns an unmodifiable list of the local network addresses that will be used for streamhost
221     * candidates of outgoing SOCKS5 Bytestream requests.
222     *
223     * @return unmodifiable list of the local network addresses
224     */
225    public List<String> getLocalAddresses() {
226        return Collections.unmodifiableList(new ArrayList<String>(this.localAddresses));
227    }
228
229    /**
230     * Replaces the list of local network addresses.
231     * <p>
232     * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request and
233     * want to define their order. This may be necessary if your application is running on a machine
234     * with multiple network interfaces or if you want to provide your public address in case you
235     * are behind a NAT router.
236     *
237     * @param addresses the new list of local network addresses
238     */
239    public void replaceLocalAddresses(List<String> addresses) {
240        if (addresses == null) {
241            throw new IllegalArgumentException("list must not be null");
242        }
243        this.localAddresses.clear();
244        this.localAddresses.addAll(addresses);
245
246    }
247
248    /**
249     * Returns the port of the local SOCKS5 proxy server. If it is not running -1 will be returned.
250     *
251     * @return the port of the local SOCKS5 proxy server or -1 if proxy is not running
252     */
253    public int getPort() {
254        if (!isRunning()) {
255            return -1;
256        }
257        return this.serverSocket.getLocalPort();
258    }
259
260    /**
261     * Returns the socket for the given digest. A socket will be returned if the given digest has
262     * been in the list of allowed transfers (see {@link #addTransfer(String)}) while the peer
263     * connected to the SOCKS5 proxy.
264     *
265     * @param digest identifying the connection
266     * @return socket or null if there is no socket for the given digest
267     */
268    protected Socket getSocket(String digest) {
269        return this.connectionMap.get(digest);
270    }
271
272    /**
273     * Add the given digest to the list of allowed transfers. Only connections for allowed transfers
274     * are stored and can be retrieved by invoking {@link #getSocket(String)}. All connections to
275     * the local SOCKS5 proxy that don't contain an allowed digest are discarded.
276     *
277     * @param digest to be added to the list of allowed transfers
278     */
279    protected void addTransfer(String digest) {
280        this.allowedConnections.add(digest);
281    }
282
283    /**
284     * Removes the given digest from the list of allowed transfers. After invoking this method
285     * already stored connections with the given digest will be removed.
286     * <p>
287     * The digest should be removed after establishing the SOCKS5 Bytestream is finished, an error
288     * occurred while establishing the connection or if the connection is not allowed anymore.
289     *
290     * @param digest to be removed from the list of allowed transfers
291     */
292    protected void removeTransfer(String digest) {
293        this.allowedConnections.remove(digest);
294        this.connectionMap.remove(digest);
295    }
296
297    /**
298     * Returns <code>true</code> if the local SOCKS5 proxy server is running, otherwise
299     * <code>false</code>.
300     *
301     * @return <code>true</code> if the local SOCKS5 proxy server is running, otherwise
302     *         <code>false</code>
303     */
304    public boolean isRunning() {
305        return this.serverSocket != null;
306    }
307
308    /**
309     * Implementation of a simplified SOCKS5 proxy server.
310     */
311    private class Socks5ServerProcess implements Runnable {
312
313        public void run() {
314            while (true) {
315                Socket socket = null;
316
317                try {
318
319                    if (Socks5Proxy.this.serverSocket.isClosed()
320                                    || Thread.currentThread().isInterrupted()) {
321                        return;
322                    }
323
324                    // accept connection
325                    socket = Socks5Proxy.this.serverSocket.accept();
326
327                    // initialize connection
328                    establishConnection(socket);
329
330                }
331                catch (SocketException e) {
332                    /*
333                     * do nothing, if caused by closing the server socket, thread will terminate in
334                     * next loop
335                     */
336                }
337                catch (Exception e) {
338                    try {
339                        if (socket != null) {
340                            socket.close();
341                        }
342                    }
343                    catch (IOException e1) {
344                        /* do nothing */
345                    }
346                }
347            }
348
349        }
350
351        /**
352         * Negotiates a SOCKS5 connection and stores it on success.
353         *
354         * @param socket connection to the client
355         * @throws XMPPException if client requests a connection in an unsupported way
356         * @throws IOException if a network error occurred
357         */
358        private void establishConnection(Socket socket) throws XMPPException, IOException {
359            DataOutputStream out = new DataOutputStream(socket.getOutputStream());
360            DataInputStream in = new DataInputStream(socket.getInputStream());
361
362            // first byte is version should be 5
363            int b = in.read();
364            if (b != 5) {
365                throw new XMPPException("Only SOCKS5 supported");
366            }
367
368            // second byte number of authentication methods supported
369            b = in.read();
370
371            // read list of supported authentication methods
372            byte[] auth = new byte[b];
373            in.readFully(auth);
374
375            byte[] authMethodSelectionResponse = new byte[2];
376            authMethodSelectionResponse[0] = (byte) 0x05; // protocol version
377
378            // only authentication method 0, no authentication, supported
379            boolean noAuthMethodFound = false;
380            for (int i = 0; i < auth.length; i++) {
381                if (auth[i] == (byte) 0x00) {
382                    noAuthMethodFound = true;
383                    break;
384                }
385            }
386
387            if (!noAuthMethodFound) {
388                authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods
389                out.write(authMethodSelectionResponse);
390                out.flush();
391                throw new XMPPException("Authentication method not supported");
392            }
393
394            authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method
395            out.write(authMethodSelectionResponse);
396            out.flush();
397
398            // receive connection request
399            byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in);
400
401            // extract digest
402            String responseDigest = new String(connectionRequest, 5, connectionRequest[4]);
403
404            // return error if digest is not allowed
405            if (!Socks5Proxy.this.allowedConnections.contains(responseDigest)) {
406                connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused)
407                out.write(connectionRequest);
408                out.flush();
409
410                throw new XMPPException("Connection is not allowed");
411            }
412
413            connectionRequest[1] = (byte) 0x00; // set return status to 0 (success)
414            out.write(connectionRequest);
415            out.flush();
416
417            // store connection
418            Socks5Proxy.this.connectionMap.put(responseDigest, socket);
419        }
420
421    }
422
423}
424