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