aboutsummaryrefslogtreecommitdiff
path: root/src/org/jivesoftware/smackx/bytestreams/socks5
diff options
context:
space:
mode:
Diffstat (limited to 'src/org/jivesoftware/smackx/bytestreams/socks5')
-rw-r--r--src/org/jivesoftware/smackx/bytestreams/socks5/InitiationListener.java119
-rw-r--r--src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamListener.java43
-rw-r--r--src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java777
-rw-r--r--src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamRequest.java316
-rw-r--r--src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamSession.java98
-rw-r--r--src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Client.java204
-rw-r--r--src/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiator.java117
-rw-r--r--src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java423
-rw-r--r--src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Utils.java73
-rw-r--r--src/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java474
-rw-r--r--src/org/jivesoftware/smackx/bytestreams/socks5/provider/BytestreamsProvider.java82
11 files changed, 2726 insertions, 0 deletions
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/InitiationListener.java b/src/org/jivesoftware/smackx/bytestreams/socks5/InitiationListener.java
new file mode 100644
index 0000000..2a78250
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/InitiationListener.java
@@ -0,0 +1,119 @@
+/**
+ * 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.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.IQTypeFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.bytestreams.BytestreamListener;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
+
+/**
+ * InitiationListener handles all incoming SOCKS5 Bytestream initiation requests. If there are no
+ * listeners for a SOCKS5 bytestream request InitiationListener will always refuse the request and
+ * reply with a &lt;not-acceptable/&gt; error (<a
+ * href="http://xmpp.org/extensions/xep-0065.html#usecase-alternate">XEP-0065</a> Section 5.2.A2).
+ *
+ * @author Henning Staib
+ */
+final class InitiationListener implements PacketListener {
+
+ /* manager containing the listeners and the XMPP connection */
+ private final Socks5BytestreamManager manager;
+
+ /* packet filter for all SOCKS5 Bytestream requests */
+ private final PacketFilter initFilter = new AndFilter(new PacketTypeFilter(Bytestream.class),
+ new IQTypeFilter(IQ.Type.SET));
+
+ /* executor service to process incoming requests concurrently */
+ private final ExecutorService initiationListenerExecutor;
+
+ /**
+ * Constructor
+ *
+ * @param manager the SOCKS5 Bytestream manager
+ */
+ protected InitiationListener(Socks5BytestreamManager manager) {
+ this.manager = manager;
+ initiationListenerExecutor = Executors.newCachedThreadPool();
+ }
+
+ public void processPacket(final Packet packet) {
+ initiationListenerExecutor.execute(new Runnable() {
+
+ public void run() {
+ processRequest(packet);
+ }
+ });
+ }
+
+ private void processRequest(Packet packet) {
+ Bytestream byteStreamRequest = (Bytestream) packet;
+
+ // ignore request if in ignore list
+ if (this.manager.getIgnoredBytestreamRequests().remove(byteStreamRequest.getSessionID())) {
+ return;
+ }
+
+ // build bytestream request from packet
+ Socks5BytestreamRequest request = new Socks5BytestreamRequest(this.manager,
+ byteStreamRequest);
+
+ // notify listeners for bytestream initiation from a specific user
+ BytestreamListener userListener = this.manager.getUserListener(byteStreamRequest.getFrom());
+ if (userListener != null) {
+ userListener.incomingBytestreamRequest(request);
+
+ }
+ else if (!this.manager.getAllRequestListeners().isEmpty()) {
+ /*
+ * if there is no user specific listener inform listeners for all initiation requests
+ */
+ for (BytestreamListener listener : this.manager.getAllRequestListeners()) {
+ listener.incomingBytestreamRequest(request);
+ }
+
+ }
+ else {
+ /*
+ * if there is no listener for this initiation request, reply with reject message
+ */
+ this.manager.replyRejectPacket(byteStreamRequest);
+ }
+ }
+
+ /**
+ * Returns the packet filter for SOCKS5 Bytestream initialization requests.
+ *
+ * @return the packet filter for SOCKS5 Bytestream initialization requests
+ */
+ protected PacketFilter getFilter() {
+ return this.initFilter;
+ }
+
+ /**
+ * Shuts down the listeners executor service.
+ */
+ protected void shutdown() {
+ this.initiationListenerExecutor.shutdownNow();
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamListener.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamListener.java
new file mode 100644
index 0000000..1430b1d
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamListener.java
@@ -0,0 +1,43 @@
+/**
+ * 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 org.jivesoftware.smackx.bytestreams.BytestreamListener;
+import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
+
+/**
+ * Socks5BytestreamListener are informed if a remote user wants to initiate a SOCKS5 Bytestream.
+ * Implement this interface to handle incoming SOCKS5 Bytestream requests.
+ * <p>
+ * There are two ways to add this listener. See
+ * {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and
+ * {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)} for
+ * further details.
+ *
+ * @author Henning Staib
+ */
+public abstract class Socks5BytestreamListener implements BytestreamListener {
+
+ public void incomingBytestreamRequest(BytestreamRequest request) {
+ incomingBytestreamRequest((Socks5BytestreamRequest) request);
+ }
+
+ /**
+ * This listener is notified if a SOCKS5 Bytestream request from another user has been received.
+ *
+ * @param request the incoming SOCKS5 Bytestream request
+ */
+ public abstract void incomingBytestreamRequest(Socks5BytestreamRequest request);
+
+}
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;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamRequest.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamRequest.java
new file mode 100644
index 0000000..0b2fdeb
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamRequest.java
@@ -0,0 +1,316 @@
+/**
+ * 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.net.Socket;
+import java.util.Collection;
+import java.util.concurrent.TimeoutException;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.Cache;
+import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
+
+/**
+ * Socks5BytestreamRequest class handles incoming SOCKS5 Bytestream requests.
+ *
+ * @author Henning Staib
+ */
+public class Socks5BytestreamRequest implements BytestreamRequest {
+
+ /* lifetime of an Item in the blacklist */
+ private static final long BLACKLIST_LIFETIME = 60 * 1000 * 120;
+
+ /* size of the blacklist */
+ private static final int BLACKLIST_MAX_SIZE = 100;
+
+ /* blacklist of addresses of SOCKS5 proxies */
+ private static final Cache<String, Integer> ADDRESS_BLACKLIST = new Cache<String, Integer>(
+ BLACKLIST_MAX_SIZE, BLACKLIST_LIFETIME);
+
+ /*
+ * The number of connection failures it takes for a particular SOCKS5 proxy to be blacklisted.
+ * When a proxy is blacklisted no more connection attempts will be made to it for a period of 2
+ * hours.
+ */
+ private static int CONNECTION_FAILURE_THRESHOLD = 2;
+
+ /* the bytestream initialization request */
+ private Bytestream bytestreamRequest;
+
+ /* SOCKS5 Bytestream manager containing the XMPP connection and helper methods */
+ private Socks5BytestreamManager manager;
+
+ /* timeout to connect to all SOCKS5 proxies */
+ private int totalConnectTimeout = 10000;
+
+ /* minimum timeout to connect to one SOCKS5 proxy */
+ private int minimumConnectTimeout = 2000;
+
+ /**
+ * Returns the number of connection failures it takes for a particular SOCKS5 proxy to be
+ * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
+ * period of 2 hours. Default is 2.
+ *
+ * @return the number of connection failures it takes for a particular SOCKS5 proxy to be
+ * blacklisted
+ */
+ public static int getConnectFailureThreshold() {
+ return CONNECTION_FAILURE_THRESHOLD;
+ }
+
+ /**
+ * Sets the number of connection failures it takes for a particular SOCKS5 proxy to be
+ * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
+ * period of 2 hours. Default is 2.
+ * <p>
+ * Setting the connection failure threshold to zero disables the blacklisting.
+ *
+ * @param connectFailureThreshold the number of connection failures it takes for a particular
+ * SOCKS5 proxy to be blacklisted
+ */
+ public static void setConnectFailureThreshold(int connectFailureThreshold) {
+ CONNECTION_FAILURE_THRESHOLD = connectFailureThreshold;
+ }
+
+ /**
+ * Creates a new Socks5BytestreamRequest.
+ *
+ * @param manager the SOCKS5 Bytestream manager
+ * @param bytestreamRequest the SOCKS5 Bytestream initialization packet
+ */
+ protected Socks5BytestreamRequest(Socks5BytestreamManager manager, Bytestream bytestreamRequest) {
+ this.manager = manager;
+ this.bytestreamRequest = bytestreamRequest;
+ }
+
+ /**
+ * Returns the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
+ * <p>
+ * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
+ * by the initiator until a connection is established. This timeout divided by the number of
+ * SOCKS5 proxies determines the timeout for every connection attempt.
+ * <p>
+ * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
+ * {@link #setMinimumConnectTimeout(int)}.
+ *
+ * @return the maximum timeout to connect to SOCKS5 proxies
+ */
+ public int getTotalConnectTimeout() {
+ if (this.totalConnectTimeout <= 0) {
+ return 10000;
+ }
+ return this.totalConnectTimeout;
+ }
+
+ /**
+ * Sets the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
+ * <p>
+ * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
+ * by the initiator until a connection is established. This timeout divided by the number of
+ * SOCKS5 proxies determines the timeout for every connection attempt.
+ * <p>
+ * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
+ * {@link #setMinimumConnectTimeout(int)}.
+ *
+ * @param totalConnectTimeout the maximum timeout to connect to SOCKS5 proxies
+ */
+ public void setTotalConnectTimeout(int totalConnectTimeout) {
+ this.totalConnectTimeout = totalConnectTimeout;
+ }
+
+ /**
+ * Returns the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
+ * request. Default is 2000ms.
+ *
+ * @return the timeout to connect to one SOCKS5 proxy
+ */
+ public int getMinimumConnectTimeout() {
+ if (this.minimumConnectTimeout <= 0) {
+ return 2000;
+ }
+ return this.minimumConnectTimeout;
+ }
+
+ /**
+ * Sets the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
+ * request. Default is 2000ms.
+ *
+ * @param minimumConnectTimeout the timeout to connect to one SOCKS5 proxy
+ */
+ public void setMinimumConnectTimeout(int minimumConnectTimeout) {
+ this.minimumConnectTimeout = minimumConnectTimeout;
+ }
+
+ /**
+ * Returns the sender of the SOCKS5 Bytestream initialization request.
+ *
+ * @return the sender of the SOCKS5 Bytestream initialization request.
+ */
+ public String getFrom() {
+ return this.bytestreamRequest.getFrom();
+ }
+
+ /**
+ * Returns the session ID of the SOCKS5 Bytestream initialization request.
+ *
+ * @return the session ID of the SOCKS5 Bytestream initialization request.
+ */
+ public String getSessionID() {
+ return this.bytestreamRequest.getSessionID();
+ }
+
+ /**
+ * Accepts the SOCKS5 Bytestream initialization request and returns the socket to send/receive
+ * data.
+ * <p>
+ * Before accepting the SOCKS5 Bytestream request you can set timeouts by invoking
+ * {@link #setTotalConnectTimeout(int)} and {@link #setMinimumConnectTimeout(int)}.
+ *
+ * @return the socket to send/receive data
+ * @throws XMPPException if connection to all SOCKS5 proxies failed or if stream is invalid.
+ * @throws InterruptedException if the current thread was interrupted while waiting
+ */
+ public Socks5BytestreamSession accept() throws XMPPException, InterruptedException {
+ Collection<StreamHost> streamHosts = this.bytestreamRequest.getStreamHosts();
+
+ // throw exceptions if request contains no stream hosts
+ if (streamHosts.size() == 0) {
+ cancelRequest();
+ }
+
+ StreamHost selectedHost = null;
+ Socket socket = null;
+
+ String digest = Socks5Utils.createDigest(this.bytestreamRequest.getSessionID(),
+ this.bytestreamRequest.getFrom(), this.manager.getConnection().getUser());
+
+ /*
+ * determine timeout for each connection attempt; each SOCKS5 proxy has the same amount of
+ * time so that the first does not consume the whole timeout
+ */
+ int timeout = Math.max(getTotalConnectTimeout() / streamHosts.size(),
+ getMinimumConnectTimeout());
+
+ for (StreamHost streamHost : streamHosts) {
+ String address = streamHost.getAddress() + ":" + streamHost.getPort();
+
+ // check to see if this address has been blacklisted
+ int failures = getConnectionFailures(address);
+ if (CONNECTION_FAILURE_THRESHOLD > 0 && failures >= CONNECTION_FAILURE_THRESHOLD) {
+ continue;
+ }
+
+ // establish socket
+ try {
+
+ // build SOCKS5 client
+ final Socks5Client socks5Client = new Socks5Client(streamHost, digest);
+
+ // connect to SOCKS5 proxy with a timeout
+ socket = socks5Client.getSocket(timeout);
+
+ // set selected host
+ selectedHost = streamHost;
+ break;
+
+ }
+ catch (TimeoutException e) {
+ incrementConnectionFailures(address);
+ }
+ catch (IOException e) {
+ incrementConnectionFailures(address);
+ }
+ catch (XMPPException e) {
+ incrementConnectionFailures(address);
+ }
+
+ }
+
+ // throw exception if connecting to all SOCKS5 proxies failed
+ if (selectedHost == null || socket == null) {
+ cancelRequest();
+ }
+
+ // send used-host confirmation
+ Bytestream response = createUsedHostResponse(selectedHost);
+ this.manager.getConnection().sendPacket(response);
+
+ return new Socks5BytestreamSession(socket, selectedHost.getJID().equals(
+ this.bytestreamRequest.getFrom()));
+
+ }
+
+ /**
+ * Rejects the SOCKS5 Bytestream request by sending a reject error to the initiator.
+ */
+ public void reject() {
+ this.manager.replyRejectPacket(this.bytestreamRequest);
+ }
+
+ /**
+ * Cancels the SOCKS5 Bytestream request by sending an error to the initiator and building a
+ * XMPP exception.
+ *
+ * @throws XMPPException XMPP exception containing the XMPP error
+ */
+ private void cancelRequest() throws XMPPException {
+ String errorMessage = "Could not establish socket with any provided host";
+ XMPPError error = new XMPPError(XMPPError.Condition.item_not_found, errorMessage);
+ IQ errorIQ = IQ.createErrorResponse(this.bytestreamRequest, error);
+ this.manager.getConnection().sendPacket(errorIQ);
+ throw new XMPPException(errorMessage, error);
+ }
+
+ /**
+ * Returns the response to the SOCKS5 Bytestream request containing the SOCKS5 proxy used.
+ *
+ * @param selectedHost the used SOCKS5 proxy
+ * @return the response to the SOCKS5 Bytestream request
+ */
+ private Bytestream createUsedHostResponse(StreamHost selectedHost) {
+ Bytestream response = new Bytestream(this.bytestreamRequest.getSessionID());
+ response.setTo(this.bytestreamRequest.getFrom());
+ response.setType(IQ.Type.RESULT);
+ response.setPacketID(this.bytestreamRequest.getPacketID());
+ response.setUsedHost(selectedHost.getJID());
+ return response;
+ }
+
+ /**
+ * Increments the connection failure counter by one for the given address.
+ *
+ * @param address the address the connection failure counter should be increased
+ */
+ private void incrementConnectionFailures(String address) {
+ Integer count = ADDRESS_BLACKLIST.get(address);
+ ADDRESS_BLACKLIST.put(address, count == null ? 1 : count + 1);
+ }
+
+ /**
+ * Returns how often the connection to the given address failed.
+ *
+ * @param address the address
+ * @return number of connection failures
+ */
+ private int getConnectionFailures(String address) {
+ Integer count = ADDRESS_BLACKLIST.get(address);
+ return count != null ? count : 0;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamSession.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamSession.java
new file mode 100644
index 0000000..41ab142
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamSession.java
@@ -0,0 +1,98 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.SocketException;
+
+import org.jivesoftware.smackx.bytestreams.BytestreamSession;
+
+/**
+ * Socks5BytestreamSession class represents a SOCKS5 Bytestream session.
+ *
+ * @author Henning Staib
+ */
+public class Socks5BytestreamSession implements BytestreamSession {
+
+ /* the underlying socket of the SOCKS5 Bytestream */
+ private final Socket socket;
+
+ /* flag to indicate if this session is a direct or mediated connection */
+ private final boolean isDirect;
+
+ protected Socks5BytestreamSession(Socket socket, boolean isDirect) {
+ this.socket = socket;
+ this.isDirect = isDirect;
+ }
+
+ /**
+ * Returns <code>true</code> if the session is established through a direct connection between
+ * the initiator and target, <code>false</code> if the session is mediated over a SOCKS proxy.
+ *
+ * @return <code>true</code> if session is a direct connection, <code>false</code> if session is
+ * mediated over a SOCKS5 proxy
+ */
+ public boolean isDirect() {
+ return this.isDirect;
+ }
+
+ /**
+ * Returns <code>true</code> if the session is mediated over a SOCKS proxy, <code>false</code>
+ * if this session is established through a direct connection between the initiator and target.
+ *
+ * @return <code>true</code> if session is mediated over a SOCKS5 proxy, <code>false</code> if
+ * session is a direct connection
+ */
+ public boolean isMediated() {
+ return !this.isDirect;
+ }
+
+ public InputStream getInputStream() throws IOException {
+ return this.socket.getInputStream();
+ }
+
+ public OutputStream getOutputStream() throws IOException {
+ return this.socket.getOutputStream();
+ }
+
+ public int getReadTimeout() throws IOException {
+ try {
+ return this.socket.getSoTimeout();
+ }
+ catch (SocketException e) {
+ throw new IOException("Error on underlying Socket");
+ }
+ }
+
+ public void setReadTimeout(int timeout) throws IOException {
+ try {
+ this.socket.setSoTimeout(timeout);
+ }
+ catch (SocketException e) {
+ throw new IOException("Error on underlying Socket");
+ }
+ }
+
+ public void close() throws IOException {
+ this.socket.close();
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Client.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Client.java
new file mode 100644
index 0000000..664ea59
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Client.java
@@ -0,0 +1,204 @@
+/**
+ * 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.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
+
+/**
+ * The SOCKS5 client class handles establishing a connection to a SOCKS5 proxy. Connecting to a
+ * SOCKS5 proxy requires authentication. This implementation only supports the no-authentication
+ * authentication method.
+ *
+ * @author Henning Staib
+ */
+class Socks5Client {
+
+ /* stream host containing network settings and name of the SOCKS5 proxy */
+ protected StreamHost streamHost;
+
+ /* SHA-1 digest identifying the SOCKS5 stream */
+ protected String digest;
+
+ /**
+ * Constructor for a SOCKS5 client.
+ *
+ * @param streamHost containing network settings of the SOCKS5 proxy
+ * @param digest identifying the SOCKS5 Bytestream
+ */
+ public Socks5Client(StreamHost streamHost, String digest) {
+ this.streamHost = streamHost;
+ this.digest = digest;
+ }
+
+ /**
+ * Returns the initialized socket that can be used to transfer data between peers via the SOCKS5
+ * proxy.
+ *
+ * @param timeout timeout to connect to SOCKS5 proxy in milliseconds
+ * @return socket the initialized socket
+ * @throws IOException if initializing the socket failed due to a network error
+ * @throws XMPPException if establishing connection to SOCKS5 proxy failed
+ * @throws TimeoutException if connecting to SOCKS5 proxy timed out
+ * @throws InterruptedException if the current thread was interrupted while waiting
+ */
+ public Socket getSocket(int timeout) throws IOException, XMPPException, InterruptedException,
+ TimeoutException {
+
+ // wrap connecting in future for timeout
+ FutureTask<Socket> futureTask = new FutureTask<Socket>(new Callable<Socket>() {
+
+ public Socket call() throws Exception {
+
+ // initialize socket
+ Socket socket = new Socket();
+ SocketAddress socketAddress = new InetSocketAddress(streamHost.getAddress(),
+ streamHost.getPort());
+ socket.connect(socketAddress);
+
+ // initialize connection to SOCKS5 proxy
+ if (!establish(socket)) {
+
+ // initialization failed, close socket
+ socket.close();
+ throw new XMPPException("establishing connection to SOCKS5 proxy failed");
+
+ }
+
+ return socket;
+ }
+
+ });
+ Thread executor = new Thread(futureTask);
+ executor.start();
+
+ // get connection to initiator with timeout
+ try {
+ return futureTask.get(timeout, TimeUnit.MILLISECONDS);
+ }
+ catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ if (cause != null) {
+ // case exceptions to comply with method signature
+ if (cause instanceof IOException) {
+ throw (IOException) cause;
+ }
+ if (cause instanceof XMPPException) {
+ throw (XMPPException) cause;
+ }
+ }
+
+ // throw generic IO exception if unexpected exception was thrown
+ throw new IOException("Error while connection to SOCKS5 proxy");
+ }
+
+ }
+
+ /**
+ * Initializes the connection to the SOCKS5 proxy by negotiating authentication method and
+ * requesting a stream for the given digest. Currently only the no-authentication method is
+ * supported by the Socks5Client.
+ * <p>
+ * Returns <code>true</code> if a stream could be established, otherwise <code>false</code>. If
+ * <code>false</code> is returned the given Socket should be closed.
+ *
+ * @param socket connected to a SOCKS5 proxy
+ * @return <code>true</code> if if a stream could be established, otherwise <code>false</code>.
+ * If <code>false</code> is returned the given Socket should be closed.
+ * @throws IOException if a network error occurred
+ */
+ protected boolean establish(Socket socket) throws IOException {
+
+ /*
+ * use DataInputStream/DataOutpuStream to assure read and write is completed in a single
+ * statement
+ */
+ DataInputStream in = new DataInputStream(socket.getInputStream());
+ DataOutputStream out = new DataOutputStream(socket.getOutputStream());
+
+ // authentication negotiation
+ byte[] cmd = new byte[3];
+
+ cmd[0] = (byte) 0x05; // protocol version 5
+ cmd[1] = (byte) 0x01; // number of authentication methods supported
+ cmd[2] = (byte) 0x00; // authentication method: no-authentication required
+
+ out.write(cmd);
+ out.flush();
+
+ byte[] response = new byte[2];
+ in.readFully(response);
+
+ // check if server responded with correct version and no-authentication method
+ if (response[0] != (byte) 0x05 || response[1] != (byte) 0x00) {
+ return false;
+ }
+
+ // request SOCKS5 connection with given address/digest
+ byte[] connectionRequest = createSocks5ConnectRequest();
+ out.write(connectionRequest);
+ out.flush();
+
+ // receive response
+ byte[] connectionResponse;
+ try {
+ connectionResponse = Socks5Utils.receiveSocks5Message(in);
+ }
+ catch (XMPPException e) {
+ return false; // server answered in an unsupported way
+ }
+
+ // verify response
+ connectionRequest[1] = (byte) 0x00; // set expected return status to 0
+ return Arrays.equals(connectionRequest, connectionResponse);
+ }
+
+ /**
+ * Returns a SOCKS5 connection request message. It contains the command "connect", the address
+ * type "domain" and the digest as address.
+ * <p>
+ * (see <a href="http://tools.ietf.org/html/rfc1928">RFC1928</a>)
+ *
+ * @return SOCKS5 connection request message
+ */
+ private byte[] createSocks5ConnectRequest() {
+ byte addr[] = this.digest.getBytes();
+
+ byte[] data = new byte[7 + addr.length];
+ data[0] = (byte) 0x05; // version (SOCKS5)
+ data[1] = (byte) 0x01; // command (1 - connect)
+ data[2] = (byte) 0x00; // reserved byte (always 0)
+ data[3] = (byte) 0x03; // address type (3 - domain name)
+ data[4] = (byte) addr.length; // address length
+ System.arraycopy(addr, 0, data, 5, addr.length); // address
+ data[data.length - 2] = (byte) 0; // address port (2 bytes always 0)
+ data[data.length - 1] = (byte) 0;
+
+ return data;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiator.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiator.java
new file mode 100644
index 0000000..0d90791
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiator.java
@@ -0,0 +1,117 @@
+/**
+ * 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.net.Socket;
+import java.util.concurrent.TimeoutException;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.util.SyncPacketSend;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
+
+/**
+ * Implementation of a SOCKS5 client used on the initiators side. This is needed because connecting
+ * to the local SOCKS5 proxy differs form the regular way to connect to a SOCKS5 proxy. Additionally
+ * a remote SOCKS5 proxy has to be activated by the initiator before data can be transferred between
+ * the peers.
+ *
+ * @author Henning Staib
+ */
+class Socks5ClientForInitiator extends Socks5Client {
+
+ /* the XMPP connection used to communicate with the SOCKS5 proxy */
+ private Connection connection;
+
+ /* the session ID used to activate SOCKS5 stream */
+ private String sessionID;
+
+ /* the target JID used to activate SOCKS5 stream */
+ private String target;
+
+ /**
+ * Creates a new SOCKS5 client for the initiators side.
+ *
+ * @param streamHost containing network settings of the SOCKS5 proxy
+ * @param digest identifying the SOCKS5 Bytestream
+ * @param connection the XMPP connection
+ * @param sessionID the session ID of the SOCKS5 Bytestream
+ * @param target the target JID of the SOCKS5 Bytestream
+ */
+ public Socks5ClientForInitiator(StreamHost streamHost, String digest, Connection connection,
+ String sessionID, String target) {
+ super(streamHost, digest);
+ this.connection = connection;
+ this.sessionID = sessionID;
+ this.target = target;
+ }
+
+ public Socket getSocket(int timeout) throws IOException, XMPPException, InterruptedException,
+ TimeoutException {
+ Socket socket = null;
+
+ // check if stream host is the local SOCKS5 proxy
+ if (this.streamHost.getJID().equals(this.connection.getUser())) {
+ Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy();
+ socket = socks5Server.getSocket(this.digest);
+ if (socket == null) {
+ throw new XMPPException("target is not connected to SOCKS5 proxy");
+ }
+ }
+ else {
+ socket = super.getSocket(timeout);
+
+ try {
+ activate();
+ }
+ catch (XMPPException e) {
+ socket.close();
+ throw new XMPPException("activating SOCKS5 Bytestream failed", e);
+ }
+
+ }
+
+ return socket;
+ }
+
+ /**
+ * Activates the SOCKS5 Bytestream by sending a XMPP SOCKS5 Bytestream activation packet to the
+ * SOCKS5 proxy.
+ */
+ private void activate() throws XMPPException {
+ Bytestream activate = createStreamHostActivation();
+ // if activation fails #getReply throws an exception
+ SyncPacketSend.getReply(this.connection, activate);
+ }
+
+ /**
+ * Returns a SOCKS5 Bytestream activation packet.
+ *
+ * @return SOCKS5 Bytestream activation packet
+ */
+ private Bytestream createStreamHostActivation() {
+ Bytestream activate = new Bytestream(this.sessionID);
+ activate.setMode(null);
+ activate.setType(IQ.Type.SET);
+ activate.setTo(this.streamHost.getJID());
+
+ activate.setToActivate(this.target);
+
+ return activate;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java
new file mode 100644
index 0000000..11ef7a9
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java
@@ -0,0 +1,423 @@
+/**
+ * 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.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPException;
+
+/**
+ * The Socks5Proxy class represents a local SOCKS5 proxy server. It can be enabled/disabled by
+ * setting the <code>localSocks5ProxyEnabled</code> flag in the <code>smack-config.xml</code> or by
+ * invoking {@link SmackConfiguration#setLocalSocks5ProxyEnabled(boolean)}. The proxy is enabled by
+ * default.
+ * <p>
+ * The port of the local SOCKS5 proxy can be configured by setting <code>localSocks5ProxyPort</code>
+ * in the <code>smack-config.xml</code> or by invoking
+ * {@link SmackConfiguration#setLocalSocks5ProxyPort(int)}. Default port is 7777. If you set the
+ * port to a negative value Smack tries to the absolute value and all following until it finds an
+ * open port.
+ * <p>
+ * If your application is running on a machine with multiple network interfaces or if you want to
+ * provide your public address in case you are behind a NAT router, invoke
+ * {@link #addLocalAddress(String)} or {@link #replaceLocalAddresses(List)} to modify the list of
+ * local network addresses used for outgoing SOCKS5 Bytestream requests.
+ * <p>
+ * The local SOCKS5 proxy server refuses all connections except the ones that are explicitly allowed
+ * in the process of establishing a SOCKS5 Bytestream (
+ * {@link Socks5BytestreamManager#establishSession(String)}).
+ * <p>
+ * This Implementation has the following limitations:
+ * <ul>
+ * <li>only supports the no-authentication authentication method</li>
+ * <li>only supports the <code>connect</code> command and will not answer correctly to other
+ * commands</li>
+ * <li>only supports requests with the domain address type and will not correctly answer to requests
+ * with other address types</li>
+ * </ul>
+ * (see <a href="http://tools.ietf.org/html/rfc1928">RFC 1928</a>)
+ *
+ * @author Henning Staib
+ */
+public class Socks5Proxy {
+
+ /* SOCKS5 proxy singleton */
+ private static Socks5Proxy socks5Server;
+
+ /* reusable implementation of a SOCKS5 proxy server process */
+ private Socks5ServerProcess serverProcess;
+
+ /* thread running the SOCKS5 server process */
+ private Thread serverThread;
+
+ /* server socket to accept SOCKS5 connections */
+ private ServerSocket serverSocket;
+
+ /* assigns a connection to a digest */
+ private final Map<String, Socket> connectionMap = new ConcurrentHashMap<String, Socket>();
+
+ /* list of digests connections should be stored */
+ private final List<String> allowedConnections = Collections.synchronizedList(new LinkedList<String>());
+
+ private final Set<String> localAddresses = Collections.synchronizedSet(new LinkedHashSet<String>());
+
+ /**
+ * Private constructor.
+ */
+ private Socks5Proxy() {
+ this.serverProcess = new Socks5ServerProcess();
+
+ // add default local address
+ try {
+ this.localAddresses.add(InetAddress.getLocalHost().getHostAddress());
+ }
+ catch (UnknownHostException e) {
+ // do nothing
+ }
+
+ }
+
+ /**
+ * Returns the local SOCKS5 proxy server.
+ *
+ * @return the local SOCKS5 proxy server
+ */
+ public static synchronized Socks5Proxy getSocks5Proxy() {
+ if (socks5Server == null) {
+ socks5Server = new Socks5Proxy();
+ }
+ if (SmackConfiguration.isLocalSocks5ProxyEnabled()) {
+ socks5Server.start();
+ }
+ return socks5Server;
+ }
+
+ /**
+ * Starts the local SOCKS5 proxy server. If it is already running, this method does nothing.
+ */
+ public synchronized void start() {
+ if (isRunning()) {
+ return;
+ }
+ try {
+ if (SmackConfiguration.getLocalSocks5ProxyPort() < 0) {
+ int port = Math.abs(SmackConfiguration.getLocalSocks5ProxyPort());
+ for (int i = 0; i < 65535 - port; i++) {
+ try {
+ this.serverSocket = new ServerSocket(port + i);
+ break;
+ }
+ catch (IOException e) {
+ // port is used, try next one
+ }
+ }
+ }
+ else {
+ this.serverSocket = new ServerSocket(SmackConfiguration.getLocalSocks5ProxyPort());
+ }
+
+ if (this.serverSocket != null) {
+ this.serverThread = new Thread(this.serverProcess);
+ this.serverThread.start();
+ }
+ }
+ catch (IOException e) {
+ // couldn't setup server
+ System.err.println("couldn't setup local SOCKS5 proxy on port "
+ + SmackConfiguration.getLocalSocks5ProxyPort() + ": " + e.getMessage());
+ }
+ }
+
+ /**
+ * Stops the local SOCKS5 proxy server. If it is not running this method does nothing.
+ */
+ public synchronized void stop() {
+ if (!isRunning()) {
+ return;
+ }
+
+ try {
+ this.serverSocket.close();
+ }
+ catch (IOException e) {
+ // do nothing
+ }
+
+ if (this.serverThread != null && this.serverThread.isAlive()) {
+ try {
+ this.serverThread.interrupt();
+ this.serverThread.join();
+ }
+ catch (InterruptedException e) {
+ // do nothing
+ }
+ }
+ this.serverThread = null;
+ this.serverSocket = null;
+
+ }
+
+ /**
+ * Adds the given address to the list of local network addresses.
+ * <p>
+ * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request.
+ * This may be necessary if your application is running on a machine with multiple network
+ * interfaces or if you want to provide your public address in case you are behind a NAT router.
+ * <p>
+ * The order of the addresses used is determined by the order you add addresses.
+ * <p>
+ * Note that the list of addresses initially contains the address returned by
+ * <code>InetAddress.getLocalHost().getHostAddress()</code>. You can replace the list of
+ * addresses by invoking {@link #replaceLocalAddresses(List)}.
+ *
+ * @param address the local network address to add
+ */
+ public void addLocalAddress(String address) {
+ if (address == null) {
+ throw new IllegalArgumentException("address may not be null");
+ }
+ this.localAddresses.add(address);
+ }
+
+ /**
+ * Removes the given address from the list of local network addresses. This address will then no
+ * longer be used of outgoing SOCKS5 Bytestream requests.
+ *
+ * @param address the local network address to remove
+ */
+ public void removeLocalAddress(String address) {
+ this.localAddresses.remove(address);
+ }
+
+ /**
+ * Returns an unmodifiable list of the local network addresses that will be used for streamhost
+ * candidates of outgoing SOCKS5 Bytestream requests.
+ *
+ * @return unmodifiable list of the local network addresses
+ */
+ public List<String> getLocalAddresses() {
+ return Collections.unmodifiableList(new ArrayList<String>(this.localAddresses));
+ }
+
+ /**
+ * Replaces the list of local network addresses.
+ * <p>
+ * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request and
+ * want to define their order. This may be necessary if your application is running on a machine
+ * with multiple network interfaces or if you want to provide your public address in case you
+ * are behind a NAT router.
+ *
+ * @param addresses the new list of local network addresses
+ */
+ public void replaceLocalAddresses(List<String> addresses) {
+ if (addresses == null) {
+ throw new IllegalArgumentException("list must not be null");
+ }
+ this.localAddresses.clear();
+ this.localAddresses.addAll(addresses);
+
+ }
+
+ /**
+ * Returns the port of the local SOCKS5 proxy server. If it is not running -1 will be returned.
+ *
+ * @return the port of the local SOCKS5 proxy server or -1 if proxy is not running
+ */
+ public int getPort() {
+ if (!isRunning()) {
+ return -1;
+ }
+ return this.serverSocket.getLocalPort();
+ }
+
+ /**
+ * Returns the socket for the given digest. A socket will be returned if the given digest has
+ * been in the list of allowed transfers (see {@link #addTransfer(String)}) while the peer
+ * connected to the SOCKS5 proxy.
+ *
+ * @param digest identifying the connection
+ * @return socket or null if there is no socket for the given digest
+ */
+ protected Socket getSocket(String digest) {
+ return this.connectionMap.get(digest);
+ }
+
+ /**
+ * Add the given digest to the list of allowed transfers. Only connections for allowed transfers
+ * are stored and can be retrieved by invoking {@link #getSocket(String)}. All connections to
+ * the local SOCKS5 proxy that don't contain an allowed digest are discarded.
+ *
+ * @param digest to be added to the list of allowed transfers
+ */
+ protected void addTransfer(String digest) {
+ this.allowedConnections.add(digest);
+ }
+
+ /**
+ * Removes the given digest from the list of allowed transfers. After invoking this method
+ * already stored connections with the given digest will be removed.
+ * <p>
+ * The digest should be removed after establishing the SOCKS5 Bytestream is finished, an error
+ * occurred while establishing the connection or if the connection is not allowed anymore.
+ *
+ * @param digest to be removed from the list of allowed transfers
+ */
+ protected void removeTransfer(String digest) {
+ this.allowedConnections.remove(digest);
+ this.connectionMap.remove(digest);
+ }
+
+ /**
+ * Returns <code>true</code> if the local SOCKS5 proxy server is running, otherwise
+ * <code>false</code>.
+ *
+ * @return <code>true</code> if the local SOCKS5 proxy server is running, otherwise
+ * <code>false</code>
+ */
+ public boolean isRunning() {
+ return this.serverSocket != null;
+ }
+
+ /**
+ * Implementation of a simplified SOCKS5 proxy server.
+ */
+ private class Socks5ServerProcess implements Runnable {
+
+ public void run() {
+ while (true) {
+ Socket socket = null;
+
+ try {
+
+ if (Socks5Proxy.this.serverSocket.isClosed()
+ || Thread.currentThread().isInterrupted()) {
+ return;
+ }
+
+ // accept connection
+ socket = Socks5Proxy.this.serverSocket.accept();
+
+ // initialize connection
+ establishConnection(socket);
+
+ }
+ catch (SocketException e) {
+ /*
+ * do nothing, if caused by closing the server socket, thread will terminate in
+ * next loop
+ */
+ }
+ catch (Exception e) {
+ try {
+ if (socket != null) {
+ socket.close();
+ }
+ }
+ catch (IOException e1) {
+ /* do nothing */
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Negotiates a SOCKS5 connection and stores it on success.
+ *
+ * @param socket connection to the client
+ * @throws XMPPException if client requests a connection in an unsupported way
+ * @throws IOException if a network error occurred
+ */
+ private void establishConnection(Socket socket) throws XMPPException, IOException {
+ DataOutputStream out = new DataOutputStream(socket.getOutputStream());
+ DataInputStream in = new DataInputStream(socket.getInputStream());
+
+ // first byte is version should be 5
+ int b = in.read();
+ if (b != 5) {
+ throw new XMPPException("Only SOCKS5 supported");
+ }
+
+ // second byte number of authentication methods supported
+ b = in.read();
+
+ // read list of supported authentication methods
+ byte[] auth = new byte[b];
+ in.readFully(auth);
+
+ byte[] authMethodSelectionResponse = new byte[2];
+ authMethodSelectionResponse[0] = (byte) 0x05; // protocol version
+
+ // only authentication method 0, no authentication, supported
+ boolean noAuthMethodFound = false;
+ for (int i = 0; i < auth.length; i++) {
+ if (auth[i] == (byte) 0x00) {
+ noAuthMethodFound = true;
+ break;
+ }
+ }
+
+ if (!noAuthMethodFound) {
+ authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods
+ out.write(authMethodSelectionResponse);
+ out.flush();
+ throw new XMPPException("Authentication method not supported");
+ }
+
+ authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method
+ out.write(authMethodSelectionResponse);
+ out.flush();
+
+ // receive connection request
+ byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in);
+
+ // extract digest
+ String responseDigest = new String(connectionRequest, 5, connectionRequest[4]);
+
+ // return error if digest is not allowed
+ if (!Socks5Proxy.this.allowedConnections.contains(responseDigest)) {
+ connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused)
+ out.write(connectionRequest);
+ out.flush();
+
+ throw new XMPPException("Connection is not allowed");
+ }
+
+ connectionRequest[1] = (byte) 0x00; // set return status to 0 (success)
+ out.write(connectionRequest);
+ out.flush();
+
+ // store connection
+ Socks5Proxy.this.connectionMap.put(responseDigest, socket);
+ }
+
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Utils.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Utils.java
new file mode 100644
index 0000000..9c92563
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Utils.java
@@ -0,0 +1,73 @@
+/**
+ * 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.DataInputStream;
+import java.io.IOException;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * A collection of utility methods for SOcKS5 messages.
+ *
+ * @author Henning Staib
+ */
+class Socks5Utils {
+
+ /**
+ * Returns a SHA-1 digest of the given parameters as specified in <a
+ * href="http://xmpp.org/extensions/xep-0065.html#impl-socks5">XEP-0065</a>.
+ *
+ * @param sessionID for the SOCKS5 Bytestream
+ * @param initiatorJID JID of the initiator of a SOCKS5 Bytestream
+ * @param targetJID JID of the target of a SOCKS5 Bytestream
+ * @return SHA-1 digest of the given parameters
+ */
+ public static String createDigest(String sessionID, String initiatorJID, String targetJID) {
+ StringBuilder b = new StringBuilder();
+ b.append(sessionID).append(initiatorJID).append(targetJID);
+ return StringUtils.hash(b.toString());
+ }
+
+ /**
+ * Reads a SOCKS5 message from the given InputStream. The message can either be a SOCKS5 request
+ * message or a SOCKS5 response message.
+ * <p>
+ * (see <a href="http://tools.ietf.org/html/rfc1928">RFC1928</a>)
+ *
+ * @param in the DataInputStream to read the message from
+ * @return the SOCKS5 message
+ * @throws IOException if a network error occurred
+ * @throws XMPPException if the SOCKS5 message contains an unsupported address type
+ */
+ public static byte[] receiveSocks5Message(DataInputStream in) throws IOException, XMPPException {
+ byte[] header = new byte[5];
+ in.readFully(header, 0, 5);
+
+ if (header[3] != (byte) 0x03) {
+ throw new XMPPException("Unsupported SOCKS5 address type");
+ }
+
+ int addressLength = header[4];
+
+ byte[] response = new byte[7 + addressLength];
+ System.arraycopy(header, 0, response, 0, header.length);
+
+ in.readFully(response, header.length, addressLength + 2);
+
+ return response;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java b/src/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java
new file mode 100644
index 0000000..9e07fc3
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java
@@ -0,0 +1,474 @@
+/**
+ * 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.packet;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * A packet representing part of a SOCKS5 Bytestream negotiation.
+ *
+ * @author Alexander Wenckus
+ */
+public class Bytestream extends IQ {
+
+ private String sessionID;
+
+ private Mode mode = Mode.tcp;
+
+ private final List<StreamHost> streamHosts = new ArrayList<StreamHost>();
+
+ private StreamHostUsed usedHost;
+
+ private Activate toActivate;
+
+ /**
+ * The default constructor
+ */
+ public Bytestream() {
+ super();
+ }
+
+ /**
+ * A constructor where the session ID can be specified.
+ *
+ * @param SID The session ID related to the negotiation.
+ * @see #setSessionID(String)
+ */
+ public Bytestream(final String SID) {
+ super();
+ setSessionID(SID);
+ }
+
+ /**
+ * Set the session ID related to the bytestream. The session ID is a unique identifier used to
+ * differentiate between stream negotiations.
+ *
+ * @param sessionID the unique session ID that identifies the transfer.
+ */
+ public void setSessionID(final String sessionID) {
+ this.sessionID = sessionID;
+ }
+
+ /**
+ * Returns the session ID related to the bytestream negotiation.
+ *
+ * @return Returns the session ID related to the bytestream negotiation.
+ * @see #setSessionID(String)
+ */
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ /**
+ * Set the transport mode. This should be put in the initiation of the interaction.
+ *
+ * @param mode the transport mode, either UDP or TCP
+ * @see Mode
+ */
+ public void setMode(final Mode mode) {
+ this.mode = mode;
+ }
+
+ /**
+ * Returns the transport mode.
+ *
+ * @return Returns the transport mode.
+ * @see #setMode(Mode)
+ */
+ public Mode getMode() {
+ return mode;
+ }
+
+ /**
+ * Adds a potential stream host that the remote user can connect to to receive the file.
+ *
+ * @param JID The JID of the stream host.
+ * @param address The internet address of the stream host.
+ * @return The added stream host.
+ */
+ public StreamHost addStreamHost(final String JID, final String address) {
+ return addStreamHost(JID, address, 0);
+ }
+
+ /**
+ * Adds a potential stream host that the remote user can connect to to receive the file.
+ *
+ * @param JID The JID of the stream host.
+ * @param address The internet address of the stream host.
+ * @param port The port on which the remote host is seeking connections.
+ * @return The added stream host.
+ */
+ public StreamHost addStreamHost(final String JID, final String address, final int port) {
+ StreamHost host = new StreamHost(JID, address);
+ host.setPort(port);
+ addStreamHost(host);
+
+ return host;
+ }
+
+ /**
+ * Adds a potential stream host that the remote user can transfer the file through.
+ *
+ * @param host The potential stream host.
+ */
+ public void addStreamHost(final StreamHost host) {
+ streamHosts.add(host);
+ }
+
+ /**
+ * Returns the list of stream hosts contained in the packet.
+ *
+ * @return Returns the list of stream hosts contained in the packet.
+ */
+ public Collection<StreamHost> getStreamHosts() {
+ return Collections.unmodifiableCollection(streamHosts);
+ }
+
+ /**
+ * Returns the stream host related to the given JID, or null if there is none.
+ *
+ * @param JID The JID of the desired stream host.
+ * @return Returns the stream host related to the given JID, or null if there is none.
+ */
+ public StreamHost getStreamHost(final String JID) {
+ if (JID == null) {
+ return null;
+ }
+ for (StreamHost host : streamHosts) {
+ if (host.getJID().equals(JID)) {
+ return host;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the count of stream hosts contained in this packet.
+ *
+ * @return Returns the count of stream hosts contained in this packet.
+ */
+ public int countStreamHosts() {
+ return streamHosts.size();
+ }
+
+ /**
+ * Upon connecting to the stream host the target of the stream replies to the initiator with the
+ * JID of the SOCKS5 host that they used.
+ *
+ * @param JID The JID of the used host.
+ */
+ public void setUsedHost(final String JID) {
+ this.usedHost = new StreamHostUsed(JID);
+ }
+
+ /**
+ * Returns the SOCKS5 host connected to by the remote user.
+ *
+ * @return Returns the SOCKS5 host connected to by the remote user.
+ */
+ public StreamHostUsed getUsedHost() {
+ return usedHost;
+ }
+
+ /**
+ * Returns the activate element of the packet sent to the proxy host to verify the identity of
+ * the initiator and match them to the appropriate stream.
+ *
+ * @return Returns the activate element of the packet sent to the proxy host to verify the
+ * identity of the initiator and match them to the appropriate stream.
+ */
+ public Activate getToActivate() {
+ return toActivate;
+ }
+
+ /**
+ * Upon the response from the target of the used host the activate packet is sent to the SOCKS5
+ * proxy. The proxy will activate the stream or return an error after verifying the identity of
+ * the initiator, using the activate packet.
+ *
+ * @param targetID The JID of the target of the file transfer.
+ */
+ public void setToActivate(final String targetID) {
+ this.toActivate = new Activate(targetID);
+ }
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<query xmlns=\"http://jabber.org/protocol/bytestreams\"");
+ if (this.getType().equals(IQ.Type.SET)) {
+ if (getSessionID() != null) {
+ buf.append(" sid=\"").append(getSessionID()).append("\"");
+ }
+ if (getMode() != null) {
+ buf.append(" mode = \"").append(getMode()).append("\"");
+ }
+ buf.append(">");
+ if (getToActivate() == null) {
+ for (StreamHost streamHost : getStreamHosts()) {
+ buf.append(streamHost.toXML());
+ }
+ }
+ else {
+ buf.append(getToActivate().toXML());
+ }
+ }
+ else if (this.getType().equals(IQ.Type.RESULT)) {
+ buf.append(">");
+ if (getUsedHost() != null) {
+ buf.append(getUsedHost().toXML());
+ }
+ // A result from the server can also contain stream hosts
+ else if (countStreamHosts() > 0) {
+ for (StreamHost host : streamHosts) {
+ buf.append(host.toXML());
+ }
+ }
+ }
+ else if (this.getType().equals(IQ.Type.GET)) {
+ return buf.append("/>").toString();
+ }
+ else {
+ return null;
+ }
+ buf.append("</query>");
+
+ return buf.toString();
+ }
+
+ /**
+ * Packet extension that represents a potential SOCKS5 proxy for the file transfer. Stream hosts
+ * are forwarded to the target of the file transfer who then chooses and connects to one.
+ *
+ * @author Alexander Wenckus
+ */
+ public static class StreamHost implements PacketExtension {
+
+ public static String NAMESPACE = "";
+
+ public static String ELEMENTNAME = "streamhost";
+
+ private final String JID;
+
+ private final String addy;
+
+ private int port = 0;
+
+ /**
+ * Default constructor.
+ *
+ * @param JID The JID of the stream host.
+ * @param address The internet address of the stream host.
+ */
+ public StreamHost(final String JID, final String address) {
+ this.JID = JID;
+ this.addy = address;
+ }
+
+ /**
+ * Returns the JID of the stream host.
+ *
+ * @return Returns the JID of the stream host.
+ */
+ public String getJID() {
+ return JID;
+ }
+
+ /**
+ * Returns the internet address of the stream host.
+ *
+ * @return Returns the internet address of the stream host.
+ */
+ public String getAddress() {
+ return addy;
+ }
+
+ /**
+ * Sets the port of the stream host.
+ *
+ * @param port The port on which the potential stream host would accept the connection.
+ */
+ public void setPort(final int port) {
+ this.port = port;
+ }
+
+ /**
+ * Returns the port on which the potential stream host would accept the connection.
+ *
+ * @return Returns the port on which the potential stream host would accept the connection.
+ */
+ public int getPort() {
+ return port;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String getElementName() {
+ return ELEMENTNAME;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(getElementName()).append(" ");
+ buf.append("jid=\"").append(getJID()).append("\" ");
+ buf.append("host=\"").append(getAddress()).append("\" ");
+ if (getPort() != 0) {
+ buf.append("port=\"").append(getPort()).append("\"");
+ }
+ else {
+ buf.append("zeroconf=\"_jabber.bytestreams\"");
+ }
+ buf.append("/>");
+
+ return buf.toString();
+ }
+ }
+
+ /**
+ * After selected a SOCKS5 stream host and successfully connecting, the target of the file
+ * transfer returns a byte stream packet with the stream host used extension.
+ *
+ * @author Alexander Wenckus
+ */
+ public static class StreamHostUsed implements PacketExtension {
+
+ public String NAMESPACE = "";
+
+ public static String ELEMENTNAME = "streamhost-used";
+
+ private final String JID;
+
+ /**
+ * Default constructor.
+ *
+ * @param JID The JID of the selected stream host.
+ */
+ public StreamHostUsed(final String JID) {
+ this.JID = JID;
+ }
+
+ /**
+ * Returns the JID of the selected stream host.
+ *
+ * @return Returns the JID of the selected stream host.
+ */
+ public String getJID() {
+ return JID;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String getElementName() {
+ return ELEMENTNAME;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<").append(getElementName()).append(" ");
+ buf.append("jid=\"").append(getJID()).append("\" ");
+ buf.append("/>");
+ return buf.toString();
+ }
+ }
+
+ /**
+ * The packet sent by the stream initiator to the stream proxy to activate the connection.
+ *
+ * @author Alexander Wenckus
+ */
+ public static class Activate implements PacketExtension {
+
+ public String NAMESPACE = "";
+
+ public static String ELEMENTNAME = "activate";
+
+ private final String target;
+
+ /**
+ * Default constructor specifying the target of the stream.
+ *
+ * @param target The target of the stream.
+ */
+ public Activate(final String target) {
+ this.target = target;
+ }
+
+ /**
+ * Returns the target of the activation.
+ *
+ * @return Returns the target of the activation.
+ */
+ public String getTarget() {
+ return target;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String getElementName() {
+ return ELEMENTNAME;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<").append(getElementName()).append(">");
+ buf.append(getTarget());
+ buf.append("</").append(getElementName()).append(">");
+ return buf.toString();
+ }
+ }
+
+ /**
+ * The stream can be either a TCP stream or a UDP stream.
+ *
+ * @author Alexander Wenckus
+ */
+ public enum Mode {
+
+ /**
+ * A TCP based stream.
+ */
+ tcp,
+
+ /**
+ * A UDP based stream.
+ */
+ udp;
+
+ public static Mode fromName(String name) {
+ Mode mode;
+ try {
+ mode = Mode.valueOf(name);
+ }
+ catch (Exception ex) {
+ mode = tcp;
+ }
+
+ return mode;
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/provider/BytestreamsProvider.java b/src/org/jivesoftware/smackx/bytestreams/socks5/provider/BytestreamsProvider.java
new file mode 100644
index 0000000..76f9b0c
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/provider/BytestreamsProvider.java
@@ -0,0 +1,82 @@
+/**
+ * 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.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Parses a bytestream packet.
+ *
+ * @author Alexander Wenckus
+ */
+public class BytestreamsProvider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ boolean done = false;
+
+ Bytestream toReturn = new Bytestream();
+
+ String id = parser.getAttributeValue("", "sid");
+ String mode = parser.getAttributeValue("", "mode");
+
+ // streamhost
+ String JID = null;
+ String host = null;
+ String port = null;
+
+ int eventType;
+ String elementName;
+ while (!done) {
+ eventType = parser.next();
+ elementName = parser.getName();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (elementName.equals(Bytestream.StreamHost.ELEMENTNAME)) {
+ JID = parser.getAttributeValue("", "jid");
+ host = parser.getAttributeValue("", "host");
+ port = parser.getAttributeValue("", "port");
+ }
+ else if (elementName.equals(Bytestream.StreamHostUsed.ELEMENTNAME)) {
+ toReturn.setUsedHost(parser.getAttributeValue("", "jid"));
+ }
+ else if (elementName.equals(Bytestream.Activate.ELEMENTNAME)) {
+ toReturn.setToActivate(parser.getAttributeValue("", "jid"));
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (elementName.equals("streamhost")) {
+ if (port == null) {
+ toReturn.addStreamHost(JID, host);
+ }
+ else {
+ toReturn.addStreamHost(JID, host, Integer.parseInt(port));
+ }
+ JID = null;
+ host = null;
+ port = null;
+ }
+ else if (elementName.equals("query")) {
+ done = true;
+ }
+ }
+ }
+
+ toReturn.setMode((Bytestream.Mode.fromName(mode)));
+ toReturn.setSessionID(id);
+ return toReturn;
+ }
+
+}