aboutsummaryrefslogtreecommitdiff
path: root/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java')
-rw-r--r--src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java777
1 files changed, 777 insertions, 0 deletions
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java
new file mode 100644
index 0000000..1383495
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java
@@ -0,0 +1,777 @@
+/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.socks5;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeoutException;
+
+import org.jivesoftware.smack.AbstractConnectionListener;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.ConnectionCreationListener;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.SyncPacketSend;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.bytestreams.BytestreamListener;
+import org.jivesoftware.smackx.bytestreams.BytestreamManager;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHostUsed;
+import org.jivesoftware.smackx.filetransfer.FileTransferManager;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DiscoverItems;
+import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
+import org.jivesoftware.smackx.packet.DiscoverItems.Item;
+
+/**
+ * The Socks5BytestreamManager class handles establishing SOCKS5 Bytestreams as specified in the <a
+ * href="http://xmpp.org/extensions/xep-0065.html">XEP-0065</a>.
+ * <p>
+ * A SOCKS5 Bytestream is negotiated partly over the XMPP XML stream and partly over a separate
+ * socket. The actual transfer though takes place over a separately created socket.
+ * <p>
+ * A SOCKS5 Bytestream generally has three parties, the initiator, the target, and the stream host.
+ * The stream host is a specialized SOCKS5 proxy setup on a server, or, the initiator can act as the
+ * stream host.
+ * <p>
+ * To establish a SOCKS5 Bytestream invoke the {@link #establishSession(String)} method. This will
+ * negotiate a SOCKS5 Bytestream with the given target JID and return a socket.
+ * <p>
+ * If a session ID for the SOCKS5 Bytestream was already negotiated (e.g. while negotiating a file
+ * transfer) invoke {@link #establishSession(String, String)}.
+ * <p>
+ * To handle incoming SOCKS5 Bytestream requests add an {@link Socks5BytestreamListener} to the
+ * manager. There are two ways to add this listener. If you want to be informed about incoming
+ * SOCKS5 Bytestreams from a specific user add the listener by invoking
+ * {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the listener should
+ * respond to all SOCKS5 Bytestream requests invoke
+ * {@link #addIncomingBytestreamListener(BytestreamListener)}.
+ * <p>
+ * Note that the registered {@link Socks5BytestreamListener} will NOT be notified on incoming Socks5
+ * bytestream requests sent in the context of <a
+ * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
+ * {@link FileTransferManager})
+ * <p>
+ * If no {@link Socks5BytestreamListener}s are registered, all incoming SOCKS5 Bytestream requests
+ * will be rejected by returning a &lt;not-acceptable/&gt; error to the initiator.
+ *
+ * @author Henning Staib
+ */
+public final class Socks5BytestreamManager implements BytestreamManager {
+
+ /*
+ * create a new Socks5BytestreamManager and register a shutdown listener on every established
+ * connection
+ */
+ static {
+ Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+
+ public void connectionCreated(final Connection connection) {
+ final Socks5BytestreamManager manager;
+ manager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ // register shutdown listener
+ connection.addConnectionListener(new AbstractConnectionListener() {
+
+ public void connectionClosed() {
+ manager.disableService();
+ }
+
+ public void connectionClosedOnError(Exception e) {
+ manager.disableService();
+ }
+
+ public void reconnectionSuccessful() {
+ managers.put(connection, manager);
+ }
+
+ });
+ }
+
+ });
+ }
+
+ /**
+ * The XMPP namespace of the SOCKS5 Bytestream
+ */
+ public static final String NAMESPACE = "http://jabber.org/protocol/bytestreams";
+
+ /* prefix used to generate session IDs */
+ private static final String SESSION_ID_PREFIX = "js5_";
+
+ /* random generator to create session IDs */
+ private final static Random randomGenerator = new Random();
+
+ /* stores one Socks5BytestreamManager for each XMPP connection */
+ private final static Map<Connection, Socks5BytestreamManager> managers = new WeakHashMap<Connection, Socks5BytestreamManager>();
+
+ /* XMPP connection */
+ private final Connection connection;
+
+ /*
+ * assigns a user to a listener that is informed if a bytestream request for this user is
+ * received
+ */
+ private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>();
+
+ /*
+ * list of listeners that respond to all bytestream requests if there are not user specific
+ * listeners for that request
+ */
+ private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());
+
+ /* listener that handles all incoming bytestream requests */
+ private final InitiationListener initiationListener;
+
+ /* timeout to wait for the response to the SOCKS5 Bytestream initialization request */
+ private int targetResponseTimeout = 10000;
+
+ /* timeout for connecting to the SOCKS5 proxy selected by the target */
+ private int proxyConnectionTimeout = 10000;
+
+ /* blacklist of errornous SOCKS5 proxies */
+ private final List<String> proxyBlacklist = Collections.synchronizedList(new LinkedList<String>());
+
+ /* remember the last proxy that worked to prioritize it */
+ private String lastWorkingProxy = null;
+
+ /* flag to enable/disable prioritization of last working proxy */
+ private boolean proxyPrioritizationEnabled = true;
+
+ /*
+ * list containing session IDs of SOCKS5 Bytestream initialization packets that should be
+ * ignored by the InitiationListener
+ */
+ private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());
+
+ /**
+ * Returns the Socks5BytestreamManager to handle SOCKS5 Bytestreams for a given
+ * {@link Connection}.
+ * <p>
+ * If no manager exists a new is created and initialized.
+ *
+ * @param connection the XMPP connection or <code>null</code> if given connection is
+ * <code>null</code>
+ * @return the Socks5BytestreamManager for the given XMPP connection
+ */
+ public static synchronized Socks5BytestreamManager getBytestreamManager(Connection connection) {
+ if (connection == null) {
+ return null;
+ }
+ Socks5BytestreamManager manager = managers.get(connection);
+ if (manager == null) {
+ manager = new Socks5BytestreamManager(connection);
+ managers.put(connection, manager);
+ manager.activate();
+ }
+ return manager;
+ }
+
+ /**
+ * Private constructor.
+ *
+ * @param connection the XMPP connection
+ */
+ private Socks5BytestreamManager(Connection connection) {
+ this.connection = connection;
+ this.initiationListener = new InitiationListener(this);
+ }
+
+ /**
+ * Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request unless
+ * there is a user specific BytestreamListener registered.
+ * <p>
+ * If no listeners are registered all SOCKS5 Bytestream request are rejected with a
+ * &lt;not-acceptable/&gt; error.
+ * <p>
+ * Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
+ * bytestream requests sent in the context of <a
+ * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
+ * {@link FileTransferManager})
+ *
+ * @param listener the listener to register
+ */
+ public void addIncomingBytestreamListener(BytestreamListener listener) {
+ this.allRequestListeners.add(listener);
+ }
+
+ /**
+ * Removes the given listener from the list of listeners for all incoming SOCKS5 Bytestream
+ * requests.
+ *
+ * @param listener the listener to remove
+ */
+ public void removeIncomingBytestreamListener(BytestreamListener listener) {
+ this.allRequestListeners.remove(listener);
+ }
+
+ /**
+ * Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request from the
+ * given user.
+ * <p>
+ * Use this method if you are awaiting an incoming SOCKS5 Bytestream request from a specific
+ * user.
+ * <p>
+ * If no listeners are registered all SOCKS5 Bytestream request are rejected with a
+ * &lt;not-acceptable/&gt; error.
+ * <p>
+ * Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
+ * bytestream requests sent in the context of <a
+ * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
+ * {@link FileTransferManager})
+ *
+ * @param listener the listener to register
+ * @param initiatorJID the JID of the user that wants to establish a SOCKS5 Bytestream
+ */
+ public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) {
+ this.userListeners.put(initiatorJID, listener);
+ }
+
+ /**
+ * Removes the listener for the given user.
+ *
+ * @param initiatorJID the JID of the user the listener should be removed
+ */
+ public void removeIncomingBytestreamListener(String initiatorJID) {
+ this.userListeners.remove(initiatorJID);
+ }
+
+ /**
+ * Use this method to ignore the next incoming SOCKS5 Bytestream request containing the given
+ * session ID. No listeners will be notified for this request and and no error will be returned
+ * to the initiator.
+ * <p>
+ * This method should be used if you are awaiting a SOCKS5 Bytestream request as a reply to
+ * another packet (e.g. file transfer).
+ *
+ * @param sessionID to be ignored
+ */
+ public void ignoreBytestreamRequestOnce(String sessionID) {
+ this.ignoredBytestreamRequests.add(sessionID);
+ }
+
+ /**
+ * Disables the SOCKS5 Bytestream manager by removing the SOCKS5 Bytestream feature from the
+ * service discovery, disabling the listener for SOCKS5 Bytestream initiation requests and
+ * resetting its internal state.
+ * <p>
+ * To re-enable the SOCKS5 Bytestream feature invoke {@link #getBytestreamManager(Connection)}.
+ * Using the file transfer API will automatically re-enable the SOCKS5 Bytestream feature.
+ */
+ public synchronized void disableService() {
+
+ // remove initiation packet listener
+ this.connection.removePacketListener(this.initiationListener);
+
+ // shutdown threads
+ this.initiationListener.shutdown();
+
+ // clear listeners
+ this.allRequestListeners.clear();
+ this.userListeners.clear();
+
+ // reset internal state
+ this.lastWorkingProxy = null;
+ this.proxyBlacklist.clear();
+ this.ignoredBytestreamRequests.clear();
+
+ // remove manager from static managers map
+ managers.remove(this.connection);
+
+ // shutdown local SOCKS5 proxy if there are no more managers for other connections
+ if (managers.size() == 0) {
+ Socks5Proxy.getSocks5Proxy().stop();
+ }
+
+ // remove feature from service discovery
+ ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
+
+ // check if service discovery is not already disposed by connection shutdown
+ if (serviceDiscoveryManager != null) {
+ serviceDiscoveryManager.removeFeature(NAMESPACE);
+ }
+
+ }
+
+ /**
+ * Returns the timeout to wait for the response to the SOCKS5 Bytestream initialization request.
+ * Default is 10000ms.
+ *
+ * @return the timeout to wait for the response to the SOCKS5 Bytestream initialization request
+ */
+ public int getTargetResponseTimeout() {
+ if (this.targetResponseTimeout <= 0) {
+ this.targetResponseTimeout = 10000;
+ }
+ return targetResponseTimeout;
+ }
+
+ /**
+ * Sets the timeout to wait for the response to the SOCKS5 Bytestream initialization request.
+ * Default is 10000ms.
+ *
+ * @param targetResponseTimeout the timeout to set
+ */
+ public void setTargetResponseTimeout(int targetResponseTimeout) {
+ this.targetResponseTimeout = targetResponseTimeout;
+ }
+
+ /**
+ * Returns the timeout for connecting to the SOCKS5 proxy selected by the target. Default is
+ * 10000ms.
+ *
+ * @return the timeout for connecting to the SOCKS5 proxy selected by the target
+ */
+ public int getProxyConnectionTimeout() {
+ if (this.proxyConnectionTimeout <= 0) {
+ this.proxyConnectionTimeout = 10000;
+ }
+ return proxyConnectionTimeout;
+ }
+
+ /**
+ * Sets the timeout for connecting to the SOCKS5 proxy selected by the target. Default is
+ * 10000ms.
+ *
+ * @param proxyConnectionTimeout the timeout to set
+ */
+ public void setProxyConnectionTimeout(int proxyConnectionTimeout) {
+ this.proxyConnectionTimeout = proxyConnectionTimeout;
+ }
+
+ /**
+ * Returns if the prioritization of the last working SOCKS5 proxy on successive SOCKS5
+ * Bytestream connections is enabled. Default is <code>true</code>.
+ *
+ * @return <code>true</code> if prioritization is enabled, <code>false</code> otherwise
+ */
+ public boolean isProxyPrioritizationEnabled() {
+ return proxyPrioritizationEnabled;
+ }
+
+ /**
+ * Enable/disable the prioritization of the last working SOCKS5 proxy on successive SOCKS5
+ * Bytestream connections.
+ *
+ * @param proxyPrioritizationEnabled enable/disable the prioritization of the last working
+ * SOCKS5 proxy
+ */
+ public void setProxyPrioritizationEnabled(boolean proxyPrioritizationEnabled) {
+ this.proxyPrioritizationEnabled = proxyPrioritizationEnabled;
+ }
+
+ /**
+ * Establishes a SOCKS5 Bytestream with the given user and returns the Socket to send/receive
+ * data to/from the user.
+ * <p>
+ * Use this method to establish SOCKS5 Bytestreams to users accepting all incoming Socks5
+ * bytestream requests since this method doesn't provide a way to tell the user something about
+ * the data to be sent.
+ * <p>
+ * To establish a SOCKS5 Bytestream after negotiation the kind of data to be sent (e.g. file
+ * transfer) use {@link #establishSession(String, String)}.
+ *
+ * @param targetJID the JID of the user a SOCKS5 Bytestream should be established
+ * @return the Socket to send/receive data to/from the user
+ * @throws XMPPException if the user doesn't support or accept SOCKS5 Bytestreams, if no Socks5
+ * Proxy could be found, if the user couldn't connect to any of the SOCKS5 Proxies
+ * @throws IOException if the bytestream could not be established
+ * @throws InterruptedException if the current thread was interrupted while waiting
+ */
+ public Socks5BytestreamSession establishSession(String targetJID) throws XMPPException,
+ IOException, InterruptedException {
+ String sessionID = getNextSessionID();
+ return establishSession(targetJID, sessionID);
+ }
+
+ /**
+ * Establishes a SOCKS5 Bytestream with the given user using the given session ID and returns
+ * the Socket to send/receive data to/from the user.
+ *
+ * @param targetJID the JID of the user a SOCKS5 Bytestream should be established
+ * @param sessionID the session ID for the SOCKS5 Bytestream request
+ * @return the Socket to send/receive data to/from the user
+ * @throws XMPPException if the user doesn't support or accept SOCKS5 Bytestreams, if no Socks5
+ * Proxy could be found, if the user couldn't connect to any of the SOCKS5 Proxies
+ * @throws IOException if the bytestream could not be established
+ * @throws InterruptedException if the current thread was interrupted while waiting
+ */
+ public Socks5BytestreamSession establishSession(String targetJID, String sessionID)
+ throws XMPPException, IOException, InterruptedException {
+
+ XMPPException discoveryException = null;
+ // check if target supports SOCKS5 Bytestream
+ if (!supportsSocks5(targetJID)) {
+ throw new XMPPException(targetJID + " doesn't support SOCKS5 Bytestream");
+ }
+
+ List<String> proxies = new ArrayList<String>();
+ // determine SOCKS5 proxies from XMPP-server
+ try {
+ proxies.addAll(determineProxies());
+ } catch (XMPPException e) {
+ // don't abort here, just remember the exception thrown by determineProxies()
+ // determineStreamHostInfos() will at least add the local Socks5 proxy (if enabled)
+ discoveryException = e;
+ }
+
+ // determine address and port of each proxy
+ List<StreamHost> streamHosts = determineStreamHostInfos(proxies);
+
+ if (streamHosts.isEmpty()) {
+ throw discoveryException != null ? discoveryException : new XMPPException("no SOCKS5 proxies available");
+ }
+
+ // compute digest
+ String digest = Socks5Utils.createDigest(sessionID, this.connection.getUser(), targetJID);
+
+ // prioritize last working SOCKS5 proxy if exists
+ if (this.proxyPrioritizationEnabled && this.lastWorkingProxy != null) {
+ StreamHost selectedStreamHost = null;
+ for (StreamHost streamHost : streamHosts) {
+ if (streamHost.getJID().equals(this.lastWorkingProxy)) {
+ selectedStreamHost = streamHost;
+ break;
+ }
+ }
+ if (selectedStreamHost != null) {
+ streamHosts.remove(selectedStreamHost);
+ streamHosts.add(0, selectedStreamHost);
+ }
+
+ }
+
+ Socks5Proxy socks5Proxy = Socks5Proxy.getSocks5Proxy();
+ try {
+
+ // add transfer digest to local proxy to make transfer valid
+ socks5Proxy.addTransfer(digest);
+
+ // create initiation packet
+ Bytestream initiation = createBytestreamInitiation(sessionID, targetJID, streamHosts);
+
+ // send initiation packet
+ Packet response = SyncPacketSend.getReply(this.connection, initiation,
+ getTargetResponseTimeout());
+
+ // extract used stream host from response
+ StreamHostUsed streamHostUsed = ((Bytestream) response).getUsedHost();
+ StreamHost usedStreamHost = initiation.getStreamHost(streamHostUsed.getJID());
+
+ if (usedStreamHost == null) {
+ throw new XMPPException("Remote user responded with unknown host");
+ }
+
+ // build SOCKS5 client
+ Socks5Client socks5Client = new Socks5ClientForInitiator(usedStreamHost, digest,
+ this.connection, sessionID, targetJID);
+
+ // establish connection to proxy
+ Socket socket = socks5Client.getSocket(getProxyConnectionTimeout());
+
+ // remember last working SOCKS5 proxy to prioritize it for next request
+ this.lastWorkingProxy = usedStreamHost.getJID();
+
+ // negotiation successful, return the output stream
+ return new Socks5BytestreamSession(socket, usedStreamHost.getJID().equals(
+ this.connection.getUser()));
+
+ }
+ catch (TimeoutException e) {
+ throw new IOException("Timeout while connecting to SOCKS5 proxy");
+ }
+ finally {
+
+ // remove transfer digest if output stream is returned or an exception
+ // occurred
+ socks5Proxy.removeTransfer(digest);
+
+ }
+ }
+
+ /**
+ * Returns <code>true</code> if the given target JID supports feature SOCKS5 Bytestream.
+ *
+ * @param targetJID the target JID
+ * @return <code>true</code> if the given target JID supports feature SOCKS5 Bytestream
+ * otherwise <code>false</code>
+ * @throws XMPPException if there was an error querying target for supported features
+ */
+ private boolean supportsSocks5(String targetJID) throws XMPPException {
+ ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
+ DiscoverInfo discoverInfo = serviceDiscoveryManager.discoverInfo(targetJID);
+ return discoverInfo.containsFeature(NAMESPACE);
+ }
+
+ /**
+ * Returns a list of JIDs of SOCKS5 proxies by querying the XMPP server. The SOCKS5 proxies are
+ * in the same order as returned by the XMPP server.
+ *
+ * @return list of JIDs of SOCKS5 proxies
+ * @throws XMPPException if there was an error querying the XMPP server for SOCKS5 proxies
+ */
+ private List<String> determineProxies() throws XMPPException {
+ ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
+
+ List<String> proxies = new ArrayList<String>();
+
+ // get all items form XMPP server
+ DiscoverItems discoverItems = serviceDiscoveryManager.discoverItems(this.connection.getServiceName());
+ Iterator<Item> itemIterator = discoverItems.getItems();
+
+ // query all items if they are SOCKS5 proxies
+ while (itemIterator.hasNext()) {
+ Item item = itemIterator.next();
+
+ // skip blacklisted servers
+ if (this.proxyBlacklist.contains(item.getEntityID())) {
+ continue;
+ }
+
+ try {
+ DiscoverInfo proxyInfo;
+ proxyInfo = serviceDiscoveryManager.discoverInfo(item.getEntityID());
+ Iterator<Identity> identities = proxyInfo.getIdentities();
+
+ // item must have category "proxy" and type "bytestream"
+ while (identities.hasNext()) {
+ Identity identity = identities.next();
+
+ if ("proxy".equalsIgnoreCase(identity.getCategory())
+ && "bytestreams".equalsIgnoreCase(identity.getType())) {
+ proxies.add(item.getEntityID());
+ break;
+ }
+
+ /*
+ * server is not a SOCKS5 proxy, blacklist server to skip next time a Socks5
+ * bytestream should be established
+ */
+ this.proxyBlacklist.add(item.getEntityID());
+
+ }
+ }
+ catch (XMPPException e) {
+ // blacklist errornous server
+ this.proxyBlacklist.add(item.getEntityID());
+ }
+ }
+
+ return proxies;
+ }
+
+ /**
+ * Returns a list of stream hosts containing the IP address an the port for the given list of
+ * SOCKS5 proxy JIDs. The order of the returned list is the same as the given list of JIDs
+ * excluding all SOCKS5 proxies who's network settings could not be determined. If a local
+ * SOCKS5 proxy is running it will be the first item in the list returned.
+ *
+ * @param proxies a list of SOCKS5 proxy JIDs
+ * @return a list of stream hosts containing the IP address an the port
+ */
+ private List<StreamHost> determineStreamHostInfos(List<String> proxies) {
+ List<StreamHost> streamHosts = new ArrayList<StreamHost>();
+
+ // add local proxy on first position if exists
+ List<StreamHost> localProxies = getLocalStreamHost();
+ if (localProxies != null) {
+ streamHosts.addAll(localProxies);
+ }
+
+ // query SOCKS5 proxies for network settings
+ for (String proxy : proxies) {
+ Bytestream streamHostRequest = createStreamHostRequest(proxy);
+ try {
+ Bytestream response = (Bytestream) SyncPacketSend.getReply(this.connection,
+ streamHostRequest);
+ streamHosts.addAll(response.getStreamHosts());
+ }
+ catch (XMPPException e) {
+ // blacklist errornous proxies
+ this.proxyBlacklist.add(proxy);
+ }
+ }
+
+ return streamHosts;
+ }
+
+ /**
+ * Returns a IQ packet to query a SOCKS5 proxy its network settings.
+ *
+ * @param proxy the proxy to query
+ * @return IQ packet to query a SOCKS5 proxy its network settings
+ */
+ private Bytestream createStreamHostRequest(String proxy) {
+ Bytestream request = new Bytestream();
+ request.setType(IQ.Type.GET);
+ request.setTo(proxy);
+ return request;
+ }
+
+ /**
+ * Returns the stream host information of the local SOCKS5 proxy containing the IP address and
+ * the port or null if local SOCKS5 proxy is not running.
+ *
+ * @return the stream host information of the local SOCKS5 proxy or null if local SOCKS5 proxy
+ * is not running
+ */
+ private List<StreamHost> getLocalStreamHost() {
+
+ // get local proxy singleton
+ Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy();
+
+ if (socks5Server.isRunning()) {
+ List<String> addresses = socks5Server.getLocalAddresses();
+ int port = socks5Server.getPort();
+
+ if (addresses.size() >= 1) {
+ List<StreamHost> streamHosts = new ArrayList<StreamHost>();
+ for (String address : addresses) {
+ StreamHost streamHost = new StreamHost(this.connection.getUser(), address);
+ streamHost.setPort(port);
+ streamHosts.add(streamHost);
+ }
+ return streamHosts;
+ }
+
+ }
+
+ // server is not running or local address could not be determined
+ return null;
+ }
+
+ /**
+ * Returns a SOCKS5 Bytestream initialization request packet with the given session ID
+ * containing the given stream hosts for the given target JID.
+ *
+ * @param sessionID the session ID for the SOCKS5 Bytestream
+ * @param targetJID the target JID of SOCKS5 Bytestream request
+ * @param streamHosts a list of SOCKS5 proxies the target should connect to
+ * @return a SOCKS5 Bytestream initialization request packet
+ */
+ private Bytestream createBytestreamInitiation(String sessionID, String targetJID,
+ List<StreamHost> streamHosts) {
+ Bytestream initiation = new Bytestream(sessionID);
+
+ // add all stream hosts
+ for (StreamHost streamHost : streamHosts) {
+ initiation.addStreamHost(streamHost);
+ }
+
+ initiation.setType(IQ.Type.SET);
+ initiation.setTo(targetJID);
+
+ return initiation;
+ }
+
+ /**
+ * Responses to the given packet's sender with a XMPP error that a SOCKS5 Bytestream is not
+ * accepted.
+ *
+ * @param packet Packet that should be answered with a not-acceptable error
+ */
+ protected void replyRejectPacket(IQ packet) {
+ XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable);
+ IQ errorIQ = IQ.createErrorResponse(packet, xmppError);
+ this.connection.sendPacket(errorIQ);
+ }
+
+ /**
+ * Activates the Socks5BytestreamManager by registering the SOCKS5 Bytestream initialization
+ * listener and enabling the SOCKS5 Bytestream feature.
+ */
+ private void activate() {
+ // register bytestream initiation packet listener
+ this.connection.addPacketListener(this.initiationListener,
+ this.initiationListener.getFilter());
+
+ // enable SOCKS5 feature
+ enableService();
+ }
+
+ /**
+ * Adds the SOCKS5 Bytestream feature to the service discovery.
+ */
+ private void enableService() {
+ ServiceDiscoveryManager manager = ServiceDiscoveryManager.getInstanceFor(this.connection);
+ if (!manager.includesFeature(NAMESPACE)) {
+ manager.addFeature(NAMESPACE);
+ }
+ }
+
+ /**
+ * Returns a new unique session ID.
+ *
+ * @return a new unique session ID
+ */
+ private String getNextSessionID() {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append(SESSION_ID_PREFIX);
+ buffer.append(Math.abs(randomGenerator.nextLong()));
+ return buffer.toString();
+ }
+
+ /**
+ * Returns the XMPP connection.
+ *
+ * @return the XMPP connection
+ */
+ protected Connection getConnection() {
+ return this.connection;
+ }
+
+ /**
+ * Returns the {@link BytestreamListener} that should be informed if a SOCKS5 Bytestream request
+ * from the given initiator JID is received.
+ *
+ * @param initiator the initiator's JID
+ * @return the listener
+ */
+ protected BytestreamListener getUserListener(String initiator) {
+ return this.userListeners.get(initiator);
+ }
+
+ /**
+ * Returns a list of {@link BytestreamListener} that are informed if there are no listeners for
+ * a specific initiator.
+ *
+ * @return list of listeners
+ */
+ protected List<BytestreamListener> getAllRequestListeners() {
+ return this.allRequestListeners;
+ }
+
+ /**
+ * Returns the list of session IDs that should be ignored by the InitialtionListener
+ *
+ * @return list of session IDs
+ */
+ protected List<String> getIgnoredBytestreamRequests() {
+ return ignoredBytestreamRequests;
+ }
+
+}