001/**
002 *
003 * Copyright the original author or authors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.bytestreams.socks5;
018
019import java.io.IOException;
020import java.net.Socket;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.LinkedList;
025import java.util.List;
026import java.util.Map;
027import java.util.Random;
028import java.util.concurrent.ConcurrentHashMap;
029import java.util.concurrent.TimeoutException;
030
031import org.jivesoftware.smack.Manager;
032import org.jivesoftware.smack.SmackException;
033import org.jivesoftware.smack.SmackException.NoResponseException;
034import org.jivesoftware.smack.SmackException.FeatureNotSupportedException;
035import org.jivesoftware.smack.SmackException.NotConnectedException;
036import org.jivesoftware.smack.XMPPConnection;
037import org.jivesoftware.smack.ConnectionCreationListener;
038import org.jivesoftware.smack.XMPPConnectionRegistry;
039import org.jivesoftware.smack.XMPPException;
040import org.jivesoftware.smack.XMPPException.XMPPErrorException;
041import org.jivesoftware.smack.packet.IQ;
042import org.jivesoftware.smack.packet.Stanza;
043import org.jivesoftware.smack.packet.XMPPError;
044import org.jivesoftware.smackx.bytestreams.BytestreamListener;
045import org.jivesoftware.smackx.bytestreams.BytestreamManager;
046import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
047import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
048import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHostUsed;
049import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
050import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
051import org.jivesoftware.smackx.disco.packet.DiscoverItems;
052import org.jivesoftware.smackx.disco.packet.DiscoverItems.Item;
053import org.jivesoftware.smackx.filetransfer.FileTransferManager;
054
055/**
056 * The Socks5BytestreamManager class handles establishing SOCKS5 Bytestreams as specified in the <a
057 * href="http://xmpp.org/extensions/xep-0065.html">XEP-0065</a>.
058 * <p>
059 * A SOCKS5 Bytestream is negotiated partly over the XMPP XML stream and partly over a separate
060 * socket. The actual transfer though takes place over a separately created socket.
061 * <p>
062 * A SOCKS5 Bytestream generally has three parties, the initiator, the target, and the stream host.
063 * The stream host is a specialized SOCKS5 proxy setup on a server, or, the initiator can act as the
064 * stream host.
065 * <p>
066 * To establish a SOCKS5 Bytestream invoke the {@link #establishSession(String)} method. This will
067 * negotiate a SOCKS5 Bytestream with the given target JID and return a socket.
068 * <p>
069 * If a session ID for the SOCKS5 Bytestream was already negotiated (e.g. while negotiating a file
070 * transfer) invoke {@link #establishSession(String, String)}.
071 * <p>
072 * To handle incoming SOCKS5 Bytestream requests add an {@link Socks5BytestreamListener} to the
073 * manager. There are two ways to add this listener. If you want to be informed about incoming
074 * SOCKS5 Bytestreams from a specific user add the listener by invoking
075 * {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the listener should
076 * respond to all SOCKS5 Bytestream requests invoke
077 * {@link #addIncomingBytestreamListener(BytestreamListener)}.
078 * <p>
079 * Note that the registered {@link Socks5BytestreamListener} will NOT be notified on incoming Socks5
080 * bytestream requests sent in the context of <a
081 * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
082 * {@link FileTransferManager})
083 * <p>
084 * If no {@link Socks5BytestreamListener}s are registered, all incoming SOCKS5 Bytestream requests
085 * will be rejected by returning a &lt;not-acceptable/&gt; error to the initiator.
086 * 
087 * @author Henning Staib
088 */
089public final class Socks5BytestreamManager extends Manager implements BytestreamManager {
090
091    /*
092     * create a new Socks5BytestreamManager and register a shutdown listener on every established
093     * connection
094     */
095    static {
096        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
097
098            public void connectionCreated(final XMPPConnection connection) {
099                // create the manager for this connection
100                Socks5BytestreamManager.getBytestreamManager(connection);
101            }
102
103        });
104    }
105
106    /* prefix used to generate session IDs */
107    private static final String SESSION_ID_PREFIX = "js5_";
108
109    /* random generator to create session IDs */
110    private final static Random randomGenerator = new Random();
111
112    /* stores one Socks5BytestreamManager for each XMPP connection */
113    private final static Map<XMPPConnection, Socks5BytestreamManager> managers = new HashMap<XMPPConnection, Socks5BytestreamManager>();
114
115    /*
116     * assigns a user to a listener that is informed if a bytestream request for this user is
117     * received
118     */
119    private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>();
120
121    /*
122     * list of listeners that respond to all bytestream requests if there are not user specific
123     * listeners for that request
124     */
125    private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());
126
127    /* listener that handles all incoming bytestream requests */
128    private final InitiationListener initiationListener;
129
130    /* timeout to wait for the response to the SOCKS5 Bytestream initialization request */
131    private int targetResponseTimeout = 10000;
132
133    /* timeout for connecting to the SOCKS5 proxy selected by the target */
134    private int proxyConnectionTimeout = 10000;
135
136    /* blacklist of errornous SOCKS5 proxies */
137    private final List<String> proxyBlacklist = Collections.synchronizedList(new LinkedList<String>());
138
139    /* remember the last proxy that worked to prioritize it */
140    private String lastWorkingProxy = null;
141
142    /* flag to enable/disable prioritization of last working proxy */
143    private boolean proxyPrioritizationEnabled = true;
144
145    /*
146     * list containing session IDs of SOCKS5 Bytestream initialization packets that should be
147     * ignored by the InitiationListener
148     */
149    private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());
150
151    /**
152     * Returns the Socks5BytestreamManager to handle SOCKS5 Bytestreams for a given
153     * {@link XMPPConnection}.
154     * <p>
155     * If no manager exists a new is created and initialized.
156     * 
157     * @param connection the XMPP connection or <code>null</code> if given connection is
158     *        <code>null</code>
159     * @return the Socks5BytestreamManager for the given XMPP connection
160     */
161    public static synchronized Socks5BytestreamManager getBytestreamManager(XMPPConnection connection) {
162        if (connection == null) {
163            return null;
164        }
165        Socks5BytestreamManager manager = managers.get(connection);
166        if (manager == null) {
167            manager = new Socks5BytestreamManager(connection);
168            managers.put(connection, manager);
169        }
170        return manager;
171    }
172
173    /**
174     * Private constructor.
175     * 
176     * @param connection the XMPP connection
177     */
178    private Socks5BytestreamManager(XMPPConnection connection) {
179        super(connection);
180        this.initiationListener = new InitiationListener(this);
181        activate();
182    }
183
184    /**
185     * Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request unless
186     * there is a user specific BytestreamListener registered.
187     * <p>
188     * If no listeners are registered all SOCKS5 Bytestream request are rejected with a
189     * &lt;not-acceptable/&gt; error.
190     * <p>
191     * Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
192     * bytestream requests sent in the context of <a
193     * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
194     * {@link FileTransferManager})
195     * 
196     * @param listener the listener to register
197     */
198    public void addIncomingBytestreamListener(BytestreamListener listener) {
199        this.allRequestListeners.add(listener);
200    }
201
202    /**
203     * Removes the given listener from the list of listeners for all incoming SOCKS5 Bytestream
204     * requests.
205     * 
206     * @param listener the listener to remove
207     */
208    public void removeIncomingBytestreamListener(BytestreamListener listener) {
209        this.allRequestListeners.remove(listener);
210    }
211
212    /**
213     * Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request from the
214     * given user.
215     * <p>
216     * Use this method if you are awaiting an incoming SOCKS5 Bytestream request from a specific
217     * user.
218     * <p>
219     * If no listeners are registered all SOCKS5 Bytestream request are rejected with a
220     * &lt;not-acceptable/&gt; error.
221     * <p>
222     * Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
223     * bytestream requests sent in the context of <a
224     * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
225     * {@link FileTransferManager})
226     * 
227     * @param listener the listener to register
228     * @param initiatorJID the JID of the user that wants to establish a SOCKS5 Bytestream
229     */
230    public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) {
231        this.userListeners.put(initiatorJID, listener);
232    }
233
234    /**
235     * Removes the listener for the given user.
236     * 
237     * @param initiatorJID the JID of the user the listener should be removed
238     */
239    public void removeIncomingBytestreamListener(String initiatorJID) {
240        this.userListeners.remove(initiatorJID);
241    }
242
243    /**
244     * Use this method to ignore the next incoming SOCKS5 Bytestream request containing the given
245     * session ID. No listeners will be notified for this request and and no error will be returned
246     * to the initiator.
247     * <p>
248     * This method should be used if you are awaiting a SOCKS5 Bytestream request as a reply to
249     * another stanza(/packet) (e.g. file transfer).
250     * 
251     * @param sessionID to be ignored
252     */
253    public void ignoreBytestreamRequestOnce(String sessionID) {
254        this.ignoredBytestreamRequests.add(sessionID);
255    }
256
257    /**
258     * Disables the SOCKS5 Bytestream manager by removing the SOCKS5 Bytestream feature from the
259     * service discovery, disabling the listener for SOCKS5 Bytestream initiation requests and
260     * resetting its internal state, which includes removing this instance from the managers map.
261     * <p>
262     * To re-enable the SOCKS5 Bytestream feature invoke {@link #getBytestreamManager(XMPPConnection)}.
263     * Using the file transfer API will automatically re-enable the SOCKS5 Bytestream feature.
264     */
265    public synchronized void disableService() {
266        XMPPConnection connection = connection();
267        // remove initiation packet listener
268        connection.unregisterIQRequestHandler(initiationListener);
269
270        // shutdown threads
271        this.initiationListener.shutdown();
272
273        // clear listeners
274        this.allRequestListeners.clear();
275        this.userListeners.clear();
276
277        // reset internal state
278        this.lastWorkingProxy = null;
279        this.proxyBlacklist.clear();
280        this.ignoredBytestreamRequests.clear();
281
282        // remove manager from static managers map
283        managers.remove(connection);
284
285        // shutdown local SOCKS5 proxy if there are no more managers for other connections
286        if (managers.size() == 0) {
287            Socks5Proxy.getSocks5Proxy().stop();
288        }
289
290        // remove feature from service discovery
291        ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
292
293        // check if service discovery is not already disposed by connection shutdown
294        if (serviceDiscoveryManager != null) {
295            serviceDiscoveryManager.removeFeature(Bytestream.NAMESPACE);
296        }
297
298    }
299
300    /**
301     * Returns the timeout to wait for the response to the SOCKS5 Bytestream initialization request.
302     * Default is 10000ms.
303     * 
304     * @return the timeout to wait for the response to the SOCKS5 Bytestream initialization request
305     */
306    public int getTargetResponseTimeout() {
307        if (this.targetResponseTimeout <= 0) {
308            this.targetResponseTimeout = 10000;
309        }
310        return targetResponseTimeout;
311    }
312
313    /**
314     * Sets the timeout to wait for the response to the SOCKS5 Bytestream initialization request.
315     * Default is 10000ms.
316     * 
317     * @param targetResponseTimeout the timeout to set
318     */
319    public void setTargetResponseTimeout(int targetResponseTimeout) {
320        this.targetResponseTimeout = targetResponseTimeout;
321    }
322
323    /**
324     * Returns the timeout for connecting to the SOCKS5 proxy selected by the target. Default is
325     * 10000ms.
326     * 
327     * @return the timeout for connecting to the SOCKS5 proxy selected by the target
328     */
329    public int getProxyConnectionTimeout() {
330        if (this.proxyConnectionTimeout <= 0) {
331            this.proxyConnectionTimeout = 10000;
332        }
333        return proxyConnectionTimeout;
334    }
335
336    /**
337     * Sets the timeout for connecting to the SOCKS5 proxy selected by the target. Default is
338     * 10000ms.
339     * 
340     * @param proxyConnectionTimeout the timeout to set
341     */
342    public void setProxyConnectionTimeout(int proxyConnectionTimeout) {
343        this.proxyConnectionTimeout = proxyConnectionTimeout;
344    }
345
346    /**
347     * Returns if the prioritization of the last working SOCKS5 proxy on successive SOCKS5
348     * Bytestream connections is enabled. Default is <code>true</code>.
349     * 
350     * @return <code>true</code> if prioritization is enabled, <code>false</code> otherwise
351     */
352    public boolean isProxyPrioritizationEnabled() {
353        return proxyPrioritizationEnabled;
354    }
355
356    /**
357     * Enable/disable the prioritization of the last working SOCKS5 proxy on successive SOCKS5
358     * Bytestream connections.
359     * 
360     * @param proxyPrioritizationEnabled enable/disable the prioritization of the last working
361     *        SOCKS5 proxy
362     */
363    public void setProxyPrioritizationEnabled(boolean proxyPrioritizationEnabled) {
364        this.proxyPrioritizationEnabled = proxyPrioritizationEnabled;
365    }
366
367    /**
368     * Establishes a SOCKS5 Bytestream with the given user and returns the Socket to send/receive
369     * data to/from the user.
370     * <p>
371     * Use this method to establish SOCKS5 Bytestreams to users accepting all incoming Socks5
372     * bytestream requests since this method doesn't provide a way to tell the user something about
373     * the data to be sent.
374     * <p>
375     * To establish a SOCKS5 Bytestream after negotiation the kind of data to be sent (e.g. file
376     * transfer) use {@link #establishSession(String, String)}.
377     * 
378     * @param targetJID the JID of the user a SOCKS5 Bytestream should be established
379     * @return the Socket to send/receive data to/from the user
380     * @throws XMPPException if the user doesn't support or accept SOCKS5 Bytestreams, if no Socks5
381     *         Proxy could be found, if the user couldn't connect to any of the SOCKS5 Proxies
382     * @throws IOException if the bytestream could not be established
383     * @throws InterruptedException if the current thread was interrupted while waiting
384     * @throws SmackException if there was no response from the server.
385     */
386    public Socks5BytestreamSession establishSession(String targetJID) throws XMPPException,
387                    IOException, InterruptedException, SmackException {
388        String sessionID = getNextSessionID();
389        return establishSession(targetJID, sessionID);
390    }
391
392    /**
393     * Establishes a SOCKS5 Bytestream with the given user using the given session ID and returns
394     * the Socket to send/receive data to/from the user.
395     * 
396     * @param targetJID the JID of the user a SOCKS5 Bytestream should be established
397     * @param sessionID the session ID for the SOCKS5 Bytestream request
398     * @return the Socket to send/receive data to/from the user
399     * @throws IOException if the bytestream could not be established
400     * @throws InterruptedException if the current thread was interrupted while waiting
401     * @throws NoResponseException 
402     * @throws SmackException if the target does not support SOCKS5.
403     * @throws XMPPException 
404     */
405    public Socks5BytestreamSession establishSession(String targetJID, String sessionID)
406                    throws IOException, InterruptedException, NoResponseException, SmackException, XMPPException{
407        XMPPConnection connection = connection();
408        XMPPErrorException discoveryException = null;
409        // check if target supports SOCKS5 Bytestream
410        if (!supportsSocks5(targetJID)) {
411            throw new FeatureNotSupportedException("SOCKS5 Bytestream", targetJID);
412        }
413
414        List<String> proxies = new ArrayList<String>();
415        // determine SOCKS5 proxies from XMPP-server
416        try {
417            proxies.addAll(determineProxies());
418        } catch (XMPPErrorException e) {
419            // don't abort here, just remember the exception thrown by determineProxies()
420            // determineStreamHostInfos() will at least add the local Socks5 proxy (if enabled)
421            discoveryException = e;
422        }
423
424        // determine address and port of each proxy
425        List<StreamHost> streamHosts = determineStreamHostInfos(proxies);
426
427        if (streamHosts.isEmpty()) {
428            if (discoveryException != null) {
429                throw discoveryException;
430            } else {
431                throw new SmackException("no SOCKS5 proxies available");
432            }
433        }
434
435        // compute digest
436        String digest = Socks5Utils.createDigest(sessionID, connection.getUser(), targetJID);
437
438        // prioritize last working SOCKS5 proxy if exists
439        if (this.proxyPrioritizationEnabled && this.lastWorkingProxy != null) {
440            StreamHost selectedStreamHost = null;
441            for (StreamHost streamHost : streamHosts) {
442                if (streamHost.getJID().equals(this.lastWorkingProxy)) {
443                    selectedStreamHost = streamHost;
444                    break;
445                }
446            }
447            if (selectedStreamHost != null) {
448                streamHosts.remove(selectedStreamHost);
449                streamHosts.add(0, selectedStreamHost);
450            }
451
452        }
453
454        Socks5Proxy socks5Proxy = Socks5Proxy.getSocks5Proxy();
455        try {
456
457            // add transfer digest to local proxy to make transfer valid
458            socks5Proxy.addTransfer(digest);
459
460            // create initiation packet
461            Bytestream initiation = createBytestreamInitiation(sessionID, targetJID, streamHosts);
462
463            // send initiation packet
464            Stanza response = connection.createPacketCollectorAndSend(initiation).nextResultOrThrow(
465                            getTargetResponseTimeout());
466
467            // extract used stream host from response
468            StreamHostUsed streamHostUsed = ((Bytestream) response).getUsedHost();
469            StreamHost usedStreamHost = initiation.getStreamHost(streamHostUsed.getJID());
470
471            if (usedStreamHost == null) {
472                throw new SmackException("Remote user responded with unknown host");
473            }
474
475            // build SOCKS5 client
476            Socks5Client socks5Client = new Socks5ClientForInitiator(usedStreamHost, digest,
477                            connection, sessionID, targetJID);
478
479            // establish connection to proxy
480            Socket socket = socks5Client.getSocket(getProxyConnectionTimeout());
481
482            // remember last working SOCKS5 proxy to prioritize it for next request
483            this.lastWorkingProxy = usedStreamHost.getJID();
484
485            // negotiation successful, return the output stream
486            return new Socks5BytestreamSession(socket, usedStreamHost.getJID().equals(
487                            connection.getUser()));
488
489        }
490        catch (TimeoutException e) {
491            throw new IOException("Timeout while connecting to SOCKS5 proxy");
492        }
493        finally {
494
495            // remove transfer digest if output stream is returned or an exception
496            // occurred
497            socks5Proxy.removeTransfer(digest);
498
499        }
500    }
501
502    /**
503     * Returns <code>true</code> if the given target JID supports feature SOCKS5 Bytestream.
504     * 
505     * @param targetJID the target JID
506     * @return <code>true</code> if the given target JID supports feature SOCKS5 Bytestream
507     *         otherwise <code>false</code>
508     * @throws XMPPErrorException 
509     * @throws NoResponseException 
510     * @throws NotConnectedException 
511     */
512    private boolean supportsSocks5(String targetJID) throws NoResponseException, XMPPErrorException, NotConnectedException {
513        return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(targetJID, Bytestream.NAMESPACE);
514    }
515
516    /**
517     * Returns a list of JIDs of SOCKS5 proxies by querying the XMPP server. The SOCKS5 proxies are
518     * in the same order as returned by the XMPP server.
519     * 
520     * @return list of JIDs of SOCKS5 proxies
521     * @throws XMPPErrorException if there was an error querying the XMPP server for SOCKS5 proxies
522     * @throws NoResponseException if there was no response from the server.
523     * @throws NotConnectedException 
524     */
525    private List<String> determineProxies() throws NoResponseException, XMPPErrorException, NotConnectedException {
526        XMPPConnection connection = connection();
527        ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
528
529        List<String> proxies = new ArrayList<String>();
530
531        // get all items from XMPP server
532        DiscoverItems discoverItems = serviceDiscoveryManager.discoverItems(connection.getServiceName());
533
534        // query all items if they are SOCKS5 proxies
535        for (Item item : discoverItems.getItems()) {
536            // skip blacklisted servers
537            if (this.proxyBlacklist.contains(item.getEntityID())) {
538                continue;
539            }
540
541            DiscoverInfo proxyInfo;
542            try {
543                proxyInfo = serviceDiscoveryManager.discoverInfo(item.getEntityID());
544            }
545            catch (NoResponseException|XMPPErrorException e) {
546                // blacklist errornous server
547                proxyBlacklist.add(item.getEntityID());
548                continue;
549            }
550
551            if (proxyInfo.hasIdentity("proxy", "bytestreams")) {
552                proxies.add(item.getEntityID());
553            } else {
554                /*
555                 * server is not a SOCKS5 proxy, blacklist server to skip next time a Socks5
556                 * bytestream should be established
557                 */
558                this.proxyBlacklist.add(item.getEntityID());
559            }
560        }
561
562        return proxies;
563    }
564
565    /**
566     * Returns a list of stream hosts containing the IP address an the port for the given list of
567     * SOCKS5 proxy JIDs. The order of the returned list is the same as the given list of JIDs
568     * excluding all SOCKS5 proxies who's network settings could not be determined. If a local
569     * SOCKS5 proxy is running it will be the first item in the list returned.
570     * 
571     * @param proxies a list of SOCKS5 proxy JIDs
572     * @return a list of stream hosts containing the IP address an the port
573     */
574    private List<StreamHost> determineStreamHostInfos(List<String> proxies) {
575        XMPPConnection connection = connection();
576        List<StreamHost> streamHosts = new ArrayList<StreamHost>();
577
578        // add local proxy on first position if exists
579        List<StreamHost> localProxies = getLocalStreamHost();
580        if (localProxies != null) {
581            streamHosts.addAll(localProxies);
582        }
583
584        // query SOCKS5 proxies for network settings
585        for (String proxy : proxies) {
586            Bytestream streamHostRequest = createStreamHostRequest(proxy);
587            try {
588                Bytestream response = (Bytestream) connection.createPacketCollectorAndSend(
589                                streamHostRequest).nextResultOrThrow();
590                streamHosts.addAll(response.getStreamHosts());
591            }
592            catch (Exception e) {
593                // blacklist errornous proxies
594                this.proxyBlacklist.add(proxy);
595            }
596        }
597
598        return streamHosts;
599    }
600
601    /**
602     * Returns a IQ stanza(/packet) to query a SOCKS5 proxy its network settings.
603     * 
604     * @param proxy the proxy to query
605     * @return IQ stanza(/packet) to query a SOCKS5 proxy its network settings
606     */
607    private Bytestream createStreamHostRequest(String proxy) {
608        Bytestream request = new Bytestream();
609        request.setType(IQ.Type.get);
610        request.setTo(proxy);
611        return request;
612    }
613
614    /**
615     * Returns the stream host information of the local SOCKS5 proxy containing the IP address and
616     * the port or null if local SOCKS5 proxy is not running.
617     * 
618     * @return the stream host information of the local SOCKS5 proxy or null if local SOCKS5 proxy
619     *         is not running
620     */
621    private List<StreamHost> getLocalStreamHost() {
622        XMPPConnection connection = connection();
623        // get local proxy singleton
624        Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy();
625
626        if (!socks5Server.isRunning()) {
627            // server is not running
628            return null;
629        }
630        List<String> addresses = socks5Server.getLocalAddresses();
631        if (addresses.isEmpty()) {
632            // local address could not be determined
633            return null;
634        }
635        final int port = socks5Server.getPort();
636
637        List<StreamHost> streamHosts = new ArrayList<StreamHost>();
638        outerloop: for (String address : addresses) {
639            // Prevent loopback addresses from appearing as streamhost
640            final String[] loopbackAddresses = { "127.0.0.1", "0:0:0:0:0:0:0:1", "::1" };
641            for (String loopbackAddress : loopbackAddresses) {
642                // Use 'startsWith' here since IPv6 addresses may have scope ID,
643                // ie. the part after the '%' sign.
644                if (address.startsWith(loopbackAddress)) {
645                    continue outerloop;
646                }
647            }
648            streamHosts.add(new StreamHost(connection.getUser(), address, port));
649        }
650        return streamHosts;
651    }
652
653    /**
654     * Returns a SOCKS5 Bytestream initialization request stanza(/packet) with the given session ID
655     * containing the given stream hosts for the given target JID.
656     * 
657     * @param sessionID the session ID for the SOCKS5 Bytestream
658     * @param targetJID the target JID of SOCKS5 Bytestream request
659     * @param streamHosts a list of SOCKS5 proxies the target should connect to
660     * @return a SOCKS5 Bytestream initialization request packet
661     */
662    private Bytestream createBytestreamInitiation(String sessionID, String targetJID,
663                    List<StreamHost> streamHosts) {
664        Bytestream initiation = new Bytestream(sessionID);
665
666        // add all stream hosts
667        for (StreamHost streamHost : streamHosts) {
668            initiation.addStreamHost(streamHost);
669        }
670
671        initiation.setType(IQ.Type.set);
672        initiation.setTo(targetJID);
673
674        return initiation;
675    }
676
677    /**
678     * Responses to the given packet's sender with an XMPP error that a SOCKS5 Bytestream is not
679     * accepted.
680     * <p>
681     * Specified in XEP-65 5.3.1 (Example 13)
682     * </p>
683     * 
684     * @param packet Stanza(/Packet) that should be answered with a not-acceptable error
685     * @throws NotConnectedException 
686     */
687    protected void replyRejectPacket(IQ packet) throws NotConnectedException {
688        XMPPError xmppError = new XMPPError(XMPPError.Condition.not_acceptable);
689        IQ errorIQ = IQ.createErrorResponse(packet, xmppError);
690        connection().sendStanza(errorIQ);
691    }
692
693    /**
694     * Activates the Socks5BytestreamManager by registering the SOCKS5 Bytestream initialization
695     * listener and enabling the SOCKS5 Bytestream feature.
696     */
697    private void activate() {
698        // register bytestream initiation packet listener
699        connection().registerIQRequestHandler(initiationListener);
700
701        // enable SOCKS5 feature
702        enableService();
703    }
704
705    /**
706     * Adds the SOCKS5 Bytestream feature to the service discovery.
707     */
708    private void enableService() {
709        ServiceDiscoveryManager manager = ServiceDiscoveryManager.getInstanceFor(connection());
710        manager.addFeature(Bytestream.NAMESPACE);
711    }
712
713    /**
714     * Returns a new unique session ID.
715     * 
716     * @return a new unique session ID
717     */
718    private String getNextSessionID() {
719        StringBuilder buffer = new StringBuilder();
720        buffer.append(SESSION_ID_PREFIX);
721        buffer.append(Math.abs(randomGenerator.nextLong()));
722        return buffer.toString();
723    }
724
725    /**
726     * Returns the XMPP connection.
727     * 
728     * @return the XMPP connection
729     */
730    protected XMPPConnection getConnection() {
731        return connection();
732    }
733
734    /**
735     * Returns the {@link BytestreamListener} that should be informed if a SOCKS5 Bytestream request
736     * from the given initiator JID is received.
737     * 
738     * @param initiator the initiator's JID
739     * @return the listener
740     */
741    protected BytestreamListener getUserListener(String initiator) {
742        return this.userListeners.get(initiator);
743    }
744
745    /**
746     * Returns a list of {@link BytestreamListener} that are informed if there are no listeners for
747     * a specific initiator.
748     * 
749     * @return list of listeners
750     */
751    protected List<BytestreamListener> getAllRequestListeners() {
752        return this.allRequestListeners;
753    }
754
755    /**
756     * Returns the list of session IDs that should be ignored by the InitialtionListener
757     * 
758     * @return list of session IDs
759     */
760    protected List<String> getIgnoredBytestreamRequests() {
761        return ignoredBytestreamRequests;
762    }
763
764}