1
0
Fork 0
mirror of https://codeberg.org/Mercury-IM/Smack synced 2025-12-06 05:01:12 +01:00

merged branch improve_bytestreams in trunk

git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/trunk@11821 b35dd754-fafc-0310-a699-88a17e54d16e
This commit is contained in:
Henning Staib 2010-08-15 11:57:11 +00:00 committed by henning
parent ef74695a1b
commit 8b54f34153
74 changed files with 11866 additions and 1902 deletions

View file

@ -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();
}
}

View file

@ -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);
}

View file

@ -0,0 +1,760 @@
/**
* 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.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.SyncPacketSend;
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(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<Connection, Socks5BytestreamManager> managers = new HashMap<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 {
// check if target supports SOCKS5 Bytestream
if (!supportsSocks5(targetJID)) {
throw new XMPPException(targetJID + " doesn't support SOCKS5 Bytestream");
}
// determine SOCKS5 proxies from XMPP-server
List<String> proxies = determineProxies();
// determine address and port of each proxy
List<StreamHost> streamHosts = determineStreamHostInfos(proxies);
// compute digest
String digest = Socks5Utils.createDigest(sessionID, this.connection.getUser(), targetJID);
if (streamHosts.isEmpty()) {
throw new XMPPException("no SOCKS5 proxies available");
}
// 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;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,81 @@
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();
}
}

View file

@ -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;
}
}

View file

@ -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.smackx.bytestreams.socks5.packet.Bytestream;
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
import org.jivesoftware.smackx.packet.SyncPacketSend;
/**
* 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;
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}