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.AbstractConnectionClosedListener;
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 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                // register shutdown listener
103                connection.addConnectionListener(new AbstractConnectionClosedListener() {
104
105                    @Override
106                    public void connectionTerminated() {
107                        Socks5BytestreamManager.getBytestreamManager(connection).disableService();
108                    }
109
110                    @Override
111                    public void reconnectionSuccessful() {
112                        // re-create the manager for this connection
113                        Socks5BytestreamManager.getBytestreamManager(connection);
114                    }
115
116                });
117            }
118
119        });
120    }
121
122    /* prefix used to generate session IDs */
123    private static final String SESSION_ID_PREFIX = "js5_";
124
125    /* random generator to create session IDs */
126    private final static Random randomGenerator = new Random();
127
128    /* stores one Socks5BytestreamManager for each XMPP connection */
129    private final static Map<XMPPConnection, Socks5BytestreamManager> managers = new HashMap<XMPPConnection, Socks5BytestreamManager>();
130
131    /* XMPP connection */
132    private final XMPPConnection connection;
133
134    /*
135     * assigns a user to a listener that is informed if a bytestream request for this user is
136     * received
137     */
138    private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>();
139
140    /*
141     * list of listeners that respond to all bytestream requests if there are not user specific
142     * listeners for that request
143     */
144    private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());
145
146    /* listener that handles all incoming bytestream requests */
147    private final InitiationListener initiationListener;
148
149    /* timeout to wait for the response to the SOCKS5 Bytestream initialization request */
150    private int targetResponseTimeout = 10000;
151
152    /* timeout for connecting to the SOCKS5 proxy selected by the target */
153    private int proxyConnectionTimeout = 10000;
154
155    /* blacklist of errornous SOCKS5 proxies */
156    private final List<String> proxyBlacklist = Collections.synchronizedList(new LinkedList<String>());
157
158    /* remember the last proxy that worked to prioritize it */
159    private String lastWorkingProxy = null;
160
161    /* flag to enable/disable prioritization of last working proxy */
162    private boolean proxyPrioritizationEnabled = true;
163
164    /*
165     * list containing session IDs of SOCKS5 Bytestream initialization packets that should be
166     * ignored by the InitiationListener
167     */
168    private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());
169
170    /**
171     * Returns the Socks5BytestreamManager to handle SOCKS5 Bytestreams for a given
172     * {@link XMPPConnection}.
173     * <p>
174     * If no manager exists a new is created and initialized.
175     * 
176     * @param connection the XMPP connection or <code>null</code> if given connection is
177     *        <code>null</code>
178     * @return the Socks5BytestreamManager for the given XMPP connection
179     */
180    public static synchronized Socks5BytestreamManager getBytestreamManager(XMPPConnection connection) {
181        if (connection == null) {
182            return null;
183        }
184        Socks5BytestreamManager manager = managers.get(connection);
185        if (manager == null) {
186            manager = new Socks5BytestreamManager(connection);
187            managers.put(connection, manager);
188            manager.activate();
189        }
190        return manager;
191    }
192
193    /**
194     * Private constructor.
195     * 
196     * @param connection the XMPP connection
197     */
198    private Socks5BytestreamManager(XMPPConnection connection) {
199        this.connection = connection;
200        this.initiationListener = new InitiationListener(this);
201    }
202
203    /**
204     * Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request unless
205     * there is a user specific BytestreamListener registered.
206     * <p>
207     * If no listeners are registered all SOCKS5 Bytestream request are rejected with a
208     * &lt;not-acceptable/&gt; error.
209     * <p>
210     * Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
211     * bytestream requests sent in the context of <a
212     * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
213     * {@link FileTransferManager})
214     * 
215     * @param listener the listener to register
216     */
217    public void addIncomingBytestreamListener(BytestreamListener listener) {
218        this.allRequestListeners.add(listener);
219    }
220
221    /**
222     * Removes the given listener from the list of listeners for all incoming SOCKS5 Bytestream
223     * requests.
224     * 
225     * @param listener the listener to remove
226     */
227    public void removeIncomingBytestreamListener(BytestreamListener listener) {
228        this.allRequestListeners.remove(listener);
229    }
230
231    /**
232     * Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request from the
233     * given user.
234     * <p>
235     * Use this method if you are awaiting an incoming SOCKS5 Bytestream request from a specific
236     * user.
237     * <p>
238     * If no listeners are registered all SOCKS5 Bytestream request are rejected with a
239     * &lt;not-acceptable/&gt; error.
240     * <p>
241     * Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
242     * bytestream requests sent in the context of <a
243     * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
244     * {@link FileTransferManager})
245     * 
246     * @param listener the listener to register
247     * @param initiatorJID the JID of the user that wants to establish a SOCKS5 Bytestream
248     */
249    public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) {
250        this.userListeners.put(initiatorJID, listener);
251    }
252
253    /**
254     * Removes the listener for the given user.
255     * 
256     * @param initiatorJID the JID of the user the listener should be removed
257     */
258    public void removeIncomingBytestreamListener(String initiatorJID) {
259        this.userListeners.remove(initiatorJID);
260    }
261
262    /**
263     * Use this method to ignore the next incoming SOCKS5 Bytestream request containing the given
264     * session ID. No listeners will be notified for this request and and no error will be returned
265     * to the initiator.
266     * <p>
267     * This method should be used if you are awaiting a SOCKS5 Bytestream request as a reply to
268     * another stanza(/packet) (e.g. file transfer).
269     * 
270     * @param sessionID to be ignored
271     */
272    public void ignoreBytestreamRequestOnce(String sessionID) {
273        this.ignoredBytestreamRequests.add(sessionID);
274    }
275
276    /**
277     * Disables the SOCKS5 Bytestream manager by removing the SOCKS5 Bytestream feature from the
278     * service discovery, disabling the listener for SOCKS5 Bytestream initiation requests and
279     * resetting its internal state, which includes removing this instance from the managers map.
280     * <p>
281     * To re-enable the SOCKS5 Bytestream feature invoke {@link #getBytestreamManager(XMPPConnection)}.
282     * Using the file transfer API will automatically re-enable the SOCKS5 Bytestream feature.
283     */
284    public synchronized void disableService() {
285
286        // remove initiation packet listener
287        connection.unregisterIQRequestHandler(initiationListener);
288
289        // shutdown threads
290        this.initiationListener.shutdown();
291
292        // clear listeners
293        this.allRequestListeners.clear();
294        this.userListeners.clear();
295
296        // reset internal state
297        this.lastWorkingProxy = null;
298        this.proxyBlacklist.clear();
299        this.ignoredBytestreamRequests.clear();
300
301        // remove manager from static managers map
302        managers.remove(this.connection);
303
304        // shutdown local SOCKS5 proxy if there are no more managers for other connections
305        if (managers.size() == 0) {
306            Socks5Proxy.getSocks5Proxy().stop();
307        }
308
309        // remove feature from service discovery
310        ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
311
312        // check if service discovery is not already disposed by connection shutdown
313        if (serviceDiscoveryManager != null) {
314            serviceDiscoveryManager.removeFeature(Bytestream.NAMESPACE);
315        }
316
317    }
318
319    /**
320     * Returns the timeout to wait for the response to the SOCKS5 Bytestream initialization request.
321     * Default is 10000ms.
322     * 
323     * @return the timeout to wait for the response to the SOCKS5 Bytestream initialization request
324     */
325    public int getTargetResponseTimeout() {
326        if (this.targetResponseTimeout <= 0) {
327            this.targetResponseTimeout = 10000;
328        }
329        return targetResponseTimeout;
330    }
331
332    /**
333     * Sets the timeout to wait for the response to the SOCKS5 Bytestream initialization request.
334     * Default is 10000ms.
335     * 
336     * @param targetResponseTimeout the timeout to set
337     */
338    public void setTargetResponseTimeout(int targetResponseTimeout) {
339        this.targetResponseTimeout = targetResponseTimeout;
340    }
341
342    /**
343     * Returns the timeout for connecting to the SOCKS5 proxy selected by the target. Default is
344     * 10000ms.
345     * 
346     * @return the timeout for connecting to the SOCKS5 proxy selected by the target
347     */
348    public int getProxyConnectionTimeout() {
349        if (this.proxyConnectionTimeout <= 0) {
350            this.proxyConnectionTimeout = 10000;
351        }
352        return proxyConnectionTimeout;
353    }
354
355    /**
356     * Sets the timeout for connecting to the SOCKS5 proxy selected by the target. Default is
357     * 10000ms.
358     * 
359     * @param proxyConnectionTimeout the timeout to set
360     */
361    public void setProxyConnectionTimeout(int proxyConnectionTimeout) {
362        this.proxyConnectionTimeout = proxyConnectionTimeout;
363    }
364
365    /**
366     * Returns if the prioritization of the last working SOCKS5 proxy on successive SOCKS5
367     * Bytestream connections is enabled. Default is <code>true</code>.
368     * 
369     * @return <code>true</code> if prioritization is enabled, <code>false</code> otherwise
370     */
371    public boolean isProxyPrioritizationEnabled() {
372        return proxyPrioritizationEnabled;
373    }
374
375    /**
376     * Enable/disable the prioritization of the last working SOCKS5 proxy on successive SOCKS5
377     * Bytestream connections.
378     * 
379     * @param proxyPrioritizationEnabled enable/disable the prioritization of the last working
380     *        SOCKS5 proxy
381     */
382    public void setProxyPrioritizationEnabled(boolean proxyPrioritizationEnabled) {
383        this.proxyPrioritizationEnabled = proxyPrioritizationEnabled;
384    }
385
386    /**
387     * Establishes a SOCKS5 Bytestream with the given user and returns the Socket to send/receive
388     * data to/from the user.
389     * <p>
390     * Use this method to establish SOCKS5 Bytestreams to users accepting all incoming Socks5
391     * bytestream requests since this method doesn't provide a way to tell the user something about
392     * the data to be sent.
393     * <p>
394     * To establish a SOCKS5 Bytestream after negotiation the kind of data to be sent (e.g. file
395     * transfer) use {@link #establishSession(String, String)}.
396     * 
397     * @param targetJID the JID of the user a SOCKS5 Bytestream should be established
398     * @return the Socket to send/receive data to/from the user
399     * @throws XMPPException if the user doesn't support or accept SOCKS5 Bytestreams, if no Socks5
400     *         Proxy could be found, if the user couldn't connect to any of the SOCKS5 Proxies
401     * @throws IOException if the bytestream could not be established
402     * @throws InterruptedException if the current thread was interrupted while waiting
403     * @throws SmackException if there was no response from the server.
404     */
405    public Socks5BytestreamSession establishSession(String targetJID) throws XMPPException,
406                    IOException, InterruptedException, SmackException {
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 IOException if the bytestream could not be established
419     * @throws InterruptedException if the current thread was interrupted while waiting
420     * @throws NoResponseException 
421     * @throws SmackException if the target does not support SOCKS5.
422     * @throws XMPPException 
423     */
424    public Socks5BytestreamSession establishSession(String targetJID, String sessionID)
425                    throws IOException, InterruptedException, NoResponseException, SmackException, XMPPException{
426
427        XMPPErrorException discoveryException = null;
428        // check if target supports SOCKS5 Bytestream
429        if (!supportsSocks5(targetJID)) {
430            throw new FeatureNotSupportedException("SOCKS5 Bytestream", targetJID);
431        }
432
433        List<String> proxies = new ArrayList<String>();
434        // determine SOCKS5 proxies from XMPP-server
435        try {
436            proxies.addAll(determineProxies());
437        } catch (XMPPErrorException e) {
438            // don't abort here, just remember the exception thrown by determineProxies()
439            // determineStreamHostInfos() will at least add the local Socks5 proxy (if enabled)
440            discoveryException = e;
441        }
442
443        // determine address and port of each proxy
444        List<StreamHost> streamHosts = determineStreamHostInfos(proxies);
445
446        if (streamHosts.isEmpty()) {
447            if (discoveryException != null) {
448                throw discoveryException;
449            } else {
450                throw new SmackException("no SOCKS5 proxies available");
451            }
452        }
453
454        // compute digest
455        String digest = Socks5Utils.createDigest(sessionID, this.connection.getUser(), targetJID);
456
457        // prioritize last working SOCKS5 proxy if exists
458        if (this.proxyPrioritizationEnabled && this.lastWorkingProxy != null) {
459            StreamHost selectedStreamHost = null;
460            for (StreamHost streamHost : streamHosts) {
461                if (streamHost.getJID().equals(this.lastWorkingProxy)) {
462                    selectedStreamHost = streamHost;
463                    break;
464                }
465            }
466            if (selectedStreamHost != null) {
467                streamHosts.remove(selectedStreamHost);
468                streamHosts.add(0, selectedStreamHost);
469            }
470
471        }
472
473        Socks5Proxy socks5Proxy = Socks5Proxy.getSocks5Proxy();
474        try {
475
476            // add transfer digest to local proxy to make transfer valid
477            socks5Proxy.addTransfer(digest);
478
479            // create initiation packet
480            Bytestream initiation = createBytestreamInitiation(sessionID, targetJID, streamHosts);
481
482            // send initiation packet
483            Stanza response = connection.createPacketCollectorAndSend(initiation).nextResultOrThrow(
484                            getTargetResponseTimeout());
485
486            // extract used stream host from response
487            StreamHostUsed streamHostUsed = ((Bytestream) response).getUsedHost();
488            StreamHost usedStreamHost = initiation.getStreamHost(streamHostUsed.getJID());
489
490            if (usedStreamHost == null) {
491                throw new SmackException("Remote user responded with unknown host");
492            }
493
494            // build SOCKS5 client
495            Socks5Client socks5Client = new Socks5ClientForInitiator(usedStreamHost, digest,
496                            this.connection, sessionID, targetJID);
497
498            // establish connection to proxy
499            Socket socket = socks5Client.getSocket(getProxyConnectionTimeout());
500
501            // remember last working SOCKS5 proxy to prioritize it for next request
502            this.lastWorkingProxy = usedStreamHost.getJID();
503
504            // negotiation successful, return the output stream
505            return new Socks5BytestreamSession(socket, usedStreamHost.getJID().equals(
506                            this.connection.getUser()));
507
508        }
509        catch (TimeoutException e) {
510            throw new IOException("Timeout while connecting to SOCKS5 proxy");
511        }
512        finally {
513
514            // remove transfer digest if output stream is returned or an exception
515            // occurred
516            socks5Proxy.removeTransfer(digest);
517
518        }
519    }
520
521    /**
522     * Returns <code>true</code> if the given target JID supports feature SOCKS5 Bytestream.
523     * 
524     * @param targetJID the target JID
525     * @return <code>true</code> if the given target JID supports feature SOCKS5 Bytestream
526     *         otherwise <code>false</code>
527     * @throws XMPPErrorException 
528     * @throws NoResponseException 
529     * @throws NotConnectedException 
530     */
531    private boolean supportsSocks5(String targetJID) throws NoResponseException, XMPPErrorException, NotConnectedException {
532        return ServiceDiscoveryManager.getInstanceFor(connection).supportsFeature(targetJID, Bytestream.NAMESPACE);
533    }
534
535    /**
536     * Returns a list of JIDs of SOCKS5 proxies by querying the XMPP server. The SOCKS5 proxies are
537     * in the same order as returned by the XMPP server.
538     * 
539     * @return list of JIDs of SOCKS5 proxies
540     * @throws XMPPErrorException if there was an error querying the XMPP server for SOCKS5 proxies
541     * @throws NoResponseException if there was no response from the server.
542     * @throws NotConnectedException 
543     */
544    private List<String> determineProxies() throws NoResponseException, XMPPErrorException, NotConnectedException {
545        ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
546
547        List<String> proxies = new ArrayList<String>();
548
549        // get all items from XMPP server
550        DiscoverItems discoverItems = serviceDiscoveryManager.discoverItems(this.connection.getServiceName());
551
552        // query all items if they are SOCKS5 proxies
553        for (Item item : discoverItems.getItems()) {
554            // skip blacklisted servers
555            if (this.proxyBlacklist.contains(item.getEntityID())) {
556                continue;
557            }
558
559            DiscoverInfo proxyInfo;
560            try {
561                proxyInfo = serviceDiscoveryManager.discoverInfo(item.getEntityID());
562            }
563            catch (NoResponseException|XMPPErrorException e) {
564                // blacklist errornous server
565                proxyBlacklist.add(item.getEntityID());
566                continue;
567            }
568
569            if (proxyInfo.hasIdentity("proxy", "bytestreams")) {
570                proxies.add(item.getEntityID());
571            } else {
572                /*
573                 * server is not a SOCKS5 proxy, blacklist server to skip next time a Socks5
574                 * bytestream should be established
575                 */
576                this.proxyBlacklist.add(item.getEntityID());
577            }
578        }
579
580        return proxies;
581    }
582
583    /**
584     * Returns a list of stream hosts containing the IP address an the port for the given list of
585     * SOCKS5 proxy JIDs. The order of the returned list is the same as the given list of JIDs
586     * excluding all SOCKS5 proxies who's network settings could not be determined. If a local
587     * SOCKS5 proxy is running it will be the first item in the list returned.
588     * 
589     * @param proxies a list of SOCKS5 proxy JIDs
590     * @return a list of stream hosts containing the IP address an the port
591     */
592    private List<StreamHost> determineStreamHostInfos(List<String> proxies) {
593        List<StreamHost> streamHosts = new ArrayList<StreamHost>();
594
595        // add local proxy on first position if exists
596        List<StreamHost> localProxies = getLocalStreamHost();
597        if (localProxies != null) {
598            streamHosts.addAll(localProxies);
599        }
600
601        // query SOCKS5 proxies for network settings
602        for (String proxy : proxies) {
603            Bytestream streamHostRequest = createStreamHostRequest(proxy);
604            try {
605                Bytestream response = (Bytestream) connection.createPacketCollectorAndSend(
606                                streamHostRequest).nextResultOrThrow();
607                streamHosts.addAll(response.getStreamHosts());
608            }
609            catch (Exception e) {
610                // blacklist errornous proxies
611                this.proxyBlacklist.add(proxy);
612            }
613        }
614
615        return streamHosts;
616    }
617
618    /**
619     * Returns a IQ stanza(/packet) to query a SOCKS5 proxy its network settings.
620     * 
621     * @param proxy the proxy to query
622     * @return IQ stanza(/packet) to query a SOCKS5 proxy its network settings
623     */
624    private Bytestream createStreamHostRequest(String proxy) {
625        Bytestream request = new Bytestream();
626        request.setType(IQ.Type.get);
627        request.setTo(proxy);
628        return request;
629    }
630
631    /**
632     * Returns the stream host information of the local SOCKS5 proxy containing the IP address and
633     * the port or null if local SOCKS5 proxy is not running.
634     * 
635     * @return the stream host information of the local SOCKS5 proxy or null if local SOCKS5 proxy
636     *         is not running
637     */
638    private List<StreamHost> getLocalStreamHost() {
639
640        // get local proxy singleton
641        Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy();
642
643        if (!socks5Server.isRunning()) {
644            // server is not running
645            return null;
646        }
647        List<String> addresses = socks5Server.getLocalAddresses();
648        if (addresses.isEmpty()) {
649            // local address could not be determined
650            return null;
651        }
652        final int port = socks5Server.getPort();
653
654        List<StreamHost> streamHosts = new ArrayList<StreamHost>();
655        outerloop: for (String address : addresses) {
656            // Prevent loopback addresses from appearing as streamhost
657            final String[] loopbackAddresses = { "127.0.0.1", "0:0:0:0:0:0:0:1", "::1" };
658            for (String loopbackAddress : loopbackAddresses) {
659                // Use 'startsWith' here since IPv6 addresses may have scope ID,
660                // ie. the part after the '%' sign.
661                if (address.startsWith(loopbackAddress)) {
662                    continue outerloop;
663                }
664            }
665            streamHosts.add(new StreamHost(connection.getUser(), address, port));
666        }
667        return streamHosts;
668    }
669
670    /**
671     * Returns a SOCKS5 Bytestream initialization request stanza(/packet) with the given session ID
672     * containing the given stream hosts for the given target JID.
673     * 
674     * @param sessionID the session ID for the SOCKS5 Bytestream
675     * @param targetJID the target JID of SOCKS5 Bytestream request
676     * @param streamHosts a list of SOCKS5 proxies the target should connect to
677     * @return a SOCKS5 Bytestream initialization request packet
678     */
679    private Bytestream createBytestreamInitiation(String sessionID, String targetJID,
680                    List<StreamHost> streamHosts) {
681        Bytestream initiation = new Bytestream(sessionID);
682
683        // add all stream hosts
684        for (StreamHost streamHost : streamHosts) {
685            initiation.addStreamHost(streamHost);
686        }
687
688        initiation.setType(IQ.Type.set);
689        initiation.setTo(targetJID);
690
691        return initiation;
692    }
693
694    /**
695     * Responses to the given packet's sender with an XMPP error that a SOCKS5 Bytestream is not
696     * accepted.
697     * <p>
698     * Specified in XEP-65 5.3.1 (Example 13)
699     * </p>
700     * 
701     * @param packet Stanza(/Packet) that should be answered with a not-acceptable error
702     * @throws NotConnectedException 
703     */
704    protected void replyRejectPacket(IQ packet) throws NotConnectedException {
705        XMPPError xmppError = new XMPPError(XMPPError.Condition.not_acceptable);
706        IQ errorIQ = IQ.createErrorResponse(packet, xmppError);
707        this.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(this.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 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 this.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(String 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}