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.IOException;
17import java.lang.ref.WeakReference;
18import java.net.Socket;
19import java.util.ArrayList;
20import java.util.Collections;
21import java.util.Iterator;
22import java.util.LinkedList;
23import java.util.List;
24import java.util.Map;
25import java.util.Random;
26import java.util.WeakHashMap;
27import java.util.concurrent.ConcurrentHashMap;
28import java.util.concurrent.TimeoutException;
29
30import org.jivesoftware.smack.AbstractConnectionListener;
31import org.jivesoftware.smack.Connection;
32import org.jivesoftware.smack.ConnectionCreationListener;
33import org.jivesoftware.smack.XMPPException;
34import org.jivesoftware.smack.packet.IQ;
35import org.jivesoftware.smack.packet.Packet;
36import org.jivesoftware.smack.packet.XMPPError;
37import org.jivesoftware.smack.util.SyncPacketSend;
38import org.jivesoftware.smackx.ServiceDiscoveryManager;
39import org.jivesoftware.smackx.bytestreams.BytestreamListener;
40import org.jivesoftware.smackx.bytestreams.BytestreamManager;
41import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
42import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
43import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHostUsed;
44import org.jivesoftware.smackx.filetransfer.FileTransferManager;
45import org.jivesoftware.smackx.packet.DiscoverInfo;
46import org.jivesoftware.smackx.packet.DiscoverItems;
47import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
48import 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 */
84public 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 {
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
773    protected List<String> getIgnoredBytestreamRequests() {
774        return ignoredBytestreamRequests;
775    }
776
777}
778