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