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