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