• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  */
14 package org.jivesoftware.smackx.bytestreams.socks5;
15 
16 import java.io.DataInputStream;
17 import java.io.DataOutputStream;
18 import java.io.IOException;
19 import java.net.InetAddress;
20 import java.net.ServerSocket;
21 import java.net.Socket;
22 import java.net.SocketException;
23 import java.net.UnknownHostException;
24 import java.util.ArrayList;
25 import java.util.Collections;
26 import java.util.LinkedHashSet;
27 import java.util.LinkedList;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Set;
31 import java.util.concurrent.ConcurrentHashMap;
32 
33 import org.jivesoftware.smack.SmackConfiguration;
34 import 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  */
69 public 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      */
Socks5Proxy()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      */
getSocks5Proxy()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      */
start()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      */
stop()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      */
addLocalAddress(String address)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      */
removeLocalAddress(String address)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      */
getLocalAddresses()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      */
replaceLocalAddresses(List<String> addresses)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      */
getPort()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      */
getSocket(String digest)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      */
addTransfer(String digest)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      */
removeTransfer(String digest)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      */
isRunning()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 
run()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          */
establishConnection(Socket socket)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