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