/** * 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.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Random; 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 XEP-0065. *
* 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. *
* 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. *
* 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. *
* If a session ID for the SOCKS5 Bytestream was already negotiated (e.g. while negotiating a file * transfer) invoke {@link #establishSession(String, String)}. *
* 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)}. *
* Note that the registered {@link Socks5BytestreamListener} will NOT be notified on incoming Socks5 * bytestream requests sent in the context of XEP-0096 file transfer. (See * {@link FileTransferManager}) *
* If no {@link Socks5BytestreamListener}s are registered, all incoming SOCKS5 Bytestream requests
* will be rejected by returning a <not-acceptable/> 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(Connection connection) {
final Socks5BytestreamManager manager;
manager = Socks5BytestreamManager.getBytestreamManager(connection);
// register shutdown listener
connection.addConnectionListener(new AbstractConnectionListener() {
public void connectionClosed() {
manager.disableService();
}
});
}
});
}
/**
* 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
* If no manager exists a new is created and initialized.
*
* @param connection the XMPP connection or
* If no listeners are registered all SOCKS5 Bytestream request are rejected with a
* <not-acceptable/> error.
*
* Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
* bytestream requests sent in the context of XEP-0096 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.
*
* Use this method if you are awaiting an incoming SOCKS5 Bytestream request from a specific
* user.
*
* If no listeners are registered all SOCKS5 Bytestream request are rejected with a
* <not-acceptable/> error.
*
* Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
* bytestream requests sent in the context of XEP-0096 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.
*
* 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.
*
* 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
* 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.
*
* 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 {
// check if target supports SOCKS5 Bytestream
if (!supportsSocks5(targetJID)) {
throw new XMPPException(targetJID + " doesn't support SOCKS5 Bytestream");
}
// determine SOCKS5 proxies from XMPP-server
Listnull
if given connection is
* null
* @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.
* true
.
*
* @return true
if prioritization is enabled, false
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.
* true
if the given target JID supports feature SOCKS5 Bytestream.
*
* @param targetJID the target JID
* @return true
if the given target JID supports feature SOCKS5 Bytestream
* otherwise false
* @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