• 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.IOException;
17 import java.lang.ref.WeakReference;
18 import java.net.Socket;
19 import java.util.ArrayList;
20 import java.util.Collections;
21 import java.util.Iterator;
22 import java.util.LinkedList;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Random;
26 import java.util.WeakHashMap;
27 import java.util.concurrent.ConcurrentHashMap;
28 import java.util.concurrent.TimeoutException;
29 
30 import org.jivesoftware.smack.AbstractConnectionListener;
31 import org.jivesoftware.smack.Connection;
32 import org.jivesoftware.smack.ConnectionCreationListener;
33 import org.jivesoftware.smack.XMPPException;
34 import org.jivesoftware.smack.packet.IQ;
35 import org.jivesoftware.smack.packet.Packet;
36 import org.jivesoftware.smack.packet.XMPPError;
37 import org.jivesoftware.smack.util.SyncPacketSend;
38 import org.jivesoftware.smackx.ServiceDiscoveryManager;
39 import org.jivesoftware.smackx.bytestreams.BytestreamListener;
40 import org.jivesoftware.smackx.bytestreams.BytestreamManager;
41 import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
42 import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
43 import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHostUsed;
44 import org.jivesoftware.smackx.filetransfer.FileTransferManager;
45 import org.jivesoftware.smackx.packet.DiscoverInfo;
46 import org.jivesoftware.smackx.packet.DiscoverItems;
47 import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
48 import org.jivesoftware.smackx.packet.DiscoverItems.Item;
49 
50 /**
51  * The Socks5BytestreamManager class handles establishing SOCKS5 Bytestreams as specified in the <a
52  * href="http://xmpp.org/extensions/xep-0065.html">XEP-0065</a>.
53  * <p>
54  * A SOCKS5 Bytestream is negotiated partly over the XMPP XML stream and partly over a separate
55  * socket. The actual transfer though takes place over a separately created socket.
56  * <p>
57  * A SOCKS5 Bytestream generally has three parties, the initiator, the target, and the stream host.
58  * The stream host is a specialized SOCKS5 proxy setup on a server, or, the initiator can act as the
59  * stream host.
60  * <p>
61  * To establish a SOCKS5 Bytestream invoke the {@link #establishSession(String)} method. This will
62  * negotiate a SOCKS5 Bytestream with the given target JID and return a socket.
63  * <p>
64  * If a session ID for the SOCKS5 Bytestream was already negotiated (e.g. while negotiating a file
65  * transfer) invoke {@link #establishSession(String, String)}.
66  * <p>
67  * To handle incoming SOCKS5 Bytestream requests add an {@link Socks5BytestreamListener} to the
68  * manager. There are two ways to add this listener. If you want to be informed about incoming
69  * SOCKS5 Bytestreams from a specific user add the listener by invoking
70  * {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the listener should
71  * respond to all SOCKS5 Bytestream requests invoke
72  * {@link #addIncomingBytestreamListener(BytestreamListener)}.
73  * <p>
74  * Note that the registered {@link Socks5BytestreamListener} will NOT be notified on incoming Socks5
75  * bytestream requests sent in the context of <a
76  * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
77  * {@link FileTransferManager})
78  * <p>
79  * If no {@link Socks5BytestreamListener}s are registered, all incoming SOCKS5 Bytestream requests
80  * will be rejected by returning a &lt;not-acceptable/&gt; error to the initiator.
81  *
82  * @author Henning Staib
83  */
84 public final class Socks5BytestreamManager implements BytestreamManager {
85 
86     /*
87      * create a new Socks5BytestreamManager and register a shutdown listener on every established
88      * connection
89      */
90     static {
Connection.addConnectionCreationListener(new ConnectionCreationListener() { public void connectionCreated(final Connection connection) { final Socks5BytestreamManager manager; manager = Socks5BytestreamManager.getBytestreamManager(connection); connection.addConnectionListener(new AbstractConnectionListener() { public void connectionClosed() { manager.disableService(); } public void connectionClosedOnError(Exception e) { manager.disableService(); } public void reconnectionSuccessful() { managers.put(connection, manager); } }); } })91         Connection.addConnectionCreationListener(new ConnectionCreationListener() {
92 
93             public void connectionCreated(final Connection connection) {
94                 final Socks5BytestreamManager manager;
95                 manager = Socks5BytestreamManager.getBytestreamManager(connection);
96 
97                 // register shutdown listener
98                 connection.addConnectionListener(new AbstractConnectionListener() {
99 
100                     public void connectionClosed() {
101                         manager.disableService();
102                     }
103 
104                     public void connectionClosedOnError(Exception e) {
105                         manager.disableService();
106                     }
107 
108                     public void reconnectionSuccessful() {
109                         managers.put(connection, manager);
110                     }
111 
112                 });
113             }
114 
115         });
116     }
117 
118     /**
119      * The XMPP namespace of the SOCKS5 Bytestream
120      */
121     public static final String NAMESPACE = "http://jabber.org/protocol/bytestreams";
122 
123     /* prefix used to generate session IDs */
124     private static final String SESSION_ID_PREFIX = "js5_";
125 
126     /* random generator to create session IDs */
127     private final static Random randomGenerator = new Random();
128 
129     /* stores one Socks5BytestreamManager for each XMPP connection */
130     private final static Map<Connection, Socks5BytestreamManager> managers = new WeakHashMap<Connection, Socks5BytestreamManager>();
131 
132     /* XMPP connection */
133     private final Connection connection;
134 
135     /*
136      * assigns a user to a listener that is informed if a bytestream request for this user is
137      * received
138      */
139     private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>();
140 
141     /*
142      * list of listeners that respond to all bytestream requests if there are not user specific
143      * listeners for that request
144      */
145     private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());
146 
147     /* listener that handles all incoming bytestream requests */
148     private final InitiationListener initiationListener;
149 
150     /* timeout to wait for the response to the SOCKS5 Bytestream initialization request */
151     private int targetResponseTimeout = 10000;
152 
153     /* timeout for connecting to the SOCKS5 proxy selected by the target */
154     private int proxyConnectionTimeout = 10000;
155 
156     /* blacklist of errornous SOCKS5 proxies */
157     private final List<String> proxyBlacklist = Collections.synchronizedList(new LinkedList<String>());
158 
159     /* remember the last proxy that worked to prioritize it */
160     private String lastWorkingProxy = null;
161 
162     /* flag to enable/disable prioritization of last working proxy */
163     private boolean proxyPrioritizationEnabled = true;
164 
165     /*
166      * list containing session IDs of SOCKS5 Bytestream initialization packets that should be
167      * ignored by the InitiationListener
168      */
169     private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());
170 
171     /**
172      * Returns the Socks5BytestreamManager to handle SOCKS5 Bytestreams for a given
173      * {@link Connection}.
174      * <p>
175      * If no manager exists a new is created and initialized.
176      *
177      * @param connection the XMPP connection or <code>null</code> if given connection is
178      *        <code>null</code>
179      * @return the Socks5BytestreamManager for the given XMPP connection
180      */
getBytestreamManager(Connection connection)181     public static synchronized Socks5BytestreamManager getBytestreamManager(Connection connection) {
182         if (connection == null) {
183             return null;
184         }
185         Socks5BytestreamManager manager = managers.get(connection);
186         if (manager == null) {
187             manager = new Socks5BytestreamManager(connection);
188             managers.put(connection, manager);
189             manager.activate();
190         }
191         return manager;
192     }
193 
194     /**
195      * Private constructor.
196      *
197      * @param connection the XMPP connection
198      */
Socks5BytestreamManager(Connection connection)199     private Socks5BytestreamManager(Connection connection) {
200         this.connection = connection;
201         this.initiationListener = new InitiationListener(this);
202     }
203 
204     /**
205      * Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request unless
206      * there is a user specific BytestreamListener registered.
207      * <p>
208      * If no listeners are registered all SOCKS5 Bytestream request are rejected with a
209      * &lt;not-acceptable/&gt; error.
210      * <p>
211      * Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
212      * bytestream requests sent in the context of <a
213      * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
214      * {@link FileTransferManager})
215      *
216      * @param listener the listener to register
217      */
addIncomingBytestreamListener(BytestreamListener listener)218     public void addIncomingBytestreamListener(BytestreamListener listener) {
219         this.allRequestListeners.add(listener);
220     }
221 
222     /**
223      * Removes the given listener from the list of listeners for all incoming SOCKS5 Bytestream
224      * requests.
225      *
226      * @param listener the listener to remove
227      */
removeIncomingBytestreamListener(BytestreamListener listener)228     public void removeIncomingBytestreamListener(BytestreamListener listener) {
229         this.allRequestListeners.remove(listener);
230     }
231 
232     /**
233      * Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request from the
234      * given user.
235      * <p>
236      * Use this method if you are awaiting an incoming SOCKS5 Bytestream request from a specific
237      * user.
238      * <p>
239      * If no listeners are registered all SOCKS5 Bytestream request are rejected with a
240      * &lt;not-acceptable/&gt; error.
241      * <p>
242      * Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
243      * bytestream requests sent in the context of <a
244      * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
245      * {@link FileTransferManager})
246      *
247      * @param listener the listener to register
248      * @param initiatorJID the JID of the user that wants to establish a SOCKS5 Bytestream
249      */
addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID)250     public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) {
251         this.userListeners.put(initiatorJID, listener);
252     }
253 
254     /**
255      * Removes the listener for the given user.
256      *
257      * @param initiatorJID the JID of the user the listener should be removed
258      */
removeIncomingBytestreamListener(String initiatorJID)259     public void removeIncomingBytestreamListener(String initiatorJID) {
260         this.userListeners.remove(initiatorJID);
261     }
262 
263     /**
264      * Use this method to ignore the next incoming SOCKS5 Bytestream request containing the given
265      * session ID. No listeners will be notified for this request and and no error will be returned
266      * to the initiator.
267      * <p>
268      * This method should be used if you are awaiting a SOCKS5 Bytestream request as a reply to
269      * another packet (e.g. file transfer).
270      *
271      * @param sessionID to be ignored
272      */
ignoreBytestreamRequestOnce(String sessionID)273     public void ignoreBytestreamRequestOnce(String sessionID) {
274         this.ignoredBytestreamRequests.add(sessionID);
275     }
276 
277     /**
278      * Disables the SOCKS5 Bytestream manager by removing the SOCKS5 Bytestream feature from the
279      * service discovery, disabling the listener for SOCKS5 Bytestream initiation requests and
280      * resetting its internal state.
281      * <p>
282      * To re-enable the SOCKS5 Bytestream feature invoke {@link #getBytestreamManager(Connection)}.
283      * Using the file transfer API will automatically re-enable the SOCKS5 Bytestream feature.
284      */
disableService()285     public synchronized void disableService() {
286 
287         // remove initiation packet listener
288         this.connection.removePacketListener(this.initiationListener);
289 
290         // shutdown threads
291         this.initiationListener.shutdown();
292 
293         // clear listeners
294         this.allRequestListeners.clear();
295         this.userListeners.clear();
296 
297         // reset internal state
298         this.lastWorkingProxy = null;
299         this.proxyBlacklist.clear();
300         this.ignoredBytestreamRequests.clear();
301 
302         // remove manager from static managers map
303         managers.remove(this.connection);
304 
305         // shutdown local SOCKS5 proxy if there are no more managers for other connections
306         if (managers.size() == 0) {
307             Socks5Proxy.getSocks5Proxy().stop();
308         }
309 
310         // remove feature from service discovery
311         ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
312 
313         // check if service discovery is not already disposed by connection shutdown
314         if (serviceDiscoveryManager != null) {
315             serviceDiscoveryManager.removeFeature(NAMESPACE);
316         }
317 
318     }
319 
320     /**
321      * Returns the timeout to wait for the response to the SOCKS5 Bytestream initialization request.
322      * Default is 10000ms.
323      *
324      * @return the timeout to wait for the response to the SOCKS5 Bytestream initialization request
325      */
getTargetResponseTimeout()326     public int getTargetResponseTimeout() {
327         if (this.targetResponseTimeout <= 0) {
328             this.targetResponseTimeout = 10000;
329         }
330         return targetResponseTimeout;
331     }
332 
333     /**
334      * Sets the timeout to wait for the response to the SOCKS5 Bytestream initialization request.
335      * Default is 10000ms.
336      *
337      * @param targetResponseTimeout the timeout to set
338      */
setTargetResponseTimeout(int targetResponseTimeout)339     public void setTargetResponseTimeout(int targetResponseTimeout) {
340         this.targetResponseTimeout = targetResponseTimeout;
341     }
342 
343     /**
344      * Returns the timeout for connecting to the SOCKS5 proxy selected by the target. Default is
345      * 10000ms.
346      *
347      * @return the timeout for connecting to the SOCKS5 proxy selected by the target
348      */
getProxyConnectionTimeout()349     public int getProxyConnectionTimeout() {
350         if (this.proxyConnectionTimeout <= 0) {
351             this.proxyConnectionTimeout = 10000;
352         }
353         return proxyConnectionTimeout;
354     }
355 
356     /**
357      * Sets the timeout for connecting to the SOCKS5 proxy selected by the target. Default is
358      * 10000ms.
359      *
360      * @param proxyConnectionTimeout the timeout to set
361      */
setProxyConnectionTimeout(int proxyConnectionTimeout)362     public void setProxyConnectionTimeout(int proxyConnectionTimeout) {
363         this.proxyConnectionTimeout = proxyConnectionTimeout;
364     }
365 
366     /**
367      * Returns if the prioritization of the last working SOCKS5 proxy on successive SOCKS5
368      * Bytestream connections is enabled. Default is <code>true</code>.
369      *
370      * @return <code>true</code> if prioritization is enabled, <code>false</code> otherwise
371      */
isProxyPrioritizationEnabled()372     public boolean isProxyPrioritizationEnabled() {
373         return proxyPrioritizationEnabled;
374     }
375 
376     /**
377      * Enable/disable the prioritization of the last working SOCKS5 proxy on successive SOCKS5
378      * Bytestream connections.
379      *
380      * @param proxyPrioritizationEnabled enable/disable the prioritization of the last working
381      *        SOCKS5 proxy
382      */
setProxyPrioritizationEnabled(boolean proxyPrioritizationEnabled)383     public void setProxyPrioritizationEnabled(boolean proxyPrioritizationEnabled) {
384         this.proxyPrioritizationEnabled = proxyPrioritizationEnabled;
385     }
386 
387     /**
388      * Establishes a SOCKS5 Bytestream with the given user and returns the Socket to send/receive
389      * data to/from the user.
390      * <p>
391      * Use this method to establish SOCKS5 Bytestreams to users accepting all incoming Socks5
392      * bytestream requests since this method doesn't provide a way to tell the user something about
393      * the data to be sent.
394      * <p>
395      * To establish a SOCKS5 Bytestream after negotiation the kind of data to be sent (e.g. file
396      * transfer) use {@link #establishSession(String, String)}.
397      *
398      * @param targetJID the JID of the user a SOCKS5 Bytestream should be established
399      * @return the Socket to send/receive data to/from the user
400      * @throws XMPPException if the user doesn't support or accept SOCKS5 Bytestreams, if no Socks5
401      *         Proxy could be found, if the user couldn't connect to any of the SOCKS5 Proxies
402      * @throws IOException if the bytestream could not be established
403      * @throws InterruptedException if the current thread was interrupted while waiting
404      */
establishSession(String targetJID)405     public Socks5BytestreamSession establishSession(String targetJID) throws XMPPException,
406                     IOException, InterruptedException {
407         String sessionID = getNextSessionID();
408         return establishSession(targetJID, sessionID);
409     }
410 
411     /**
412      * Establishes a SOCKS5 Bytestream with the given user using the given session ID and returns
413      * the Socket to send/receive data to/from the user.
414      *
415      * @param targetJID the JID of the user a SOCKS5 Bytestream should be established
416      * @param sessionID the session ID for the SOCKS5 Bytestream request
417      * @return the Socket to send/receive data to/from the user
418      * @throws XMPPException if the user doesn't support or accept SOCKS5 Bytestreams, if no Socks5
419      *         Proxy could be found, if the user couldn't connect to any of the SOCKS5 Proxies
420      * @throws IOException if the bytestream could not be established
421      * @throws InterruptedException if the current thread was interrupted while waiting
422      */
establishSession(String targetJID, String sessionID)423     public Socks5BytestreamSession establishSession(String targetJID, String sessionID)
424                     throws XMPPException, IOException, InterruptedException {
425 
426         XMPPException discoveryException = null;
427         // check if target supports SOCKS5 Bytestream
428         if (!supportsSocks5(targetJID)) {
429             throw new XMPPException(targetJID + " doesn't support SOCKS5 Bytestream");
430         }
431 
432         List<String> proxies = new ArrayList<String>();
433         // determine SOCKS5 proxies from XMPP-server
434         try {
435             proxies.addAll(determineProxies());
436         } catch (XMPPException e) {
437             // don't abort here, just remember the exception thrown by determineProxies()
438             // determineStreamHostInfos() will at least add the local Socks5 proxy (if enabled)
439             discoveryException = e;
440         }
441 
442         // determine address and port of each proxy
443         List<StreamHost> streamHosts = determineStreamHostInfos(proxies);
444 
445         if (streamHosts.isEmpty()) {
446             throw discoveryException != null ? discoveryException : new XMPPException("no SOCKS5 proxies available");
447         }
448 
449         // compute digest
450         String digest = Socks5Utils.createDigest(sessionID, this.connection.getUser(), targetJID);
451 
452         // prioritize last working SOCKS5 proxy if exists
453         if (this.proxyPrioritizationEnabled && this.lastWorkingProxy != null) {
454             StreamHost selectedStreamHost = null;
455             for (StreamHost streamHost : streamHosts) {
456                 if (streamHost.getJID().equals(this.lastWorkingProxy)) {
457                     selectedStreamHost = streamHost;
458                     break;
459                 }
460             }
461             if (selectedStreamHost != null) {
462                 streamHosts.remove(selectedStreamHost);
463                 streamHosts.add(0, selectedStreamHost);
464             }
465 
466         }
467 
468         Socks5Proxy socks5Proxy = Socks5Proxy.getSocks5Proxy();
469         try {
470 
471             // add transfer digest to local proxy to make transfer valid
472             socks5Proxy.addTransfer(digest);
473 
474             // create initiation packet
475             Bytestream initiation = createBytestreamInitiation(sessionID, targetJID, streamHosts);
476 
477             // send initiation packet
478             Packet response = SyncPacketSend.getReply(this.connection, initiation,
479                             getTargetResponseTimeout());
480 
481             // extract used stream host from response
482             StreamHostUsed streamHostUsed = ((Bytestream) response).getUsedHost();
483             StreamHost usedStreamHost = initiation.getStreamHost(streamHostUsed.getJID());
484 
485             if (usedStreamHost == null) {
486                 throw new XMPPException("Remote user responded with unknown host");
487             }
488 
489             // build SOCKS5 client
490             Socks5Client socks5Client = new Socks5ClientForInitiator(usedStreamHost, digest,
491                             this.connection, sessionID, targetJID);
492 
493             // establish connection to proxy
494             Socket socket = socks5Client.getSocket(getProxyConnectionTimeout());
495 
496             // remember last working SOCKS5 proxy to prioritize it for next request
497             this.lastWorkingProxy = usedStreamHost.getJID();
498 
499             // negotiation successful, return the output stream
500             return new Socks5BytestreamSession(socket, usedStreamHost.getJID().equals(
501                             this.connection.getUser()));
502 
503         }
504         catch (TimeoutException e) {
505             throw new IOException("Timeout while connecting to SOCKS5 proxy");
506         }
507         finally {
508 
509             // remove transfer digest if output stream is returned or an exception
510             // occurred
511             socks5Proxy.removeTransfer(digest);
512 
513         }
514     }
515 
516     /**
517      * Returns <code>true</code> if the given target JID supports feature SOCKS5 Bytestream.
518      *
519      * @param targetJID the target JID
520      * @return <code>true</code> if the given target JID supports feature SOCKS5 Bytestream
521      *         otherwise <code>false</code>
522      * @throws XMPPException if there was an error querying target for supported features
523      */
supportsSocks5(String targetJID)524     private boolean supportsSocks5(String targetJID) throws XMPPException {
525         ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
526         DiscoverInfo discoverInfo = serviceDiscoveryManager.discoverInfo(targetJID);
527         return discoverInfo.containsFeature(NAMESPACE);
528     }
529 
530     /**
531      * Returns a list of JIDs of SOCKS5 proxies by querying the XMPP server. The SOCKS5 proxies are
532      * in the same order as returned by the XMPP server.
533      *
534      * @return list of JIDs of SOCKS5 proxies
535      * @throws XMPPException if there was an error querying the XMPP server for SOCKS5 proxies
536      */
determineProxies()537     private List<String> determineProxies() throws XMPPException {
538         ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
539 
540         List<String> proxies = new ArrayList<String>();
541 
542         // get all items form XMPP server
543         DiscoverItems discoverItems = serviceDiscoveryManager.discoverItems(this.connection.getServiceName());
544         Iterator<Item> itemIterator = discoverItems.getItems();
545 
546         // query all items if they are SOCKS5 proxies
547         while (itemIterator.hasNext()) {
548             Item item = itemIterator.next();
549 
550             // skip blacklisted servers
551             if (this.proxyBlacklist.contains(item.getEntityID())) {
552                 continue;
553             }
554 
555             try {
556                 DiscoverInfo proxyInfo;
557                 proxyInfo = serviceDiscoveryManager.discoverInfo(item.getEntityID());
558                 Iterator<Identity> identities = proxyInfo.getIdentities();
559 
560                 // item must have category "proxy" and type "bytestream"
561                 while (identities.hasNext()) {
562                     Identity identity = identities.next();
563 
564                     if ("proxy".equalsIgnoreCase(identity.getCategory())
565                                     && "bytestreams".equalsIgnoreCase(identity.getType())) {
566                         proxies.add(item.getEntityID());
567                         break;
568                     }
569 
570                     /*
571                      * server is not a SOCKS5 proxy, blacklist server to skip next time a Socks5
572                      * bytestream should be established
573                      */
574                     this.proxyBlacklist.add(item.getEntityID());
575 
576                 }
577             }
578             catch (XMPPException e) {
579                 // blacklist errornous server
580                 this.proxyBlacklist.add(item.getEntityID());
581             }
582         }
583 
584         return proxies;
585     }
586 
587     /**
588      * Returns a list of stream hosts containing the IP address an the port for the given list of
589      * SOCKS5 proxy JIDs. The order of the returned list is the same as the given list of JIDs
590      * excluding all SOCKS5 proxies who's network settings could not be determined. If a local
591      * SOCKS5 proxy is running it will be the first item in the list returned.
592      *
593      * @param proxies a list of SOCKS5 proxy JIDs
594      * @return a list of stream hosts containing the IP address an the port
595      */
determineStreamHostInfos(List<String> proxies)596     private List<StreamHost> determineStreamHostInfos(List<String> proxies) {
597         List<StreamHost> streamHosts = new ArrayList<StreamHost>();
598 
599         // add local proxy on first position if exists
600         List<StreamHost> localProxies = getLocalStreamHost();
601         if (localProxies != null) {
602             streamHosts.addAll(localProxies);
603         }
604 
605         // query SOCKS5 proxies for network settings
606         for (String proxy : proxies) {
607             Bytestream streamHostRequest = createStreamHostRequest(proxy);
608             try {
609                 Bytestream response = (Bytestream) SyncPacketSend.getReply(this.connection,
610                                 streamHostRequest);
611                 streamHosts.addAll(response.getStreamHosts());
612             }
613             catch (XMPPException e) {
614                 // blacklist errornous proxies
615                 this.proxyBlacklist.add(proxy);
616             }
617         }
618 
619         return streamHosts;
620     }
621 
622     /**
623      * Returns a IQ packet to query a SOCKS5 proxy its network settings.
624      *
625      * @param proxy the proxy to query
626      * @return IQ packet to query a SOCKS5 proxy its network settings
627      */
createStreamHostRequest(String proxy)628     private Bytestream createStreamHostRequest(String proxy) {
629         Bytestream request = new Bytestream();
630         request.setType(IQ.Type.GET);
631         request.setTo(proxy);
632         return request;
633     }
634 
635     /**
636      * Returns the stream host information of the local SOCKS5 proxy containing the IP address and
637      * the port or null if local SOCKS5 proxy is not running.
638      *
639      * @return the stream host information of the local SOCKS5 proxy or null if local SOCKS5 proxy
640      *         is not running
641      */
getLocalStreamHost()642     private List<StreamHost> getLocalStreamHost() {
643 
644         // get local proxy singleton
645         Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy();
646 
647         if (socks5Server.isRunning()) {
648             List<String> addresses = socks5Server.getLocalAddresses();
649             int port = socks5Server.getPort();
650 
651             if (addresses.size() >= 1) {
652                 List<StreamHost> streamHosts = new ArrayList<StreamHost>();
653                 for (String address : addresses) {
654                     StreamHost streamHost = new StreamHost(this.connection.getUser(), address);
655                     streamHost.setPort(port);
656                     streamHosts.add(streamHost);
657                 }
658                 return streamHosts;
659             }
660 
661         }
662 
663         // server is not running or local address could not be determined
664         return null;
665     }
666 
667     /**
668      * Returns a SOCKS5 Bytestream initialization request packet with the given session ID
669      * containing the given stream hosts for the given target JID.
670      *
671      * @param sessionID the session ID for the SOCKS5 Bytestream
672      * @param targetJID the target JID of SOCKS5 Bytestream request
673      * @param streamHosts a list of SOCKS5 proxies the target should connect to
674      * @return a SOCKS5 Bytestream initialization request packet
675      */
createBytestreamInitiation(String sessionID, String targetJID, List<StreamHost> streamHosts)676     private Bytestream createBytestreamInitiation(String sessionID, String targetJID,
677                     List<StreamHost> streamHosts) {
678         Bytestream initiation = new Bytestream(sessionID);
679 
680         // add all stream hosts
681         for (StreamHost streamHost : streamHosts) {
682             initiation.addStreamHost(streamHost);
683         }
684 
685         initiation.setType(IQ.Type.SET);
686         initiation.setTo(targetJID);
687 
688         return initiation;
689     }
690 
691     /**
692      * Responses to the given packet's sender with a XMPP error that a SOCKS5 Bytestream is not
693      * accepted.
694      *
695      * @param packet Packet that should be answered with a not-acceptable error
696      */
replyRejectPacket(IQ packet)697     protected void replyRejectPacket(IQ packet) {
698         XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable);
699         IQ errorIQ = IQ.createErrorResponse(packet, xmppError);
700         this.connection.sendPacket(errorIQ);
701     }
702 
703     /**
704      * Activates the Socks5BytestreamManager by registering the SOCKS5 Bytestream initialization
705      * listener and enabling the SOCKS5 Bytestream feature.
706      */
activate()707     private void activate() {
708         // register bytestream initiation packet listener
709         this.connection.addPacketListener(this.initiationListener,
710                         this.initiationListener.getFilter());
711 
712         // enable SOCKS5 feature
713         enableService();
714     }
715 
716     /**
717      * Adds the SOCKS5 Bytestream feature to the service discovery.
718      */
enableService()719     private void enableService() {
720         ServiceDiscoveryManager manager = ServiceDiscoveryManager.getInstanceFor(this.connection);
721         if (!manager.includesFeature(NAMESPACE)) {
722             manager.addFeature(NAMESPACE);
723         }
724     }
725 
726     /**
727      * Returns a new unique session ID.
728      *
729      * @return a new unique session ID
730      */
getNextSessionID()731     private String getNextSessionID() {
732         StringBuilder buffer = new StringBuilder();
733         buffer.append(SESSION_ID_PREFIX);
734         buffer.append(Math.abs(randomGenerator.nextLong()));
735         return buffer.toString();
736     }
737 
738     /**
739      * Returns the XMPP connection.
740      *
741      * @return the XMPP connection
742      */
getConnection()743     protected Connection getConnection() {
744         return this.connection;
745     }
746 
747     /**
748      * Returns the {@link BytestreamListener} that should be informed if a SOCKS5 Bytestream request
749      * from the given initiator JID is received.
750      *
751      * @param initiator the initiator's JID
752      * @return the listener
753      */
getUserListener(String initiator)754     protected BytestreamListener getUserListener(String initiator) {
755         return this.userListeners.get(initiator);
756     }
757 
758     /**
759      * Returns a list of {@link BytestreamListener} that are informed if there are no listeners for
760      * a specific initiator.
761      *
762      * @return list of listeners
763      */
getAllRequestListeners()764     protected List<BytestreamListener> getAllRequestListeners() {
765         return this.allRequestListeners;
766     }
767 
768     /**
769      * Returns the list of session IDs that should be ignored by the InitialtionListener
770      *
771      * @return list of session IDs
772      */
getIgnoredBytestreamRequests()773     protected List<String> getIgnoredBytestreamRequests() {
774         return ignoredBytestreamRequests;
775     }
776 
777 }
778