diff --git a/build/build/java-xmlbuilder-0.3.jar b/build/build/java-xmlbuilder-0.3.jar
new file mode 100644
index 000000000..87a1d5c90
Binary files /dev/null and b/build/build/java-xmlbuilder-0.3.jar differ
diff --git a/build/javassist-3.10.0.GA.jar b/build/javassist-3.10.0.GA.jar
new file mode 100644
index 000000000..342ea8268
Binary files /dev/null and b/build/javassist-3.10.0.GA.jar differ
diff --git a/build/mockito-all-1.8.2.jar b/build/mockito-all-1.8.2.jar
new file mode 100644
index 000000000..74c278920
Binary files /dev/null and b/build/mockito-all-1.8.2.jar differ
diff --git a/build/objenesis-1.1.jar b/build/objenesis-1.1.jar
new file mode 100644
index 000000000..f178db330
Binary files /dev/null and b/build/objenesis-1.1.jar differ
diff --git a/build/powermock-mockito-1.3.5-full.jar b/build/powermock-mockito-1.3.5-full.jar
new file mode 100644
index 000000000..aa0478c4e
Binary files /dev/null and b/build/powermock-mockito-1.3.5-full.jar differ
diff --git a/build/resources/META-INF/smack-config.xml b/build/resources/META-INF/smack-config.xml
index 5b11d4add..5378c6f47 100644
--- a/build/resources/META-INF/smack-config.xml
+++ b/build/resources/META-INF/smack-config.xml
@@ -8,16 +8,24 @@
org.jivesoftware.smack.PrivacyListManagerorg.jivesoftware.smackx.XHTMLManagerorg.jivesoftware.smackx.muc.MultiUserChat
+ org.jivesoftware.smackx.ibb.InBandBytestreamManager
+ org.jivesoftware.smackx.socks5bytestream.Socks5BytestreamManagerorg.jivesoftware.smackx.filetransfer.FileTransferManagerorg.jivesoftware.smackx.LastActivityManagerorg.jivesoftware.smack.ReconnectionManagerorg.jivesoftware.smackx.commands.AdHocCommandManager
-
+
5000
-
+
30000
-
\ No newline at end of file
+
+ true
+
+
+ 7777
+
+
diff --git a/build/resources/META-INF/smack.providers b/build/resources/META-INF/smack.providers
index 7092b8a61..f4f992d4d 100644
--- a/build/resources/META-INF/smack.providers
+++ b/build/resources/META-INF/smack.providers
@@ -196,25 +196,31 @@
queryhttp://jabber.org/protocol/bytestreams
- org.jivesoftware.smackx.provider.BytestreamsProvider
+ org.jivesoftware.smackx.socks5bytestream.provider.BytestreamsProvideropenhttp://jabber.org/protocol/ibb
- org.jivesoftware.smackx.provider.IBBProviders$Open
+ org.jivesoftware.smackx.ibb.provider.OpenIQProvider
+
+
+
+ data
+ http://jabber.org/protocol/ibb
+ org.jivesoftware.smackx.ibb.provider.DataPacketProviderclosehttp://jabber.org/protocol/ibb
- org.jivesoftware.smackx.provider.IBBProviders$Close
+ org.jivesoftware.smackx.ibb.provider.CloseIQProviderdatahttp://jabber.org/protocol/ibb
- org.jivesoftware.smackx.provider.IBBProviders$Data
+ org.jivesoftware.smackx.ibb.provider.DataPacketProvider
diff --git a/source/org/jivesoftware/smack/SmackConfiguration.java b/source/org/jivesoftware/smack/SmackConfiguration.java
index faa78ff3a..86dc7f397 100644
--- a/source/org/jivesoftware/smack/SmackConfiguration.java
+++ b/source/org/jivesoftware/smack/SmackConfiguration.java
@@ -50,6 +50,9 @@ public final class SmackConfiguration {
private static int keepAliveInterval = 30000;
private static Vector defaultMechs = new Vector();
+ private static boolean localSocks5ProxyEnabled = true;
+ private static int localSocks5ProxyPort = 7777;
+
private SmackConfiguration() {
}
@@ -90,6 +93,12 @@ public final class SmackConfiguration {
}
else if (parser.getName().equals("mechName")) {
defaultMechs.add(parser.nextText());
+ } else if (parser.getName().equals("localSocks5ProxyEnabled")) {
+ localSocks5ProxyEnabled = Boolean.parseBoolean(parser
+ .nextText());
+ } else if (parser.getName().equals("localSocks5ProxyPort")) {
+ localSocks5ProxyPort = parseIntProperty(parser,
+ localSocks5ProxyPort);
}
}
eventType = parser.next();
@@ -230,6 +239,43 @@ public final class SmackConfiguration {
return defaultMechs;
}
+ /**
+ * Returns true if the local Socks5 proxy should be started. Default is true.
+ *
+ * @return if the local Socks5 proxy should be started
+ */
+ public static boolean isLocalSocks5ProxyEnabled() {
+ return localSocks5ProxyEnabled;
+ }
+
+ /**
+ * Sets if the local Socks5 proxy should be started. Default is true.
+ *
+ * @param localSocks5ProxyEnabled if the local Socks5 proxy should be started
+ */
+ public static void setLocalSocks5ProxyEnabled(boolean localSocks5ProxyEnabled) {
+ SmackConfiguration.localSocks5ProxyEnabled = localSocks5ProxyEnabled;
+ }
+
+ /**
+ * Return the port of the local Socks5 proxy. Default is 7777.
+ *
+ * @return the port of the local Socks5 proxy
+ */
+ public static int getLocalSocks5ProxyPort() {
+ return localSocks5ProxyPort;
+ }
+
+ /**
+ * Sets the port of the local Socks5 proxy. Default is 7777. If you set the port to a negative
+ * value Smack tries the absolute value and all following until it finds an open port.
+ *
+ * @param localSocks5ProxyPort the port of the local Socks5 proxy to set
+ */
+ public static void setLocalSocks5ProxyPort(int localSocks5ProxyPort) {
+ SmackConfiguration.localSocks5ProxyPort = localSocks5ProxyPort;
+ }
+
private static void parseClassToLoad(XmlPullParser parser) throws Exception {
String className = parser.nextText();
// Attempt to load the class so that the class can get initialized
diff --git a/source/org/jivesoftware/smackx/bytestreams/BytestreamListener.java b/source/org/jivesoftware/smackx/bytestreams/BytestreamListener.java
new file mode 100644
index 000000000..5cc698008
--- /dev/null
+++ b/source/org/jivesoftware/smackx/bytestreams/BytestreamListener.java
@@ -0,0 +1,47 @@
+/**
+ * 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;
+
+import org.jivesoftware.smackx.ibb.InBandBytestreamListener;
+import org.jivesoftware.smackx.ibb.InBandBytestreamManager;
+import org.jivesoftware.smackx.socks5bytestream.Socks5BytestreamListener;
+import org.jivesoftware.smackx.socks5bytestream.Socks5BytestreamManager;
+
+/**
+ * BytestreamListener are notified if a remote user wants to initiate a bytestream. Implement this
+ * interface to handle incoming bytestream requests.
+ *
+ * BytestreamListener can be registered at the {@link Socks5BytestreamManager} or the
+ * {@link InBandBytestreamManager}.
+ *
+ * There are two ways to add this listener. See
+ * {@link BytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and
+ * {@link BytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)} for further
+ * details.
+ *
+ * {@link Socks5BytestreamListener} or {@link InBandBytestreamListener} provide a more specific
+ * interface of the BytestreamListener.
+ *
+ * @author Henning Staib
+ */
+public interface BytestreamListener {
+
+ /**
+ * This listener is notified if a bytestream request from another user has been received.
+ *
+ * @param request the incoming bytestream request
+ */
+ public void incomingBytestreamRequest(BytestreamRequest request);
+
+}
diff --git a/source/org/jivesoftware/smackx/bytestreams/BytestreamManager.java b/source/org/jivesoftware/smackx/bytestreams/BytestreamManager.java
new file mode 100644
index 000000000..cd61c5a0c
--- /dev/null
+++ b/source/org/jivesoftware/smackx/bytestreams/BytestreamManager.java
@@ -0,0 +1,114 @@
+/**
+ * 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;
+
+import java.io.IOException;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smackx.ibb.InBandBytestreamManager;
+import org.jivesoftware.smackx.socks5bytestream.Socks5BytestreamManager;
+
+/**
+ * BytestreamManager provides a generic interface for bytestream managers.
+ *
+ * There are two implementations of the interface. See {@link Socks5BytestreamManager} and
+ * {@link InBandBytestreamManager}.
+ *
+ * @author Henning Staib
+ */
+public interface BytestreamManager {
+
+ /**
+ * Adds {@link BytestreamListener} that is called for every incoming bytestream request unless
+ * there is a user specific {@link BytestreamListener} registered.
+ *
+ * See {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and
+ * {@link InBandBytestreamManager#addIncomingBytestreamListener(BytestreamListener)} for further
+ * details.
+ *
+ * @param listener the listener to register
+ */
+ public void addIncomingBytestreamListener(BytestreamListener listener);
+
+ /**
+ * Removes the given listener from the list of listeners for all incoming bytestream requests.
+ *
+ * @param listener the listener to remove
+ */
+ public void removeIncomingBytestreamListener(BytestreamListener listener);
+
+ /**
+ * Adds {@link BytestreamListener} that is called for every incoming bytestream request unless
+ * there is a user specific {@link BytestreamListener} registered.
+ *
+ * Use this method if you are awaiting an incoming bytestream request from a specific user.
+ *
+ * See {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)}
+ * and {@link InBandBytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)}
+ * for further details.
+ *
+ * @param listener the listener to register
+ * @param initiatorJID the JID of the user that wants to establish a bytestream
+ */
+ public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID);
+
+ /**
+ * Removes the listener for the given user.
+ *
+ * @param initiatorJID the JID of the user the listener should be removed
+ */
+ public void removeIncomingBytestreamListener(String initiatorJID);
+
+ /**
+ * Establishes a bytestream with the given user and returns the session to send/receive data
+ * to/from the user.
+ *
+ * Use this method to establish bytestreams to users accepting all incoming bytestream requests
+ * since this method doesn't provide a way to tell the user something about the data to be sent.
+ *
+ * To establish a bytestream after negotiation the kind of data to be sent (e.g. file transfer)
+ * use {@link #establishSession(String, String)}.
+ *
+ * See {@link Socks5BytestreamManager#establishSession(String)} and
+ * {@link InBandBytestreamManager#establishSession(String)} for further details.
+ *
+ * @param targetJID the JID of the user a bytestream should be established
+ * @return the session to send/receive data to/from the user
+ * @throws XMPPException if an error occurred while establishing the session
+ * @throws IOException if an IO error occurred while establishing the session
+ * @throws InterruptedException if the thread was interrupted while waiting in a blocking
+ * operation
+ */
+ public BytestreamSession establishSession(String targetJID) throws XMPPException, IOException,
+ InterruptedException;
+
+ /**
+ * Establishes a bytestream with the given user and returns the session to send/receive data
+ * to/from the user.
+ *
+ * See {@link Socks5BytestreamManager#establishSession(String)} and
+ * {@link InBandBytestreamManager#establishSession(String)} for further details.
+ *
+ * @param targetJID the JID of the user a bytestream should be established
+ * @param sessionID the session ID for the bytestream request
+ * @return the session to send/receive data to/from the user
+ * @throws XMPPException if an error occurred while establishing the session
+ * @throws IOException if an IO error occurred while establishing the session
+ * @throws InterruptedException if the thread was interrupted while waiting in a blocking
+ * operation
+ */
+ public BytestreamSession establishSession(String targetJID, String sessionID)
+ throws XMPPException, IOException, InterruptedException;
+
+}
diff --git a/source/org/jivesoftware/smackx/bytestreams/BytestreamRequest.java b/source/org/jivesoftware/smackx/bytestreams/BytestreamRequest.java
new file mode 100644
index 000000000..e10a65994
--- /dev/null
+++ b/source/org/jivesoftware/smackx/bytestreams/BytestreamRequest.java
@@ -0,0 +1,59 @@
+/**
+ * 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;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smackx.ibb.InBandBytestreamRequest;
+import org.jivesoftware.smackx.socks5bytestream.Socks5BytestreamRequest;
+
+/**
+ * BytestreamRequest provides an interface to handle incoming bytestream requests.
+ *
+ * There are two implementations of the interface. See {@link Socks5BytestreamRequest} and
+ * {@link InBandBytestreamRequest}.
+ *
+ * @author Henning Staib
+ */
+public interface BytestreamRequest {
+
+ /**
+ * Returns the sender of the bytestream open request.
+ *
+ * @return the sender of the bytestream open request
+ */
+ public String getFrom();
+
+ /**
+ * Returns the session ID of the bytestream open request.
+ *
+ * @return the session ID of the bytestream open request
+ */
+ public String getSessionID();
+
+ /**
+ * Accepts the bytestream open request and returns the session to send/receive data.
+ *
+ * @return the session to send/receive data
+ * @throws XMPPException if an error occurred while accepting the bytestream request
+ * @throws InterruptedException if the thread was interrupted while waiting in a blocking
+ * operation
+ */
+ public BytestreamSession accept() throws XMPPException, InterruptedException;
+
+ /**
+ * Rejects the bytestream request by sending a reject error to the initiator.
+ */
+ public void reject();
+
+}
diff --git a/source/org/jivesoftware/smackx/bytestreams/BytestreamSession.java b/source/org/jivesoftware/smackx/bytestreams/BytestreamSession.java
new file mode 100644
index 000000000..a0b9b3805
--- /dev/null
+++ b/source/org/jivesoftware/smackx/bytestreams/BytestreamSession.java
@@ -0,0 +1,81 @@
+/**
+ * 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;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.jivesoftware.smackx.ibb.InBandBytestreamSession;
+import org.jivesoftware.smackx.socks5bytestream.Socks5BytestreamSession;
+
+/**
+ * BytestreamSession provides an interface for established bytestream sessions.
+ *
+ * There are two implementations of the interface. See {@link Socks5BytestreamSession} and
+ * {@link InBandBytestreamSession}.
+ *
+ * @author Henning Staib
+ */
+public interface BytestreamSession {
+
+ /**
+ * Returns the InputStream associated with this session to send data.
+ *
+ * @return the InputStream associated with this session to send data
+ * @throws IOException if an error occurs while retrieving the input stream
+ */
+ public InputStream getInputStream() throws IOException;
+
+ /**
+ * Returns the OutputStream associated with this session to receive data.
+ *
+ * @return the OutputStream associated with this session to receive data
+ * @throws IOException if an error occurs while retrieving the output stream
+ */
+ public OutputStream getOutputStream() throws IOException;
+
+ /**
+ * Closes the bytestream session.
+ *
+ * Closing the session will also close the input stream and the output stream associated to this
+ * session.
+ *
+ * @throws IOException if an error occurs while closing the session
+ */
+ public void close() throws IOException;
+
+ /**
+ * Returns the timeout for read operations of the input stream associated with this session. 0
+ * returns implies that the option is disabled (i.e., timeout of infinity). Default is 0.
+ *
+ * @return the timeout for read operations
+ * @throws IOException if there is an error in the underlying protocol
+ */
+ public int getReadTimeout() throws IOException;
+
+ /**
+ * Sets the specified timeout, in milliseconds. With this option set to a non-zero timeout, a
+ * read() call on the input stream associated with this session will block for only this amount
+ * of time. If the timeout expires, a java.net.SocketTimeoutException is raised, though the
+ * session is still valid. The option must be enabled prior to entering the blocking operation
+ * to have effect. The timeout must be > 0. A timeout of zero is interpreted as an infinite
+ * timeout. Default is 0.
+ *
+ * @param timeout the specified timeout, in milliseconds
+ * @throws IOException if there is an error in the underlying protocol
+ */
+ public void setReadTimeout(int timeout) throws IOException;
+
+}
diff --git a/source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java b/source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java
index 0a910dc99..044e293b4 100644
--- a/source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java
+++ b/source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java
@@ -19,9 +19,20 @@
*/
package org.jivesoftware.smackx.filetransfer;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.PacketCollector;
-import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketIDFilter;
import org.jivesoftware.smack.packet.IQ;
@@ -30,12 +41,10 @@ import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smackx.Form;
import org.jivesoftware.smackx.FormField;
import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.ibb.InBandBytestreamManager;
import org.jivesoftware.smackx.packet.DataForm;
import org.jivesoftware.smackx.packet.StreamInitiation;
-
-import java.net.URLConnection;
-import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
+import org.jivesoftware.smackx.socks5bytestream.Socks5BytestreamManager;
/**
* Manages the negotiation of file transfers according to JEP-0096. If a file is
@@ -43,27 +52,15 @@ import java.util.concurrent.ConcurrentHashMap;
* will be sent.
*
* @author Alexander Wenckus
- * @see JEP-0096: File Transfer
+ * @see JEP-0096: File Transfer
*/
public class FileTransferNegotiator {
// Static
- /**
- * The XMPP namespace of the SOCKS5 bytestream
- */
- public static final String BYTE_STREAM = "http://jabber.org/protocol/bytestreams";
-
- /**
- * The XMPP namespace of the In-Band bytestream
- */
- public static final String INBAND_BYTE_STREAM = "http://jabber.org/protocol/ibb";
-
private static final String[] NAMESPACE = {
"http://jabber.org/protocol/si/profile/file-transfer",
- "http://jabber.org/protocol/si", BYTE_STREAM, INBAND_BYTE_STREAM};
-
- private static final String[] PROTOCOLS = {BYTE_STREAM, INBAND_BYTE_STREAM};
+ "http://jabber.org/protocol/si"};
private static final Map transferObject =
new ConcurrentHashMap();
@@ -121,14 +118,24 @@ public class FileTransferNegotiator {
final boolean isEnabled) {
ServiceDiscoveryManager manager = ServiceDiscoveryManager
.getInstanceFor(connection);
- for (String ns : NAMESPACE) {
+
+ List namespaces = new ArrayList();
+ namespaces.addAll(Arrays.asList(NAMESPACE));
+ namespaces.add(InBandBytestreamManager.NAMESPACE);
+ if (!IBB_ONLY) {
+ namespaces.add(Socks5BytestreamManager.NAMESPACE);
+ }
+
+ for (String namespace : namespaces) {
if (isEnabled) {
- manager.addFeature(ns);
- }
- else {
- manager.removeFeature(ns);
+ if (!manager.includesFeature(namespace)) {
+ manager.addFeature(namespace);
+ }
+ } else {
+ manager.removeFeature(namespace);
}
}
+
}
/**
@@ -139,20 +146,31 @@ public class FileTransferNegotiator {
* @return True if all related services are enabled, false if they are not.
*/
public static boolean isServiceEnabled(final Connection connection) {
- for (String ns : NAMESPACE) {
- if (!ServiceDiscoveryManager.getInstanceFor(connection).includesFeature(ns))
+ ServiceDiscoveryManager manager = ServiceDiscoveryManager
+ .getInstanceFor(connection);
+
+ List namespaces = new ArrayList();
+ namespaces.addAll(Arrays.asList(NAMESPACE));
+ namespaces.add(InBandBytestreamManager.NAMESPACE);
+ if (!IBB_ONLY) {
+ namespaces.add(Socks5BytestreamManager.NAMESPACE);
+ }
+
+ for (String namespace : namespaces) {
+ if (!manager.includesFeature(namespace)) {
return false;
+ }
}
return true;
}
/**
- * A convience method to create an IQ packet.
+ * A convenience method to create an IQ packet.
*
* @param ID The packet ID of the
* @param to To whom the packet is addressed.
* @param from From whom the packet is sent.
- * @param type The iq type of the packet.
+ * @param type The IQ type of the packet.
* @return The created IQ packet.
*/
public static IQ createIQ(final String ID, final String to,
@@ -176,14 +194,19 @@ public class FileTransferNegotiator {
* @return Returns a collection of the supported transfer protocols.
*/
public static Collection getSupportedProtocols() {
- return Collections.unmodifiableList(Arrays.asList(PROTOCOLS));
+ List protocols = new ArrayList();
+ protocols.add(InBandBytestreamManager.NAMESPACE);
+ if (!IBB_ONLY) {
+ protocols.add(Socks5BytestreamManager.NAMESPACE);
+ }
+ return Collections.unmodifiableList(protocols);
}
// non-static
private final Connection connection;
- private final Socks5TransferNegotiatorManager byteStreamTransferManager;
+ private final StreamNegotiator byteStreamTransferManager;
private final StreamNegotiator inbandTransferManager;
@@ -191,7 +214,7 @@ public class FileTransferNegotiator {
configureConnection(connection);
this.connection = connection;
- byteStreamTransferManager = new Socks5TransferNegotiatorManager(connection);
+ byteStreamTransferManager = new Socks5TransferNegotiator(connection);
inbandTransferManager = new IBBTransferNegotiator(connection);
}
@@ -221,7 +244,6 @@ public class FileTransferNegotiator {
private void cleanup(final Connection connection) {
if (transferObject.remove(connection) != null) {
- byteStreamTransferManager.cleanup();
inbandTransferManager.cleanup();
}
}
@@ -288,10 +310,10 @@ public class FileTransferNegotiator {
boolean isIBB = false;
for (Iterator it = field.getOptions(); it.hasNext();) {
variable = it.next().getValue();
- if (variable.equals(BYTE_STREAM) && !IBB_ONLY) {
+ if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) {
isByteStream = true;
}
- else if (variable.equals(INBAND_BYTE_STREAM)) {
+ else if (variable.equals(InBandBytestreamManager.NAMESPACE)) {
isIBB = true;
}
}
@@ -304,11 +326,11 @@ public class FileTransferNegotiator {
if (isByteStream && isIBB && field.getType().equals(FormField.TYPE_LIST_MULTI)) {
return new FaultTolerantNegotiator(connection,
- byteStreamTransferManager.createNegotiator(),
+ byteStreamTransferManager,
inbandTransferManager);
}
else if (isByteStream) {
- return byteStreamTransferManager.createNegotiator();
+ return byteStreamTransferManager;
}
else {
return inbandTransferManager;
@@ -346,11 +368,11 @@ public class FileTransferNegotiator {
* the option of, accepting, rejecting, or not responding to a received file
* transfer request.
*
- * If they accept, the packet will contain the other user's choosen stream
+ * If they accept, the packet will contain the other user's chosen stream
* type to send the file across. The two choices this implementation
* provides to the other user for file transfer are SOCKS5 Bytestreams,
- * which is the prefered method of transfer, and In-Band Bytestreams,
* which is the fallback mechanism.
*
@@ -422,10 +444,10 @@ public class FileTransferNegotiator {
boolean isIBB = false;
for (Iterator it = field.getValues(); it.hasNext();) {
variable = it.next();
- if (variable.equals(BYTE_STREAM) && !IBB_ONLY) {
+ if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) {
isByteStream = true;
}
- else if (variable.equals(INBAND_BYTE_STREAM)) {
+ else if (variable.equals(InBandBytestreamManager.NAMESPACE)) {
isIBB = true;
}
}
@@ -438,10 +460,10 @@ public class FileTransferNegotiator {
if (isByteStream && isIBB) {
return new FaultTolerantNegotiator(connection,
- byteStreamTransferManager.createNegotiator(), inbandTransferManager);
+ byteStreamTransferManager, inbandTransferManager);
}
else if (isByteStream) {
- return byteStreamTransferManager.createNegotiator();
+ return byteStreamTransferManager;
}
else {
return inbandTransferManager;
@@ -453,9 +475,9 @@ public class FileTransferNegotiator {
FormField field = new FormField(STREAM_DATA_FIELD_NAME);
field.setType(FormField.TYPE_LIST_MULTI);
if (!IBB_ONLY) {
- field.addOption(new FormField.Option(BYTE_STREAM));
+ field.addOption(new FormField.Option(Socks5BytestreamManager.NAMESPACE));
}
- field.addOption(new FormField.Option(INBAND_BYTE_STREAM));
+ field.addOption(new FormField.Option(InBandBytestreamManager.NAMESPACE));
form.addField(field);
return form;
}
diff --git a/source/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java b/source/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java
index 6669f9ab0..b894d2ddb 100644
--- a/source/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java
+++ b/source/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java
@@ -19,402 +19,107 @@
*/
package org.jivesoftware.smackx.filetransfer;
-import org.jivesoftware.smack.*;
-import org.jivesoftware.smack.util.StringUtils;
-import org.jivesoftware.smack.filter.*;
-import org.jivesoftware.smack.packet.IQ;
-import org.jivesoftware.smack.packet.Message;
-import org.jivesoftware.smack.packet.Packet;
-import org.jivesoftware.smack.packet.XMPPError;
-import org.jivesoftware.smackx.packet.IBBExtensions;
-import org.jivesoftware.smackx.packet.IBBExtensions.Open;
-import org.jivesoftware.smackx.packet.StreamInitiation;
-
-import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.FromContainsFilter;
+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.ibb.InBandBytestreamManager;
+import org.jivesoftware.smackx.ibb.InBandBytestreamRequest;
+import org.jivesoftware.smackx.ibb.InBandBytestreamSession;
+import org.jivesoftware.smackx.ibb.packet.Open;
+import org.jivesoftware.smackx.packet.StreamInitiation;
+
/**
- * The in-band bytestream file transfer method, or IBB for short, transfers the
+ * The In-Band Bytestream file transfer method, or IBB for short, transfers the
* file over the same XML Stream used by XMPP. It is the fall-back mechanism in
- * case the SOCKS5 bytestream method of transfering files is not available.
- *
+ * case the SOCKS5 bytestream method of transferring files is not available.
+ *
* @author Alexander Wenckus
- * @see JEP-0047: In-Band
+ * @author Henning Staib
+ * @see XEP-0047: In-Band
* Bytestreams (IBB)
*/
public class IBBTransferNegotiator extends StreamNegotiator {
- protected static final String NAMESPACE = "http://jabber.org/protocol/ibb";
-
- public static final int DEFAULT_BLOCK_SIZE = 4096;
-
private Connection connection;
+ private InBandBytestreamManager manager;
+
/**
- * The default constructor for the In-Band Bystream Negotiator.
- *
+ * The default constructor for the In-Band Bytestream Negotiator.
+ *
* @param connection The connection which this negotiator works on.
*/
protected IBBTransferNegotiator(Connection connection) {
this.connection = connection;
- }
-
- public PacketFilter getInitiationPacketFilter(String from, String streamID) {
- return new AndFilter(new FromContainsFilter(
- from), new IBBOpenSidFilter(streamID));
- }
-
- InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException {
- Open openRequest = (Open) streamInitiation;
-
- if (openRequest.getType().equals(IQ.Type.ERROR)) {
- throw new XMPPException(openRequest.getError());
- }
-
- PacketFilter dataFilter = new IBBMessageSidFilter(openRequest.getFrom(),
- openRequest.getSessionID());
- PacketFilter closeFilter = new AndFilter(new PacketTypeFilter(
- IBBExtensions.Close.class), new FromMatchesFilter(openRequest
- .getFrom()));
-
- InputStream stream = new IBBInputStream(openRequest.getSessionID(),
- dataFilter, closeFilter);
-
- initInBandTransfer(openRequest);
-
- return stream;
- }
-
- public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException {
- Packet openRequest = initiateIncomingStream(connection, initiation);
- return negotiateIncomingStream(openRequest);
- }
-
- /**
- * Creates and sends the response for the open request.
- *
- * @param openRequest The open request recieved from the peer.
- */
- private void initInBandTransfer(final Open openRequest) {
- connection.sendPacket(FileTransferNegotiator.createIQ(openRequest
- .getPacketID(), openRequest.getFrom(), openRequest.getTo(),
- IQ.Type.RESULT));
+ this.manager = InBandBytestreamManager.getByteStreamManager(connection);
}
public OutputStream createOutgoingStream(String streamID, String initiator,
- String target) throws XMPPException {
- Open openIQ = new Open(streamID, DEFAULT_BLOCK_SIZE);
- openIQ.setTo(target);
- openIQ.setType(IQ.Type.SET);
+ String target) throws XMPPException {
+ InBandBytestreamSession session = this.manager.establishSession(target, streamID);
+ session.setCloseBothStreamsEnabled(true);
+ return session.getOutputStream();
+ }
- // wait for the result from the peer
- PacketCollector collector = connection
- .createPacketCollector(new PacketIDFilter(openIQ.getPacketID()));
- connection.sendPacket(openIQ);
- // We don't want to wait forever for the result
- IQ openResponse = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
- collector.cancel();
+ public InputStream createIncomingStream(StreamInitiation initiation)
+ throws XMPPException {
+ /*
+ * In-Band Bytestream initiation listener must ignore next in-band
+ * bytestream request with given session ID
+ */
+ this.manager.ignoreBytestreamRequestOnce(initiation.getSessionID());
- if (openResponse == null) {
- throw new XMPPException("No response from peer on IBB open");
- }
+ Packet streamInitiation = initiateIncomingStream(this.connection, initiation);
+ return negotiateIncomingStream(streamInitiation);
+ }
- IQ.Type type = openResponse.getType();
- if (!type.equals(IQ.Type.RESULT)) {
- if (type.equals(IQ.Type.ERROR)) {
- throw new XMPPException("Target returned an error",
- openResponse.getError());
- }
- else {
- throw new XMPPException("Target returned unknown response");
- }
- }
+ public PacketFilter getInitiationPacketFilter(String from, String streamID) {
+ /*
+ * this method is always called prior to #negotiateIncomingStream() so
+ * the In-Band Bytestream initiation listener must ignore the next
+ * In-Band Bytestream request with the given session ID
+ */
+ this.manager.ignoreBytestreamRequestOnce(streamID);
- return new IBBOutputStream(target, streamID, DEFAULT_BLOCK_SIZE);
+ return new AndFilter(new FromContainsFilter(from), new IBBOpenSidFilter(streamID));
}
public String[] getNamespaces() {
- return new String[]{NAMESPACE};
+ return new String[] { InBandBytestreamManager.NAMESPACE };
+ }
+
+ InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException {
+ // build In-Band Bytestream request
+ InBandBytestreamRequest request = new ByteStreamRequest(this.manager,
+ (Open) streamInitiation);
+
+ // always accept the request
+ InBandBytestreamSession session = request.accept();
+ session.setCloseBothStreamsEnabled(true);
+ return session.getInputStream();
}
public void cleanup() {
}
- private class IBBOutputStream extends OutputStream {
-
- protected byte[] buffer;
-
- protected int count = 0;
-
- protected int seq = 0;
-
- final String userID;
-
- final private IQ closePacket;
-
- private String messageID;
- private String sid;
-
- IBBOutputStream(String userID, String sid, int blockSize) {
- if (blockSize <= 0) {
- throw new IllegalArgumentException("Buffer size <= 0");
- }
- buffer = new byte[blockSize];
- this.userID = userID;
-
- Message template = new Message(userID);
- messageID = template.getPacketID();
- this.sid = sid;
- closePacket = createClosePacket(userID, sid);
- }
-
- private IQ createClosePacket(String userID, String sid) {
- IQ packet = new IBBExtensions.Close(sid);
- packet.setTo(userID);
- packet.setType(IQ.Type.SET);
- return packet;
- }
-
- public void write(int b) throws IOException {
- if (count >= buffer.length) {
- flushBuffer();
- }
-
- buffer[count++] = (byte) b;
- }
-
- public synchronized void write(byte b[], int off, int len)
- throws IOException {
- if (len >= buffer.length) {
- // "byte" off the first chunck to write out
- writeOut(b, off, buffer.length);
- // recursivly call this method again with the lesser amount subtracted.
- write(b, off + buffer.length, len - buffer.length);
- } else {
- writeOut(b, off, len);
- }
- }
-
- private void writeOut(byte b[], int off, int len) {
- if (len > buffer.length - count) {
- flushBuffer();
- }
- System.arraycopy(b, off, buffer, count, len);
- count += len;
- }
-
- private synchronized void flushBuffer() {
- writeToXML(buffer, 0, count);
-
- count = 0;
- }
-
- private synchronized void writeToXML(byte[] buffer, int offset, int len) {
- Message template = createTemplate(messageID + "_" + seq);
- IBBExtensions.Data ext = new IBBExtensions.Data(sid);
- template.addExtension(ext);
-
- String enc = StringUtils.encodeBase64(buffer, offset, len, false);
-
- ext.setData(enc);
- ext.setSeq(seq);
- synchronized (this) {
- try {
- this.wait(100);
- }
- catch (InterruptedException e) {
- /* Do Nothing */
- }
- }
-
- connection.sendPacket(template);
-
- seq = (seq + 1 == 65535 ? 0 : seq + 1);
- }
-
- public void close() throws IOException {
- this.flush();
- connection.sendPacket(closePacket);
- }
-
- public void flush() throws IOException {
- flushBuffer();
- }
-
- public void write(byte[] b) throws IOException {
- write(b, 0, b.length);
- }
-
- public Message createTemplate(String messageID) {
- Message template = new Message(userID);
- template.setPacketID(messageID);
- return template;
- }
- }
-
- private class IBBInputStream extends InputStream implements PacketListener {
-
- private String streamID;
-
- private PacketCollector dataCollector;
-
- private byte[] buffer;
-
- private int bufferPointer;
-
- private int seq = -1;
-
- private boolean isDone;
-
- private boolean isEOF;
-
- private boolean isClosed;
-
- private IQ closeConfirmation;
-
- private Message lastMess;
-
- private IBBInputStream(String streamID, PacketFilter dataFilter,
- PacketFilter closeFilter) {
- this.streamID = streamID;
- this.dataCollector = connection.createPacketCollector(dataFilter);
- connection.addPacketListener(this, closeFilter);
- this.bufferPointer = -1;
- }
-
- public synchronized int read() throws IOException {
- if (isEOF || isClosed) {
- return -1;
- }
- if (bufferPointer == -1 || bufferPointer >= buffer.length) {
- loadBufferWait();
- }
-
- return (int) buffer[bufferPointer++];
- }
-
- public synchronized int read(byte[] b) throws IOException {
- return read(b, 0, b.length);
- }
-
- public synchronized int read(byte[] b, int off, int len)
- throws IOException {
- if (isEOF || isClosed) {
- return -1;
- }
- if (bufferPointer == -1 || bufferPointer >= buffer.length) {
- if (!loadBufferWait()) {
- isEOF = true;
- return -1;
- }
- }
-
- if (len - off > buffer.length - bufferPointer) {
- len = buffer.length - bufferPointer;
- }
-
- System.arraycopy(buffer, bufferPointer, b, off, len);
- bufferPointer += len;
- return len;
- }
-
- private boolean loadBufferWait() throws IOException {
- IBBExtensions.Data data;
-
- Message mess = null;
- while (mess == null) {
- if (isDone) {
- mess = (Message) dataCollector.pollResult();
- if (mess == null) {
- return false;
- }
- }
- else {
- mess = (Message) dataCollector.nextResult(1000);
- }
- }
- lastMess = mess;
- data = (IBBExtensions.Data) mess.getExtension(
- IBBExtensions.Data.ELEMENT_NAME,
- IBBExtensions.NAMESPACE);
-
- checkSequence(mess, (int) data.getSeq());
- buffer = StringUtils.decodeBase64(data.getData());
- bufferPointer = 0;
- return true;
- }
-
- private void checkSequence(Message mess, int seq) throws IOException {
- if (this.seq == 65535) {
- this.seq = -1;
- }
- if (seq - 1 != this.seq) {
- cancelTransfer(mess);
- throw new IOException("Packets out of sequence");
- }
- else {
- this.seq = seq;
- }
- }
-
- private void cancelTransfer(Message mess) {
- cleanup();
-
- sendCancelMessage(mess);
- }
-
- private void cleanup() {
- dataCollector.cancel();
- connection.removePacketListener(this);
- }
-
- private void sendCancelMessage(Message message) {
- IQ error = FileTransferNegotiator.createIQ(message.getPacketID(), message.getFrom(), message.getTo(),
- IQ.Type.ERROR);
- error.setError(new XMPPError(XMPPError.Condition.remote_server_timeout, "Cancel Message Transfer"));
- connection.sendPacket(error);
- }
-
- public boolean markSupported() {
- return false;
- }
-
- public void processPacket(Packet packet) {
- IBBExtensions.Close close = (IBBExtensions.Close) packet;
- if (close.getSessionID().equals(streamID)) {
- isDone = true;
- closeConfirmation = FileTransferNegotiator.createIQ(packet
- .getPacketID(), packet.getFrom(), packet.getTo(),
- IQ.Type.RESULT);
- }
- }
-
- public synchronized void close() throws IOException {
- if (isClosed) {
- return;
- }
- cleanup();
-
- if (isEOF) {
- sendCloseConfirmation();
- }
- else if (lastMess != null) {
- sendCancelMessage(lastMess);
- }
- isClosed = true;
- }
-
- private void sendCloseConfirmation() {
- connection.sendPacket(closeConfirmation);
- }
- }
-
- private static class IBBOpenSidFilter implements PacketFilter {
+ /**
+ * This PacketFilter accepts an incoming In-Band Bytestream open request
+ * with a specified session ID.
+ */
+ private static class IBBOpenSidFilter extends PacketTypeFilter {
private String sessionID;
public IBBOpenSidFilter(String sessionID) {
+ super(Open.class);
if (sessionID == null) {
throw new IllegalArgumentException("StreamID cannot be null");
}
@@ -422,39 +127,26 @@ public class IBBTransferNegotiator extends StreamNegotiator {
}
public boolean accept(Packet packet) {
- if (!IBBExtensions.Open.class.isInstance(packet)) {
- return false;
- }
- IBBExtensions.Open open = (IBBExtensions.Open) packet;
- String sessionID = open.getSessionID();
+ if (super.accept(packet)) {
+ Open bytestream = (Open) packet;
- return (sessionID != null && sessionID.equals(this.sessionID));
+ // packet must by of type SET and contains the given session ID
+ return this.sessionID.equals(bytestream.getSessionID())
+ && IQ.Type.SET.equals(bytestream.getType());
+ }
+ return false;
}
}
- private static class IBBMessageSidFilter implements PacketFilter {
+ /**
+ * Derive from InBandBytestreamRequest to access protected constructor.
+ */
+ private static class ByteStreamRequest extends InBandBytestreamRequest {
- private final String sessionID;
- private String from;
-
- public IBBMessageSidFilter(String from, String sessionID) {
- this.from = from;
- this.sessionID = sessionID;
+ private ByteStreamRequest(InBandBytestreamManager manager, Open byteStreamRequest) {
+ super(manager, byteStreamRequest);
}
- public boolean accept(Packet packet) {
- if (!(packet instanceof Message)) {
- return false;
- }
- if (!packet.getFrom().equalsIgnoreCase(from)) {
- return false;
- }
-
- IBBExtensions.Data data = (IBBExtensions.Data) packet.
- getExtension(IBBExtensions.Data.ELEMENT_NAME, IBBExtensions.NAMESPACE);
- return data != null && data.getSessionID() != null
- && data.getSessionID().equalsIgnoreCase(sessionID);
- }
}
}
diff --git a/source/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java b/source/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java
index 624aa537c..2cf0a8d2d 100644
--- a/source/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java
+++ b/source/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java
@@ -1,10 +1,4 @@
/**
- * $RCSfile$
- * $Revision: $
- * $Date: $
- *
- * Copyright 2003-2006 Jive Software.
- *
* 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
@@ -19,119 +13,100 @@
*/
package org.jivesoftware.smackx.filetransfer;
-import org.jivesoftware.smack.PacketCollector;
-import org.jivesoftware.smack.SmackConfiguration;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PushbackInputStream;
+
import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.FromMatchesFilter;
import org.jivesoftware.smack.filter.PacketFilter;
-import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
-import org.jivesoftware.smack.packet.XMPPError;
-import org.jivesoftware.smack.util.StringUtils;
-import org.jivesoftware.smackx.packet.Bytestream;
-import org.jivesoftware.smackx.packet.Bytestream.StreamHost;
-import org.jivesoftware.smackx.packet.Bytestream.StreamHostUsed;
import org.jivesoftware.smackx.packet.StreamInitiation;
-
-import java.io.*;
-import java.net.InetAddress;
-import java.net.Socket;
-import java.net.UnknownHostException;
-import java.util.Collection;
-import java.util.Iterator;
+import org.jivesoftware.smackx.socks5bytestream.Socks5BytestreamManager;
+import org.jivesoftware.smackx.socks5bytestream.Socks5BytestreamRequest;
+import org.jivesoftware.smackx.socks5bytestream.Socks5BytestreamSession;
+import org.jivesoftware.smackx.socks5bytestream.packet.Bytestream;
/**
- * A SOCKS5 bytestream is negotiated partly over the XMPP XML stream and partly
- * over a seperate socket. The actual transfer though takes place over a
- * seperatly created socket.
- *
- * A SOCKS5 file transfer generally has three parites, the initiator, the
- * target, and the stream host. The stream host is a specialized SOCKS5 proxy
- * setup on the server, or, the Initiator can act as the Stream Host if the
- * proxy is not available.
- *
- * The advantage of having a seperate proxy over directly connecting to
- * eachother is if the Initator and the Target are not on the same LAN and are
- * operating behind NAT, the proxy allows for a common location for both parties
- * to connect to and transfer the file.
- *
- * Smack will attempt to automatically discover any proxies present on your
- * server. If any are detected they will be forwarded to any user attempting to
- * recieve files from you.
- *
- * @author Alexander Wenckus
- * @see JEP-0065: SOCKS5
- * Bytestreams
+ * Negotiates a SOCKS5 Bytestream to be used for file transfers. The implementation is based on the
+ * {@link Socks5BytestreamManager} and the {@link Socks5BytestreamRequest}.
+ *
+ * @author Henning Staib
+ * @see XEP-0065: SOCKS5 Bytestreams
*/
public class Socks5TransferNegotiator extends StreamNegotiator {
- protected static final String NAMESPACE = "http://jabber.org/protocol/bytestreams";
+ private Connection connection;
- /**
- * The number of connection failures it takes to a streamhost for that particular streamhost
- * to be blacklisted. When a host is blacklisted no more connection attempts will be made to
- * it for a period of 2 hours.
- */
- private static final int CONNECT_FAILURE_THRESHOLD = 2;
+ private Socks5BytestreamManager manager;
- public static boolean isAllowLocalProxyHost = true;
-
- private final Connection connection;
-
- private Socks5TransferNegotiatorManager transferNegotiatorManager;
-
- public Socks5TransferNegotiator(Socks5TransferNegotiatorManager transferNegotiatorManager,
- final Connection connection)
- {
+ Socks5TransferNegotiator(Connection connection) {
this.connection = connection;
- this.transferNegotiatorManager = transferNegotiatorManager;
+ this.manager = Socks5BytestreamManager.getBytestreamManager(this.connection);
}
- public PacketFilter getInitiationPacketFilter(String from, String sessionID) {
- return new AndFilter(new FromMatchesFilter(from),
- new BytestreamSIDFilter(sessionID));
+ @Override
+ public OutputStream createOutgoingStream(String streamID, String initiator, String target)
+ throws XMPPException {
+ try {
+ return this.manager.establishSession(target, streamID).getOutputStream();
+ }
+ catch (IOException e) {
+ throw new XMPPException("error establishing SOCKS5 Bytestream", e);
+ }
+ catch (InterruptedException e) {
+ throw new XMPPException("error establishing SOCKS5 Bytestream", e);
+ }
}
- /*
- * (non-Javadoc)
- *
- * @see org.jivesoftware.smackx.filetransfer.StreamNegotiator#initiateDownload(
- * org.jivesoftware.smackx.packet.StreamInitiation, java.io.File)
- */
- InputStream negotiateIncomingStream(Packet streamInitiation)
- throws XMPPException {
- Bytestream streamHostsInfo = (Bytestream) streamInitiation;
+ @Override
+ public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException,
+ InterruptedException {
+ /*
+ * SOCKS5 initiation listener must ignore next SOCKS5 Bytestream request with given session
+ * ID
+ */
+ this.manager.ignoreBytestreamRequestOnce(initiation.getSessionID());
- if (streamHostsInfo.getType().equals(IQ.Type.ERROR)) {
- throw new XMPPException(streamHostsInfo.getError());
- }
- SelectedHostInfo selectedHost;
+ Packet streamInitiation = initiateIncomingStream(this.connection, initiation);
+ return negotiateIncomingStream(streamInitiation);
+ }
+
+ @Override
+ public PacketFilter getInitiationPacketFilter(final String from, String streamID) {
+ /*
+ * this method is always called prior to #negotiateIncomingStream() so the SOCKS5
+ * InitiationListener must ignore the next SOCKS5 Bytestream request with the given session
+ * ID
+ */
+ this.manager.ignoreBytestreamRequestOnce(streamID);
+
+ return new AndFilter(new FromMatchesFilter(from), new BytestreamSIDFilter(streamID));
+ }
+
+ @Override
+ public String[] getNamespaces() {
+ return new String[] { Socks5BytestreamManager.NAMESPACE };
+ }
+
+ @Override
+ InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException,
+ InterruptedException {
+ // build SOCKS5 Bytestream request
+ Socks5BytestreamRequest request = new ByteStreamRequest(this.manager,
+ (Bytestream) streamInitiation);
+
+ // always accept the request
+ Socks5BytestreamSession session = request.accept();
+
+ // test input stream
try {
- // select appropriate host
- selectedHost = selectHost(streamHostsInfo);
- }
- catch (XMPPException ex) {
- if (ex.getXMPPError() != null) {
- IQ errorPacket = super.createError(streamHostsInfo.getTo(),
- streamHostsInfo.getFrom(), streamHostsInfo.getPacketID(),
- ex.getXMPPError());
- connection.sendPacket(errorPacket);
- }
- throw (ex);
- }
-
- // send used-host confirmation
- Bytestream streamResponse = createUsedHostConfirmation(
- selectedHost.selectedHost, streamHostsInfo.getFrom(),
- streamHostsInfo.getTo(), streamHostsInfo.getPacketID());
- connection.sendPacket(streamResponse);
-
- try {
- PushbackInputStream stream = new PushbackInputStream(
- selectedHost.establishedSocket.getInputStream());
+ PushbackInputStream stream = new PushbackInputStream(session.getInputStream());
int firstByte = stream.read();
stream.unread(firstByte);
return stream;
@@ -139,435 +114,51 @@ public class Socks5TransferNegotiator extends StreamNegotiator {
catch (IOException e) {
throw new XMPPException("Error establishing input stream", e);
}
-
- }
-
- public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException {
- Packet streamInitiation = initiateIncomingStream(connection, initiation);
- return negotiateIncomingStream(streamInitiation);
- }
-
- /**
- * The used host confirmation is sent to the initiator to indicate to them
- * which of the hosts they provided has been selected and successfully
- * connected to.
- *
- * @param selectedHost The selected stream host.
- * @param initiator The initiator of the stream.
- * @param target The target of the stream.
- * @param packetID The of the packet being responded to.
- * @return The packet that was created to send to the initiator.
- */
- private Bytestream createUsedHostConfirmation(StreamHost selectedHost,
- String initiator, String target, String packetID) {
- Bytestream streamResponse = new Bytestream();
- streamResponse.setTo(initiator);
- streamResponse.setFrom(target);
- streamResponse.setType(IQ.Type.RESULT);
- streamResponse.setPacketID(packetID);
- streamResponse.setUsedHost(selectedHost.getJID());
- return streamResponse;
- }
-
- /**
- * Selects a host to connect to over which the file will be transmitted.
- *
- * @param streamHostsInfo the packet recieved from the initiator containing the available hosts
- * to transfer the file
- * @return the selected host and socket that were created.
- * @throws XMPPException when there is no appropriate host.
- */
- private SelectedHostInfo selectHost(Bytestream streamHostsInfo)
- throws XMPPException {
- Iterator it = streamHostsInfo.getStreamHosts().iterator();
- StreamHost selectedHost = null;
- Socket socket = null;
- while (it.hasNext()) {
- selectedHost = (StreamHost) it.next();
- String address = selectedHost.getAddress();
-
- // Check to see if this address has been blacklisted
- int failures = getConnectionFailures(address);
- if (failures >= CONNECT_FAILURE_THRESHOLD) {
- continue;
- }
- // establish socket
- try {
- socket = new Socket(address, selectedHost
- .getPort());
- establishSOCKS5ConnectionToProxy(socket, createDigest(
- streamHostsInfo.getSessionID(), streamHostsInfo
- .getFrom(), streamHostsInfo.getTo()));
- break;
- }
- catch (IOException e) {
- e.printStackTrace();
- incrementConnectionFailures(address);
- selectedHost = null;
- socket = null;
- }
- }
- if (selectedHost == null || socket == null || !socket.isConnected()) {
- String errorMessage = "Could not establish socket with any provided host";
- throw new XMPPException(errorMessage, new XMPPError(
- XMPPError.Condition.no_acceptable, errorMessage));
- }
-
- return new SelectedHostInfo(selectedHost, socket);
- }
-
- private void incrementConnectionFailures(String address) {
- transferNegotiatorManager.incrementConnectionFailures(address);
- }
-
- private int getConnectionFailures(String address) {
- return transferNegotiatorManager.getConnectionFailures(address);
- }
-
- /**
- * Creates the digest needed for a byte stream. It is the SHA1(sessionID +
- * initiator + target).
- *
- * @param sessionID The sessionID of the stream negotiation
- * @param initiator The inititator of the stream negotiation
- * @param target The target of the stream negotiation
- * @return SHA-1 hash of the three parameters
- */
- private String createDigest(final String sessionID, final String initiator,
- final String target) {
- return StringUtils.hash(sessionID + StringUtils.parseName(initiator)
- + "@" + StringUtils.parseServer(initiator) + "/"
- + StringUtils.parseResource(initiator)
- + StringUtils.parseName(target) + "@"
- + StringUtils.parseServer(target) + "/"
- + StringUtils.parseResource(target));
- }
-
- /*
- * (non-Javadoc)
- *
- * @see org.jivesoftware.smackx.filetransfer.StreamNegotiator#initiateUpload(java.lang.String,
- * org.jivesoftware.smackx.packet.StreamInitiation, java.io.File)
- */
- public OutputStream createOutgoingStream(String streamID, String initiator,
- String target) throws XMPPException
- {
- Socket socket;
- try {
- socket = initBytestreamSocket(streamID, initiator, target);
- }
- catch (Exception e) {
- throw new XMPPException("Error establishing transfer socket", e);
- }
-
- if (socket != null) {
- try {
- return new BufferedOutputStream(socket.getOutputStream());
- }
- catch (IOException e) {
- throw new XMPPException("Error establishing output stream", e);
- }
- }
- return null;
- }
-
- private Socket initBytestreamSocket(final String sessionID,
- String initiator, String target) throws Exception {
- Socks5TransferNegotiatorManager.ProxyProcess process;
- try {
- process = establishListeningSocket();
- }
- catch (IOException io) {
- process = null;
- }
-
- Socket conn;
- try {
- String localIP;
- try {
- localIP = discoverLocalIP();
- }
- catch (UnknownHostException e1) {
- localIP = null;
- }
-
- Bytestream query = createByteStreamInit(initiator, target, sessionID,
- localIP, (process != null ? process.getPort() : 0));
-
- // if the local host is one of the options we need to wait for the
- // remote connection.
- conn = waitForUsedHostResponse(sessionID, process, createDigest(
- sessionID, initiator, target), query).establishedSocket;
- }
- finally {
- cleanupListeningSocket();
- }
-
- return conn;
- }
-
-
- /**
- * Waits for the peer to respond with which host they chose to use.
- *
- * @param sessionID The session id of the stream.
- * @param proxy The server socket which will listen locally for remote
- * connections.
- * @param digest the digest of the userids and the session id
- * @param query the query which the response is being awaited
- * @return the selected host
- * @throws XMPPException when the response from the peer is an error or doesn't occur
- * @throws IOException when there is an error establishing the local socket
- */
- private SelectedHostInfo waitForUsedHostResponse(String sessionID,
- final Socks5TransferNegotiatorManager.ProxyProcess proxy, final String digest,
- final Bytestream query) throws XMPPException, IOException
- {
- SelectedHostInfo info = new SelectedHostInfo();
-
- PacketCollector collector = connection
- .createPacketCollector(new PacketIDFilter(query.getPacketID()));
- connection.sendPacket(query);
-
- Packet packet = collector.nextResult(10000);
- collector.cancel();
- Bytestream response;
- if (packet != null && packet instanceof Bytestream) {
- response = (Bytestream) packet;
- }
- else {
- throw new XMPPException("Unexpected response from remote user");
- }
-
- // check for an error
- if (response.getType().equals(IQ.Type.ERROR)) {
- throw new XMPPException("Remote client returned error, stream hosts expected",
- response.getError());
- }
-
- StreamHostUsed used = response.getUsedHost();
- StreamHost usedHost = query.getStreamHost(used.getJID());
- if (usedHost == null) {
- throw new XMPPException("Remote user responded with unknown host");
- }
- // The local computer is acting as the proxy
- if (used.getJID().equals(query.getFrom())) {
- info.establishedSocket = proxy.getSocket(digest);
- info.selectedHost = usedHost;
- return info;
- }
- else {
- info.establishedSocket = new Socket(usedHost.getAddress(), usedHost
- .getPort());
- establishSOCKS5ConnectionToProxy(info.establishedSocket, digest);
-
- Bytestream activate = createByteStreamActivate(sessionID, response
- .getTo(), usedHost.getJID(), response.getFrom());
-
- collector = connection.createPacketCollector(new PacketIDFilter(
- activate.getPacketID()));
- connection.sendPacket(activate);
-
- IQ serverResponse = (IQ) collector.nextResult(SmackConfiguration
- .getPacketReplyTimeout());
- collector.cancel();
- if (!serverResponse.getType().equals(IQ.Type.RESULT)) {
- info.establishedSocket.close();
- return null;
- }
- return info;
- }
- }
-
- private Socks5TransferNegotiatorManager.ProxyProcess establishListeningSocket()
- throws IOException {
- return transferNegotiatorManager.addTransfer();
- }
-
- private void cleanupListeningSocket() {
- transferNegotiatorManager.removeTransfer();
- }
-
- private String discoverLocalIP() throws UnknownHostException {
- return InetAddress.getLocalHost().getHostAddress();
- }
-
- /**
- * The bytestream init looks like this:
- *
- *
- *
- * @param from initiator@host1/foo - the file transfer initiator.
- * @param to target@host2/bar - the file transfer target.
- * @param sid 'mySID' - the unique identifier for this file transfer
- * @param localIP the IP of the local machine if it is being provided, null otherwise.
- * @param port the port of the local mahine if it is being provided, null otherwise.
- * @return the created Bytestream packet
- */
- private Bytestream createByteStreamInit(final String from, final String to,
- final String sid, final String localIP, final int port)
- {
- Bytestream bs = new Bytestream();
- bs.setTo(to);
- bs.setFrom(from);
- bs.setSessionID(sid);
- bs.setType(IQ.Type.SET);
- bs.setMode(Bytestream.Mode.tcp);
- if (localIP != null && port > 0) {
- bs.addStreamHost(from, localIP, port);
- }
- // make sure the proxies have been initialized completely
- Collection streamHosts = transferNegotiatorManager.getStreamHosts();
-
- if (streamHosts != null) {
- for (StreamHost host : streamHosts) {
- bs.addStreamHost(host);
- }
- }
-
- return bs;
- }
-
-
- /**
- * Returns the packet to send notification to the stream host to activate
- * the stream.
- *
- * @param sessionID the session ID of the file transfer to activate.
- * @param from the sender of the bytestreeam
- * @param to the JID of the stream host
- * @param target the JID of the file transfer target.
- * @return the packet to send notification to the stream host to
- * activate the stream.
- */
- private static Bytestream createByteStreamActivate(final String sessionID,
- final String from, final String to, final String target)
- {
- Bytestream activate = new Bytestream(sessionID);
- activate.setMode(null);
- activate.setToActivate(target);
- activate.setFrom(from);
- activate.setTo(to);
- activate.setType(IQ.Type.SET);
- return activate;
- }
-
- public String[] getNamespaces() {
- return new String[]{NAMESPACE};
- }
-
- private void establishSOCKS5ConnectionToProxy(Socket socket, String digest)
- throws IOException {
-
- byte[] cmd = new byte[3];
-
- cmd[0] = (byte) 0x05;
- cmd[1] = (byte) 0x01;
- cmd[2] = (byte) 0x00;
-
- OutputStream out = new DataOutputStream(socket.getOutputStream());
- out.write(cmd);
-
- InputStream in = new DataInputStream(socket.getInputStream());
- byte[] response = new byte[2];
-
- in.read(response);
-
- cmd = createOutgoingSocks5Message(1, digest);
- out.write(cmd);
- createIncomingSocks5Message(in);
- }
-
- static String createIncomingSocks5Message(InputStream in)
- throws IOException {
- byte[] cmd = new byte[5];
- in.read(cmd, 0, 5);
-
- byte[] addr = new byte[cmd[4]];
- in.read(addr, 0, addr.length);
- String digest = new String(addr);
- in.read();
- in.read();
-
- return digest;
- }
-
- static byte[] createOutgoingSocks5Message(int cmd, String digest) {
- byte addr[] = digest.getBytes();
-
- byte[] data = new byte[7 + addr.length];
- data[0] = (byte) 5;
- data[1] = (byte) cmd;
- data[2] = (byte) 0;
- data[3] = (byte) 0x3;
- data[4] = (byte) addr.length;
-
- System.arraycopy(addr, 0, data, 5, addr.length);
- data[data.length - 2] = (byte) 0;
- data[data.length - 1] = (byte) 0;
-
- return data;
}
+ @Override
public void cleanup() {
-
+ /* do nothing */
}
- private static class SelectedHostInfo {
-
- protected XMPPException exception;
-
- protected StreamHost selectedHost;
-
- protected Socket establishedSocket;
-
- SelectedHostInfo(StreamHost selectedHost, Socket establishedSocket) {
- this.selectedHost = selectedHost;
- this.establishedSocket = establishedSocket;
- }
-
- public SelectedHostInfo() {
- }
- }
-
-
- private static class BytestreamSIDFilter implements PacketFilter {
+ /**
+ * This PacketFilter accepts an incoming SOCKS5 Bytestream request with a specified session ID.
+ */
+ private static class BytestreamSIDFilter extends PacketTypeFilter {
private String sessionID;
public BytestreamSIDFilter(String sessionID) {
+ super(Bytestream.class);
if (sessionID == null) {
throw new IllegalArgumentException("StreamID cannot be null");
}
this.sessionID = sessionID;
}
+ @Override
public boolean accept(Packet packet) {
- if (!Bytestream.class.isInstance(packet)) {
- return false;
- }
- Bytestream bytestream = (Bytestream) packet;
- String sessionID = bytestream.getSessionID();
+ if (super.accept(packet)) {
+ Bytestream bytestream = (Bytestream) packet;
- return (sessionID != null && sessionID.equals(this.sessionID));
+ // packet must by of type SET and contains the given session ID
+ return this.sessionID.equals(bytestream.getSessionID())
+ && IQ.Type.SET.equals(bytestream.getType());
+ }
+ return false;
}
+
}
+
+ /**
+ * Derive from Socks5BytestreamRequest to access protected constructor.
+ */
+ private static class ByteStreamRequest extends Socks5BytestreamRequest {
+
+ private ByteStreamRequest(Socks5BytestreamManager manager, Bytestream byteStreamRequest) {
+ super(manager, byteStreamRequest);
+ }
+
+ }
+
}
diff --git a/source/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiatorManager.java b/source/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiatorManager.java
deleted file mode 100644
index 730c79312..000000000
--- a/source/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiatorManager.java
+++ /dev/null
@@ -1,388 +0,0 @@
-/**
- * $Revision:$
- * $Date:$
- *
- * Copyright 2003-2007 Jive Software.
- *
- * 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.filetransfer;
-
-import org.jivesoftware.smack.util.Cache;
-import org.jivesoftware.smack.XMPPException;
-import org.jivesoftware.smack.PacketCollector;
-import org.jivesoftware.smack.SmackConfiguration;
-import org.jivesoftware.smack.Connection;
-import org.jivesoftware.smack.filter.PacketIDFilter;
-import org.jivesoftware.smack.packet.IQ;
-import org.jivesoftware.smackx.ServiceDiscoveryManager;
-import org.jivesoftware.smackx.packet.DiscoverItems;
-import org.jivesoftware.smackx.packet.Bytestream;
-import org.jivesoftware.smackx.packet.DiscoverInfo;
-
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.net.SocketException;
-import java.net.SocketTimeoutException;
-import java.util.*;
-import java.io.*;
-
-/**
- *
- */
-public class Socks5TransferNegotiatorManager implements FileTransferNegotiatorManager {
-
- private static final long BLACKLIST_LIFETIME = 60 * 1000 * 120;
- // locks the proxies during their initialization process
- private final Object proxyLock = new Object();
-
- private static ProxyProcess proxyProcess;
-
- // locks on the proxy process during its initiatilization process
- private final Object processLock = new Object();
-
- private final Cache addressBlacklist
- = new Cache(100, BLACKLIST_LIFETIME);
-
- private Connection connection;
-
- private List proxies;
-
- private List streamHosts;
-
- public Socks5TransferNegotiatorManager(Connection connection) {
- this.connection = connection;
- }
-
- public StreamNegotiator createNegotiator() {
- return new Socks5TransferNegotiator(this, connection);
- }
-
- public void incrementConnectionFailures(String address) {
- Integer count = addressBlacklist.get(address);
- if (count == null) {
- count = 1;
- }
- else {
- count += 1;
- }
- addressBlacklist.put(address, count);
- }
-
- public int getConnectionFailures(String address) {
- Integer count = addressBlacklist.get(address);
- return count != null ? count : 0;
- }
-
- public ProxyProcess addTransfer() throws IOException {
- synchronized (processLock) {
- if (proxyProcess == null) {
- proxyProcess = new ProxyProcess(new ServerSocket(7777));
- proxyProcess.start();
- }
- }
- proxyProcess.addTransfer();
- return proxyProcess;
- }
-
- public void removeTransfer() {
- if (proxyProcess == null) {
- return;
- }
- proxyProcess.removeTransfer();
- }
-
- public Collection getStreamHosts() {
- synchronized (proxyLock) {
- if (proxies == null) {
- initProxies();
- }
- }
- return Collections.unmodifiableCollection(streamHosts);
- }
-
- /**
- * Checks the service discovery item returned from a server component to verify if it is
- * a File Transfer proxy or not.
- *
- * @param manager the service discovery manager which will be used to query the component
- * @param item the discovered item on the server relating
- * @return returns the JID of the proxy if it is a proxy or null if the item is not a proxy.
- */
- private String checkIsProxy(ServiceDiscoveryManager manager, DiscoverItems.Item item) {
- DiscoverInfo info;
- try {
- info = manager.discoverInfo(item.getEntityID());
- }
- catch (XMPPException e) {
- return null;
- }
- Iterator itx = info.getIdentities();
- while (itx.hasNext()) {
- DiscoverInfo.Identity identity = itx.next();
- if ("proxy".equalsIgnoreCase(identity.getCategory())
- && "bytestreams".equalsIgnoreCase(
- identity.getType())) {
- return info.getFrom();
- }
- }
- return null;
- }
-
- private void initProxies() {
- proxies = new ArrayList();
- ServiceDiscoveryManager manager = ServiceDiscoveryManager
- .getInstanceFor(connection);
- try {
- DiscoverItems discoItems = manager.discoverItems(connection.getServiceName());
- Iterator it = discoItems.getItems();
- while (it.hasNext()) {
- DiscoverItems.Item item = it.next();
- String proxy = checkIsProxy(manager, item);
- if (proxy != null) {
- proxies.add(proxy);
- }
- }
- }
- catch (XMPPException e) {
- return;
- }
- if (proxies.size() > 0) {
- initStreamHosts();
- }
- }
-
- /**
- * Loads streamhost address and ports from the proxies on the local server.
- */
- private void initStreamHosts() {
- List streamHosts = new ArrayList();
- Iterator it = proxies.iterator();
- IQ query;
- PacketCollector collector;
- Bytestream response;
- while (it.hasNext()) {
- String jid = it.next();
- query = new IQ() {
- public String getChildElementXML() {
- return "";
- }
- };
- query.setType(IQ.Type.GET);
- query.setTo(jid);
-
- collector = connection.createPacketCollector(new PacketIDFilter(
- query.getPacketID()));
- connection.sendPacket(query);
-
- response = (Bytestream) collector.nextResult(SmackConfiguration
- .getPacketReplyTimeout());
- if (response != null) {
- streamHosts.addAll(response.getStreamHosts());
- }
- collector.cancel();
- }
- this.streamHosts = streamHosts;
- }
-
- public void cleanup() {
- synchronized (processLock) {
- if (proxyProcess != null) {
- proxyProcess.stop();
- proxyProcess = null;
- }
- }
- }
-
- class ProxyProcess implements Runnable {
-
- private final ServerSocket listeningSocket;
-
- private final Map connectionMap = new HashMap();
-
- private boolean done = false;
-
- private Thread thread;
- private int transfers;
-
- public void run() {
- try {
- try {
- listeningSocket.setSoTimeout(10000);
- }
- catch (SocketException e) {
- // There was a TCP error, lets print the stack trace
- e.printStackTrace();
- return;
- }
- while (!done) {
- Socket conn = null;
- synchronized (ProxyProcess.this) {
- while (transfers <= 0 && !done) {
- transfers = -1;
- try {
- ProxyProcess.this.wait();
- }
- catch (InterruptedException e) {
- /* Do nothing */
- }
- }
- }
- if (done) {
- break;
- }
- try {
- synchronized (listeningSocket) {
- conn = listeningSocket.accept();
- }
- if (conn == null) {
- continue;
- }
- String digest = establishSocks5UploadConnection(conn);
- synchronized (connectionMap) {
- connectionMap.put(digest, conn);
- }
- }
- catch (SocketTimeoutException e) {
- /* Do Nothing */
- }
- catch (IOException e) {
- /* Do Nothing */
- }
- catch (XMPPException e) {
- e.printStackTrace();
- if (conn != null) {
- try {
- conn.close();
- }
- catch (IOException e1) {
- /* Do Nothing */
- }
- }
- }
- }
- }
- finally {
- try {
- listeningSocket.close();
- }
- catch (IOException e) {
- /* Do Nothing */
- }
- }
- }
-
- /**
- * Negotiates the Socks 5 bytestream when the local computer is acting as
- * the proxy.
- *
- * @param connection the socket connection with the peer.
- * @return the SHA-1 digest that is used to uniquely identify the file
- * transfer.
- * @throws XMPPException
- * @throws IOException
- */
- private String establishSocks5UploadConnection(Socket connection) throws XMPPException, IOException {
- OutputStream out = new DataOutputStream(connection.getOutputStream());
- InputStream in = new DataInputStream(connection.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();
- int[] auth = new int[b];
- for (int i = 0; i < b; i++) {
- auth[i] = in.read();
- }
-
- int authMethod = -1;
- for (int anAuth : auth) {
- authMethod = (anAuth == 0 ? 0 : -1); // only auth method
- // 0, no
- // authentication,
- // supported
- if (authMethod == 0) {
- break;
- }
- }
- if (authMethod != 0) {
- throw new XMPPException("Authentication method not supported");
- }
- byte[] cmd = new byte[2];
- cmd[0] = (byte) 0x05;
- cmd[1] = (byte) 0x00;
- out.write(cmd);
-
- String responseDigest = Socks5TransferNegotiator.createIncomingSocks5Message(in);
- cmd = Socks5TransferNegotiator.createOutgoingSocks5Message(0, responseDigest);
-
- if (!connection.isConnected()) {
- throw new XMPPException("Socket closed by remote user");
- }
- out.write(cmd);
- return responseDigest;
- }
-
-
- public void start() {
- thread.start();
- }
-
- public void stop() {
- done = true;
- synchronized (this) {
- this.notify();
- }
- synchronized (listeningSocket) {
- listeningSocket.notify();
- }
- }
-
- public int getPort() {
- return listeningSocket.getLocalPort();
- }
-
- ProxyProcess(ServerSocket listeningSocket) {
- thread = new Thread(this, "File Transfer Connection Listener");
- this.listeningSocket = listeningSocket;
- }
-
- public Socket getSocket(String digest) {
- synchronized (connectionMap) {
- return connectionMap.get(digest);
- }
- }
-
- public void addTransfer() {
- synchronized (this) {
- if (transfers == -1) {
- transfers = 1;
- this.notify();
- }
- else {
- transfers++;
- }
- }
- }
-
- public void removeTransfer() {
- synchronized (this) {
- transfers--;
- }
- }
- }
-}
diff --git a/source/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java b/source/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java
index c18f207a0..46ece9c35 100644
--- a/source/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java
+++ b/source/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java
@@ -37,7 +37,7 @@ import java.io.OutputStream;
/**
* After the file transfer negotiation process is completed according to
- * JEP-0096, the negotation process is passed off to a particular stream
+ * JEP-0096, the negotiation process is passed off to a particular stream
* negotiator. The stream negotiator will then negotiate the chosen stream and
* return the stream to transfer the file.
*
@@ -49,9 +49,9 @@ public abstract class StreamNegotiator {
* Creates the initiation acceptance packet to forward to the stream
* initiator.
*
- * @param streamInitiationOffer The offer from the stream initatior to connect for a stream.
+ * @param streamInitiationOffer The offer from the stream initiator to connect for a stream.
* @param namespaces The namespace that relates to the accepted means of transfer.
- * @return The response to be forwarded to the initator.
+ * @return The response to be forwarded to the initiator.
*/
public StreamInitiation createInitiationAccept(
StreamInitiation streamInitiationOffer, String[] namespaces)
@@ -104,7 +104,7 @@ public abstract class StreamNegotiator {
* Returns the packet filter that will return the initiation packet for the appropriate stream
* initiation.
*
- * @param from The initiatior of the file transfer.
+ * @param from The initiator of the file transfer.
* @param streamID The stream ID related to the transfer.
* @return The PacketFilter that will return the packet relatable to the stream
* initiation.
@@ -112,23 +112,26 @@ public abstract class StreamNegotiator {
public abstract PacketFilter getInitiationPacketFilter(String from, String streamID);
- abstract InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException;
+ abstract InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException,
+ InterruptedException;
/**
* This method handles the file stream download negotiation process. The
* appropriate stream negotiator's initiate incoming stream is called after
* an appropriate file transfer method is selected. The manager will respond
- * to the initatior with the selected means of transfer, then it will handle
- * any negotation specific to the particular transfer method. This method
+ * to the initiator with the selected means of transfer, then it will handle
+ * any negotiation specific to the particular transfer method. This method
* returns the InputStream, ready to transfer the file.
*
- * @param initiation The initation that triggered this download.
- * @return After the negotation process is complete, the InputStream to
+ * @param initiation The initiation that triggered this download.
+ * @return After the negotiation process is complete, the InputStream to
* write a file to is returned.
* @throws XMPPException If an error occurs during this process an XMPPException is
* thrown.
+ * @throws InterruptedException If thread is interrupted.
*/
- public abstract InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException;
+ public abstract InputStream createIncomingStream(StreamInitiation initiation)
+ throws XMPPException, InterruptedException;
/**
* This method handles the file upload stream negotiation process. The
@@ -138,7 +141,7 @@ public abstract class StreamNegotiator {
*
* @param streamID The streamID that uniquely identifies the file transfer.
* @param initiator The fully-qualified JID of the initiator of the file transfer.
- * @param target The fully-qualified JID of the target or reciever of the file
+ * @param target The fully-qualified JID of the target or receiver of the file
* transfer.
* @return The negotiated stream ready for data.
* @throws XMPPException If an error occurs during the negotiation process an
diff --git a/source/org/jivesoftware/smackx/ibb/CloseListener.java b/source/org/jivesoftware/smackx/ibb/CloseListener.java
new file mode 100644
index 000000000..8bec215b6
--- /dev/null
+++ b/source/org/jivesoftware/smackx/ibb/CloseListener.java
@@ -0,0 +1,75 @@
+/**
+ * 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.ibb;
+
+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.ibb.packet.Close;
+
+/**
+ * CloseListener handles all In-Band Bytestream close requests.
+ *
+ * If a close request is received it looks if a stored In-Band Bytestream
+ * session exists and closes it. If no session with the given session ID exists
+ * an <item-not-found/> error is returned to the sender.
+ *
+ * @author Henning Staib
+ */
+class CloseListener implements PacketListener {
+
+ /* manager containing the listeners and the XMPP connection */
+ private final InBandBytestreamManager manager;
+
+ /* packet filter for all In-Band Bytestream close requests */
+ private final PacketFilter closeFilter = new AndFilter(new PacketTypeFilter(
+ Close.class), new IQTypeFilter(IQ.Type.SET));
+
+ /**
+ * Constructor.
+ *
+ * @param manager the In-Band Bytestream manager
+ */
+ protected CloseListener(InBandBytestreamManager manager) {
+ this.manager = manager;
+ }
+
+ public void processPacket(Packet packet) {
+ Close closeRequest = (Close) packet;
+ InBandBytestreamSession ibbSession = this.manager.getSessions().get(
+ closeRequest.getSessionID());
+ if (ibbSession == null) {
+ this.manager.replyItemNotFoundPacket(closeRequest);
+ }
+ else {
+ ibbSession.closeByPeer(closeRequest);
+ this.manager.getSessions().remove(closeRequest.getSessionID());
+ }
+
+ }
+
+ /**
+ * Returns the packet filter for In-Band Bytestream close requests.
+ *
+ * @return the packet filter for In-Band Bytestream close requests
+ */
+ protected PacketFilter getFilter() {
+ return this.closeFilter;
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/ibb/DataListener.java b/source/org/jivesoftware/smackx/ibb/DataListener.java
new file mode 100644
index 000000000..16c132eab
--- /dev/null
+++ b/source/org/jivesoftware/smackx/ibb/DataListener.java
@@ -0,0 +1,73 @@
+/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.ibb;
+
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.ibb.packet.Data;
+
+/**
+ * DataListener handles all In-Band Bytestream IQ stanzas containing a data
+ * packet extension that don't belong to an existing session.
+ *
+ * If a data packet is received it looks if a stored In-Band Bytestream session
+ * exists. If no session with the given session ID exists an
+ * <item-not-found/> error is returned to the sender.
+ *
+ * Data packets belonging to a running In-Band Bytestream session are processed
+ * by more specific listeners registered when an {@link InBandBytestreamSession}
+ * is created.
+ *
+ * @author Henning Staib
+ */
+class DataListener implements PacketListener {
+
+ /* manager containing the listeners and the XMPP connection */
+ private final InBandBytestreamManager manager;
+
+ /* packet filter for all In-Band Bytestream data packets */
+ private final PacketFilter dataFilter = new AndFilter(
+ new PacketTypeFilter(Data.class));
+
+ /**
+ * Constructor.
+ *
+ * @param manager the In-Band Bytestream manager
+ */
+ public DataListener(InBandBytestreamManager manager) {
+ this.manager = manager;
+ }
+
+ public void processPacket(Packet packet) {
+ Data data = (Data) packet;
+ InBandBytestreamSession ibbSession = this.manager.getSessions().get(
+ data.getDataPacketExtension().getSessionID());
+ if (ibbSession == null) {
+ this.manager.replyItemNotFoundPacket(data);
+ }
+ }
+
+ /**
+ * Returns the packet filter for In-Band Bytestream data packets.
+ *
+ * @return the packet filter for In-Band Bytestream data packets
+ */
+ protected PacketFilter getFilter() {
+ return this.dataFilter;
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/ibb/InBandBytestreamListener.java b/source/org/jivesoftware/smackx/ibb/InBandBytestreamListener.java
new file mode 100644
index 000000000..3aed11177
--- /dev/null
+++ b/source/org/jivesoftware/smackx/ibb/InBandBytestreamListener.java
@@ -0,0 +1,46 @@
+/**
+ * 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.ibb;
+
+import org.jivesoftware.smackx.bytestreams.BytestreamListener;
+import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
+
+/**
+ * InBandBytestreamListener are informed if a remote user wants to initiate an In-Band Bytestream.
+ * Implement this interface to handle incoming In-Band Bytestream requests.
+ *
+ * There are two ways to add this listener. See
+ * {@link InBandBytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and
+ * {@link InBandBytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)} for
+ * further details.
+ *
+ * @author Henning Staib
+ */
+public abstract class InBandBytestreamListener implements BytestreamListener {
+
+
+
+ public void incomingBytestreamRequest(BytestreamRequest request) {
+ incomingBytestreamRequest((InBandBytestreamRequest) request);
+ }
+
+ /**
+ * This listener is notified if an In-Band Bytestream request from another user has been
+ * received.
+ *
+ * @param request the incoming In-Band Bytestream request
+ */
+ public abstract void incomingBytestreamRequest(InBandBytestreamRequest request);
+
+}
diff --git a/source/org/jivesoftware/smackx/ibb/InBandBytestreamManager.java b/source/org/jivesoftware/smackx/ibb/InBandBytestreamManager.java
new file mode 100644
index 000000000..835f4f959
--- /dev/null
+++ b/source/org/jivesoftware/smackx/ibb/InBandBytestreamManager.java
@@ -0,0 +1,546 @@
+/**
+ * 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.ibb;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ConcurrentHashMap;
+
+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.XMPPError;
+import org.jivesoftware.smackx.bytestreams.BytestreamListener;
+import org.jivesoftware.smackx.bytestreams.BytestreamManager;
+import org.jivesoftware.smackx.filetransfer.FileTransferManager;
+import org.jivesoftware.smackx.ibb.packet.Open;
+import org.jivesoftware.smackx.packet.SyncPacketSend;
+
+/**
+ * The InBandBytestreamManager class handles establishing In-Band Bytestreams as specified in the XEP-0047.
+ *
+ * The In-Band Bytestreams (IBB) enables two entities to establish a virtual bytestream over which
+ * they can exchange Base64-encoded chunks of data over XMPP itself. It is the fall-back mechanism
+ * in case the Socks5 bytestream method of transferring data is not available.
+ *
+ * There are two ways to send data over an In-Band Bytestream. It could either use IQ stanzas to
+ * send data packets or message stanzas. If IQ stanzas are used every data packet is acknowledged by
+ * the receiver. This is the recommended way to avoid possible rate-limiting penalties. Message
+ * stanzas are not acknowledged because most XMPP server implementation don't support stanza
+ * flow-control method like Advanced Message
+ * Processing. To set the stanza that should be used invoke {@link #setStanza(StanzaType)}.
+ *
+ * To establish an In-Band Bytestream invoke the {@link #establishSession(String)} method. This will
+ * negotiate an in-band bytestream with the given target JID and return a session.
+ *
+ * If a session ID for the In-Band Bytestream was already negotiated (e.g. while negotiating a file
+ * transfer) invoke {@link #establishSession(String, String)}.
+ *
+ * To handle incoming In-Band Bytestream requests add an {@link InBandBytestreamListener} to the
+ * manager. There are two ways to add this listener. If you want to be informed about incoming
+ * In-Band Bytestreams from a specific user add the listener by invoking
+ * {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the listener should
+ * respond to all In-Band Bytestream requests invoke
+ * {@link #addIncomingBytestreamListener(BytestreamListener)}.
+ *
+ * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
+ * In-Band bytestream requests sent in the context of XEP-0096 file transfer. (See
+ * {@link FileTransferManager})
+ *
+ * If no {@link InBandBytestreamListener}s are registered, all incoming In-Band bytestream requests
+ * will be rejected by returning a <not-acceptable/> error to the initiator.
+ *
+ * @author Henning Staib
+ */
+public class InBandBytestreamManager implements BytestreamManager {
+
+ /**
+ * Stanzas that can be used to encapsulate In-Band Bytestream data packets.
+ */
+ public enum StanzaType {
+
+ /**
+ * IQ stanza.
+ */
+ IQ,
+
+ /**
+ * Message stanza.
+ */
+ MESSAGE
+ }
+
+ /*
+ * create a new InBandBytestreamManager and register its shutdown listener on every established
+ * connection
+ */
+ static {
+ Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+ public void connectionCreated(Connection connection) {
+ final InBandBytestreamManager manager;
+ manager = InBandBytestreamManager.getByteStreamManager(connection);
+
+ // register shutdown listener
+ connection.addConnectionListener(new AbstractConnectionListener() {
+
+ public void connectionClosed() {
+ manager.disableService();
+ }
+
+ });
+
+ }
+ });
+ }
+
+ /**
+ * The XMPP namespace of the In-Band Bytestream
+ */
+ public static final String NAMESPACE = "http://jabber.org/protocol/ibb";
+
+ /**
+ * Maximum block size that is allowed for In-Band Bytestreams
+ */
+ public static final int MAXIMUM_BLOCK_SIZE = 65535;
+
+ /* prefix used to generate session IDs */
+ private static final String SESSION_ID_PREFIX = "jibb_";
+
+ /* random generator to create session IDs */
+ private final static Random randomGenerator = new Random();
+
+ /* stores one InBandBytestreamManager for each XMPP connection */
+ private final static Map managers = new HashMap();
+
+ /* XMPP connection */
+ private final Connection connection;
+
+ /*
+ * assigns a user to a listener that is informed if an In-Band Bytestream request for this user
+ * is received
+ */
+ private final Map userListeners = new ConcurrentHashMap();
+
+ /*
+ * list of listeners that respond to all In-Band Bytestream requests if there are no user
+ * specific listeners for that request
+ */
+ private final List allRequestListeners = Collections.synchronizedList(new LinkedList());
+
+ /* listener that handles all incoming In-Band Bytestream requests */
+ private final InitiationListener initiationListener;
+
+ /* listener that handles all incoming In-Band Bytestream IQ data packets */
+ private final DataListener dataListener;
+
+ /* listener that handles all incoming In-Band Bytestream close requests */
+ private final CloseListener closeListener;
+
+ /* assigns a session ID to the In-Band Bytestream session */
+ private final Map sessions = new ConcurrentHashMap();
+
+ /* block size used for new In-Band Bytestreams */
+ private int defaultBlockSize = 4096;
+
+ /* maximum block size allowed for this connection */
+ private int maximumBlockSize = MAXIMUM_BLOCK_SIZE;
+
+ /* the stanza used to send data packets */
+ private StanzaType stanza = StanzaType.IQ;
+
+ /*
+ * list containing session IDs of In-Band Bytestream open packets that should be ignored by the
+ * InitiationListener
+ */
+ private List ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList());
+
+ /**
+ * Returns the InBandBytestreamManager to handle In-Band Bytestreams for a given
+ * {@link Connection}.
+ *
+ * @param connection the XMPP connection
+ * @return the InBandBytestreamManager for the given XMPP connection
+ */
+ public static synchronized InBandBytestreamManager getByteStreamManager(Connection connection) {
+ if (connection == null)
+ return null;
+ InBandBytestreamManager manager = managers.get(connection);
+ if (manager == null) {
+ manager = new InBandBytestreamManager(connection);
+ managers.put(connection, manager);
+ }
+ return manager;
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param connection the XMPP connection
+ */
+ private InBandBytestreamManager(Connection connection) {
+ this.connection = connection;
+
+ // register bytestream open packet listener
+ this.initiationListener = new InitiationListener(this);
+ this.connection.addPacketListener(this.initiationListener,
+ this.initiationListener.getFilter());
+
+ // register bytestream data packet listener
+ this.dataListener = new DataListener(this);
+ this.connection.addPacketListener(this.dataListener, this.dataListener.getFilter());
+
+ // register bytestream close packet listener
+ this.closeListener = new CloseListener(this);
+ this.connection.addPacketListener(this.closeListener, this.closeListener.getFilter());
+
+ }
+
+ /**
+ * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request
+ * unless there is a user specific InBandBytestreamListener registered.
+ *
+ * If no listeners are registered all In-Band Bytestream request are rejected with a
+ * <not-acceptable/> error.
+ *
+ * Note that the registered {@link InBandBytestreamListener} 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 In-Band Bytestream
+ * requests.
+ *
+ * @param listener the listener to remove
+ */
+ public void removeIncomingBytestreamListener(BytestreamListener listener) {
+ this.allRequestListeners.remove(listener);
+ }
+
+ /**
+ * Adds InBandBytestreamListener that is called for every incoming in-band 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 In-Band Bytestream request are rejected with a
+ * <not-acceptable/> error.
+ *
+ * Note that the registered {@link InBandBytestreamListener} 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 an In-Band 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 In-Band 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 an In-Band 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);
+ }
+
+ /**
+ * Returns the default block size that is used for all outgoing in-band bytestreams for this
+ * connection.
+ *
+ * The recommended default block size is 4096 bytes. See XEP-0047 Section 5.
+ *
+ * @return the default block size
+ */
+ public int getDefaultBlockSize() {
+ return defaultBlockSize;
+ }
+
+ /**
+ * Sets the default block size that is used for all outgoing in-band bytestreams for this
+ * connection.
+ *
+ * The default block size must be between 1 and 65535 bytes. The recommended default block size
+ * is 4096 bytes. See XEP-0047
+ * Section 5.
+ *
+ * @param defaultBlockSize the default block size to set
+ */
+ public void setDefaultBlockSize(int defaultBlockSize) {
+ if (defaultBlockSize <= 0 || defaultBlockSize > MAXIMUM_BLOCK_SIZE) {
+ throw new IllegalArgumentException("Default block size must be between 1 and "
+ + MAXIMUM_BLOCK_SIZE);
+ }
+ this.defaultBlockSize = defaultBlockSize;
+ }
+
+ /**
+ * Returns the maximum block size that is allowed for In-Band Bytestreams for this connection.
+ *
+ * Incoming In-Band Bytestream open request will be rejected with an
+ * <resource-constraint/> error if the block size is greater then the maximum allowed
+ * block size.
+ *
+ * The default maximum block size is 65535 bytes.
+ *
+ * @return the maximum block size
+ */
+ public int getMaximumBlockSize() {
+ return maximumBlockSize;
+ }
+
+ /**
+ * Sets the maximum block size that is allowed for In-Band Bytestreams for this connection.
+ *
+ * The maximum block size must be between 1 and 65535 bytes.
+ *
+ * Incoming In-Band Bytestream open request will be rejected with an
+ * <resource-constraint/> error if the block size is greater then the maximum allowed
+ * block size.
+ *
+ * @param maximumBlockSize the maximum block size to set
+ */
+ public void setMaximumBlockSize(int maximumBlockSize) {
+ if (maximumBlockSize <= 0 || maximumBlockSize > MAXIMUM_BLOCK_SIZE) {
+ throw new IllegalArgumentException("Maximum block size must be between 1 and "
+ + MAXIMUM_BLOCK_SIZE);
+ }
+ this.maximumBlockSize = maximumBlockSize;
+ }
+
+ /**
+ * Returns the stanza used to send data packets.
+ *
+ * Default is {@link StanzaType#IQ}. See XEP-0047 Section 4.
+ *
+ * @return the stanza used to send data packets
+ */
+ public StanzaType getStanza() {
+ return stanza;
+ }
+
+ /**
+ * Sets the stanza used to send data packets.
+ *
+ * The use of {@link StanzaType#IQ} is recommended. See XEP-0047 Section 4.
+ *
+ * @param stanza the stanza to set
+ */
+ public void setStanza(StanzaType stanza) {
+ this.stanza = stanza;
+ }
+
+ /**
+ * Establishes an In-Band Bytestream with the given user and returns the session to send/receive
+ * data to/from the user.
+ *
+ * Use this method to establish In-Band Bytestreams to users accepting all incoming In-Band
+ * Bytestream requests since this method doesn't provide a way to tell the user something about
+ * the data to be sent.
+ *
+ * To establish an In-Band 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 an In-Band Bytestream should be established
+ * @return the session to send/receive data to/from the user
+ * @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the
+ * user prefers smaller block sizes
+ */
+ public InBandBytestreamSession establishSession(String targetJID) throws XMPPException {
+ String sessionID = getNextSessionID();
+ return establishSession(targetJID, sessionID);
+ }
+
+ /**
+ * Establishes an In-Band Bytestream with the given user using the given session ID and returns
+ * the session to send/receive data to/from the user.
+ *
+ * @param targetJID the JID of the user an In-Band Bytestream should be established
+ * @param sessionID the session ID for the In-Band Bytestream request
+ * @return the session to send/receive data to/from the user
+ * @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the
+ * user prefers smaller block sizes
+ */
+ public InBandBytestreamSession establishSession(String targetJID, String sessionID)
+ throws XMPPException {
+ Open byteStreamRequest = new Open(sessionID, this.defaultBlockSize, this.stanza);
+ byteStreamRequest.setTo(targetJID);
+
+ // sending packet will throw exception on timeout or error reply
+ SyncPacketSend.getReply(this.connection, byteStreamRequest);
+
+ InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession(
+ this.connection, byteStreamRequest, targetJID);
+ this.sessions.put(sessionID, inBandBytestreamSession);
+
+ return inBandBytestreamSession;
+ }
+
+ /**
+ * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream is
+ * not accepted.
+ *
+ * @param request IQ packet that should be answered with a not-acceptable error
+ */
+ protected void replyRejectPacket(IQ request) {
+ XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable);
+ IQ error = IQ.createErrorResponse(request, xmppError);
+ this.connection.sendPacket(error);
+ }
+
+ /**
+ * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream open
+ * request is rejected because its block size is greater than the maximum allowed block size.
+ *
+ * @param request IQ packet that should be answered with a resource-constraint error
+ */
+ protected void replyResourceConstraintPacket(IQ request) {
+ XMPPError xmppError = new XMPPError(XMPPError.Condition.resource_constraint);
+ IQ error = IQ.createErrorResponse(request, xmppError);
+ this.connection.sendPacket(error);
+ }
+
+ /**
+ * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream
+ * session could not be found.
+ *
+ * @param request IQ packet that should be answered with a item-not-found error
+ */
+ protected void replyItemNotFoundPacket(IQ request) {
+ XMPPError xmppError = new XMPPError(XMPPError.Condition.item_not_found);
+ IQ error = IQ.createErrorResponse(request, xmppError);
+ this.connection.sendPacket(error);
+ }
+
+ /**
+ * 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 InBandBytestreamListener} that should be informed if a In-Band 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 InBandBytestreamListener} that are informed if there are no
+ * listeners for a specific initiator.
+ *
+ * @return list of listeners
+ */
+ protected List getAllRequestListeners() {
+ return this.allRequestListeners;
+ }
+
+ /**
+ * Returns the sessions map.
+ *
+ * @return the sessions map
+ */
+ protected Map getSessions() {
+ return sessions;
+ }
+
+ /**
+ * Returns the list of session IDs that should be ignored by the InitialtionListener
+ *
+ * @return list of session IDs
+ */
+ protected List getIgnoredBytestreamRequests() {
+ return ignoredBytestreamRequests;
+ }
+
+ /**
+ * Disables the InBandBytestreamManager by removing its packet listeners and resetting its
+ * internal status.
+ */
+ private void disableService() {
+
+ // remove manager from static managers map
+ managers.remove(connection);
+
+ // remove all listeners registered by this manager
+ this.connection.removePacketListener(this.initiationListener);
+ this.connection.removePacketListener(this.dataListener);
+ this.connection.removePacketListener(this.closeListener);
+
+ // shutdown threads
+ this.initiationListener.shutdown();
+
+ // reset internal status
+ this.userListeners.clear();
+ this.allRequestListeners.clear();
+ this.sessions.clear();
+ this.ignoredBytestreamRequests.clear();
+
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/ibb/InBandBytestreamRequest.java b/source/org/jivesoftware/smackx/ibb/InBandBytestreamRequest.java
new file mode 100644
index 000000000..3fabdfa26
--- /dev/null
+++ b/source/org/jivesoftware/smackx/ibb/InBandBytestreamRequest.java
@@ -0,0 +1,92 @@
+/**
+ * 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.ibb;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
+import org.jivesoftware.smackx.ibb.packet.Open;
+
+/**
+ * InBandBytestreamRequest class handles incoming In-Band Bytestream requests.
+ *
+ * @author Henning Staib
+ */
+public class InBandBytestreamRequest implements BytestreamRequest {
+
+ /* the bytestream initialization request */
+ private final Open byteStreamRequest;
+
+ /*
+ * In-Band Bytestream manager containing the XMPP connection and helper
+ * methods
+ */
+ private final InBandBytestreamManager manager;
+
+ protected InBandBytestreamRequest(InBandBytestreamManager manager,
+ Open byteStreamRequest) {
+ this.manager = manager;
+ this.byteStreamRequest = byteStreamRequest;
+ }
+
+ /**
+ * Returns the sender of the In-Band Bytestream open request.
+ *
+ * @return the sender of the In-Band Bytestream open request
+ */
+ public String getFrom() {
+ return this.byteStreamRequest.getFrom();
+ }
+
+ /**
+ * Returns the session ID of the In-Band Bytestream open request.
+ *
+ * @return the session ID of the In-Band Bytestream open request
+ */
+ public String getSessionID() {
+ return this.byteStreamRequest.getSessionID();
+ }
+
+ /**
+ * Accepts the In-Band Bytestream open request and returns the session to
+ * send/receive data.
+ *
+ * @return the session to send/receive data
+ * @throws XMPPException if stream is invalid.
+ */
+ public InBandBytestreamSession accept() throws XMPPException {
+ Connection connection = this.manager.getConnection();
+
+ // create In-Band Bytestream session and store it
+ InBandBytestreamSession ibbSession = new InBandBytestreamSession(connection,
+ this.byteStreamRequest, this.byteStreamRequest.getFrom());
+ this.manager.getSessions().put(this.byteStreamRequest.getSessionID(), ibbSession);
+
+ // acknowledge request
+ IQ resultIQ = IQ.createResultIQ(this.byteStreamRequest);
+ connection.sendPacket(resultIQ);
+
+ return ibbSession;
+ }
+
+ /**
+ * Rejects the In-Band Bytestream request by sending a reject error to the
+ * initiator.
+ */
+ public void reject() {
+ this.manager.replyRejectPacket(this.byteStreamRequest);
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/ibb/InBandBytestreamSession.java b/source/org/jivesoftware/smackx/ibb/InBandBytestreamSession.java
new file mode 100644
index 000000000..8cd63f8c3
--- /dev/null
+++ b/source/org/jivesoftware/smackx/ibb/InBandBytestreamSession.java
@@ -0,0 +1,795 @@
+/**
+ * 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.ibb;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.SocketTimeoutException;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.bytestreams.BytestreamSession;
+import org.jivesoftware.smackx.ibb.packet.Close;
+import org.jivesoftware.smackx.ibb.packet.Data;
+import org.jivesoftware.smackx.ibb.packet.DataPacketExtension;
+import org.jivesoftware.smackx.ibb.packet.Open;
+import org.jivesoftware.smackx.packet.SyncPacketSend;
+
+/**
+ * InBandBytestreamSession class represents an In-Band Bytestream session.
+ *
+ * In-band bytestreams are bidirectional and this session encapsulates the streams for both
+ * directions.
+ *
+ * Note that closing the In-Band Bytestream session will close both streams. If both streams are
+ * closed individually the session will be closed automatically once the second stream is closed.
+ * Use the {@link #setCloseBothStreamsEnabled(boolean)} method if both streams should be closed
+ * automatically if one of them is closed.
+ *
+ * @author Henning Staib
+ */
+public class InBandBytestreamSession implements BytestreamSession {
+
+ /* XMPP connection */
+ private final Connection connection;
+
+ /* the In-Band Bytestream open request for this session */
+ private final Open byteStreamRequest;
+
+ /*
+ * the input stream for this session (either IQIBBInputStream or MessageIBBInputStream)
+ */
+ private IBBInputStream inputStream;
+
+ /*
+ * the output stream for this session (either IQIBBOutputStream or MessageIBBOutputStream)
+ */
+ private IBBOutputStream outputStream;
+
+ /* JID of the remote peer */
+ private String remoteJID;
+
+ /* flag to close both streams if one of them is closed */
+ private boolean closeBothStreamsEnabled = false;
+
+ /* flag to indicate if session is closed */
+ private boolean isClosed = false;
+
+ /**
+ * Constructor.
+ *
+ * @param connection the XMPP connection
+ * @param byteStreamRequest the In-Band Bytestream open request for this session
+ * @param remoteJID JID of the remote peer
+ */
+ protected InBandBytestreamSession(Connection connection, Open byteStreamRequest,
+ String remoteJID) {
+ this.connection = connection;
+ this.byteStreamRequest = byteStreamRequest;
+ this.remoteJID = remoteJID;
+
+ // initialize streams dependent to the uses stanza type
+ switch (byteStreamRequest.getStanza()) {
+ case IQ:
+ this.inputStream = new IQIBBInputStream();
+ this.outputStream = new IQIBBOutputStream();
+ break;
+ case MESSAGE:
+ this.inputStream = new MessageIBBInputStream();
+ this.outputStream = new MessageIBBOutputStream();
+ break;
+ }
+
+ }
+
+ public InputStream getInputStream() {
+ return this.inputStream;
+ }
+
+ public OutputStream getOutputStream() {
+ return this.outputStream;
+ }
+
+ public int getReadTimeout() {
+ return this.inputStream.readTimeout;
+ }
+
+ public void setReadTimeout(int timeout) {
+ if (timeout < 0) {
+ throw new IllegalArgumentException("Timeout must be >= 0");
+ }
+ this.inputStream.readTimeout = timeout;
+ }
+
+ /**
+ * Returns whether both streams should be closed automatically if one of the streams is closed.
+ * Default is false.
+ *
+ * @return true if both streams will be closed if one of the streams is closed,
+ * false if both streams can be closed independently.
+ */
+ public boolean isCloseBothStreamsEnabled() {
+ return closeBothStreamsEnabled;
+ }
+
+ /**
+ * Sets whether both streams should be closed automatically if one of the streams is closed.
+ * Default is false.
+ *
+ * @param closeBothStreamsEnabled true if both streams should be closed if one of
+ * the streams is closed, false if both streams should be closed
+ * independently
+ */
+ public void setCloseBothStreamsEnabled(boolean closeBothStreamsEnabled) {
+ this.closeBothStreamsEnabled = closeBothStreamsEnabled;
+ }
+
+ public void close() throws IOException {
+ closeByLocal(true); // close input stream
+ closeByLocal(false); // close output stream
+ }
+
+ /**
+ * This method is invoked if a request to close the In-Band Bytestream has been received.
+ *
+ * @param closeRequest the close request from the remote peer
+ */
+ protected void closeByPeer(Close closeRequest) {
+
+ /*
+ * close streams without flushing them, because stream is already considered closed on the
+ * remote peers side
+ */
+ this.inputStream.closeInternal();
+ this.inputStream.cleanup();
+ this.outputStream.closeInternal(false);
+
+ // acknowledge close request
+ IQ confirmClose = IQ.createResultIQ(closeRequest);
+ this.connection.sendPacket(confirmClose);
+
+ }
+
+ /**
+ * This method is invoked if one of the streams has been closed locally, if an error occurred
+ * locally or if the whole session should be closed.
+ *
+ * @throws IOException if an error occurs while sending the close request
+ */
+ protected synchronized void closeByLocal(boolean in) throws IOException {
+ if (this.isClosed) {
+ return;
+ }
+
+ if (this.closeBothStreamsEnabled) {
+ this.inputStream.closeInternal();
+ this.outputStream.closeInternal(true);
+ }
+ else {
+ if (in) {
+ this.inputStream.closeInternal();
+ }
+ else {
+ // close stream but try to send any data left
+ this.outputStream.closeInternal(true);
+ }
+ }
+
+ if (this.inputStream.isClosed && this.outputStream.isClosed) {
+ this.isClosed = true;
+
+ // send close request
+ Close close = new Close(this.byteStreamRequest.getSessionID());
+ close.setTo(this.remoteJID);
+ try {
+ SyncPacketSend.getReply(this.connection, close);
+ }
+ catch (XMPPException e) {
+ throw new IOException("Error while closing stream: " + e.getMessage());
+ }
+
+ this.inputStream.cleanup();
+
+ // remove session from manager
+ InBandBytestreamManager.getByteStreamManager(this.connection).getSessions().remove(this);
+ }
+
+ }
+
+ /**
+ * IBBInputStream class is the base implementation of an In-Band Bytestream input stream.
+ * Subclasses of this input stream must provide a packet listener along with a packet filter to
+ * collect the In-Band Bytestream data packets.
+ */
+ private abstract class IBBInputStream extends InputStream {
+
+ /* the data packet listener to fill the data queue */
+ private final PacketListener dataPacketListener;
+
+ /* queue containing received In-Band Bytestream data packets */
+ protected final BlockingQueue dataQueue = new LinkedBlockingQueue();
+
+ /* buffer containing the data from one data packet */
+ private byte[] buffer;
+
+ /* pointer to the next byte to read from buffer */
+ private int bufferPointer = -1;
+
+ /* data packet sequence (range from 0 to 65535) */
+ private long seq = -1;
+
+ /* flag to indicate if input stream is closed */
+ private boolean isClosed = false;
+
+ /* flag to indicate if close method was invoked */
+ private boolean closeInvoked = false;
+
+ /* timeout for read operations */
+ private int readTimeout = 0;
+
+ /**
+ * Constructor.
+ */
+ public IBBInputStream() {
+ // add data packet listener to connection
+ this.dataPacketListener = getDataPacketListener();
+ connection.addPacketListener(this.dataPacketListener, getDataPacketFilter());
+ }
+
+ /**
+ * Returns the packet listener that processes In-Band Bytestream data packets.
+ *
+ * @return the data packet listener
+ */
+ protected abstract PacketListener getDataPacketListener();
+
+ /**
+ * Returns the packet filter that accepts In-Band Bytestream data packets.
+ *
+ * @return the data packet filter
+ */
+ protected abstract PacketFilter getDataPacketFilter();
+
+ public synchronized int read() throws IOException {
+ checkClosed();
+
+ // if nothing read yet or whole buffer has been read fill buffer
+ if (bufferPointer == -1 || bufferPointer >= buffer.length) {
+ // if no data available and stream was closed return -1
+ if (!loadBuffer()) {
+ return -1;
+ }
+ }
+
+ // return byte and increment buffer pointer
+ return (int) buffer[bufferPointer++];
+ }
+
+ public synchronized int read(byte[] b, int off, int len) throws IOException {
+ if (b == null) {
+ throw new NullPointerException();
+ }
+ else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length)
+ || ((off + len) < 0)) {
+ throw new IndexOutOfBoundsException();
+ }
+ else if (len == 0) {
+ return 0;
+ }
+
+ checkClosed();
+
+ // if nothing read yet or whole buffer has been read fill buffer
+ if (bufferPointer == -1 || bufferPointer >= buffer.length) {
+ // if no data available and stream was closed return -1
+ if (!loadBuffer()) {
+ return -1;
+ }
+ }
+
+ // if more bytes wanted than available return all available
+ int bytesAvailable = buffer.length - bufferPointer;
+ if (len > bytesAvailable) {
+ len = bytesAvailable;
+ }
+
+ System.arraycopy(buffer, bufferPointer, b, off, len);
+ bufferPointer += len;
+ return len;
+ }
+
+ public synchronized int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ /**
+ * This method blocks until a data packet is received, the stream is closed or the current
+ * thread is interrupted.
+ *
+ * @return true if data was received, otherwise false
+ * @throws IOException if data packets are out of sequence
+ */
+ private synchronized boolean loadBuffer() throws IOException {
+
+ // wait until data is available or stream is closed
+ DataPacketExtension data = null;
+ try {
+ if (this.readTimeout == 0) {
+ while (data == null) {
+ if (isClosed && this.dataQueue.isEmpty()) {
+ return false;
+ }
+ data = this.dataQueue.poll(1000, TimeUnit.MILLISECONDS);
+ }
+ }
+ else {
+ data = this.dataQueue.poll(this.readTimeout, TimeUnit.MILLISECONDS);
+ if (data == null) {
+ throw new SocketTimeoutException();
+ }
+ }
+ }
+ catch (InterruptedException e) {
+ // Restore the interrupted status
+ Thread.currentThread().interrupt();
+ return false;
+ }
+
+ // handle sequence overflow
+ if (this.seq == 65535) {
+ this.seq = -1;
+ }
+
+ // check if data packets sequence is successor of last seen sequence
+ long seq = data.getSeq();
+ if (seq - 1 != this.seq) {
+ // packets out of order; close stream/session
+ InBandBytestreamSession.this.close();
+ throw new IOException("Packets out of sequence");
+ }
+ else {
+ this.seq = seq;
+ }
+
+ // set buffer to decoded data
+ buffer = data.getDecodedData();
+ bufferPointer = 0;
+ return true;
+ }
+
+ /**
+ * Checks if this stream is closed and throws an IOException if necessary
+ *
+ * @throws IOException if stream is closed and no data should be read anymore
+ */
+ private void checkClosed() throws IOException {
+ /* throw no exception if there is data available, but not if close method was invoked */
+ if ((isClosed && this.dataQueue.isEmpty()) || closeInvoked) {
+ // clear data queue in case additional data was received after stream was closed
+ this.dataQueue.clear();
+ throw new IOException("Stream is closed");
+ }
+ }
+
+ public boolean markSupported() {
+ return false;
+ }
+
+ public void close() throws IOException {
+ if (isClosed) {
+ return;
+ }
+
+ this.closeInvoked = true;
+
+ InBandBytestreamSession.this.closeByLocal(true);
+ }
+
+ /**
+ * This method sets the close flag and removes the data packet listener.
+ */
+ private void closeInternal() {
+ if (isClosed) {
+ return;
+ }
+ isClosed = true;
+ }
+
+ /**
+ * Invoked if the session is closed.
+ */
+ private void cleanup() {
+ connection.removePacketListener(this.dataPacketListener);
+ }
+
+ }
+
+ /**
+ * IQIBBInputStream class implements IBBInputStream to be used with IQ stanzas encapsulating the
+ * data packets.
+ */
+ private class IQIBBInputStream extends IBBInputStream {
+
+ protected PacketListener getDataPacketListener() {
+ return new PacketListener() {
+
+ private long lastSequence = -1;
+
+ public void processPacket(Packet packet) {
+ // get data packet extension
+ DataPacketExtension data = (DataPacketExtension) packet.getExtension(
+ DataPacketExtension.ELEMENT_NAME,
+ InBandBytestreamManager.NAMESPACE);
+
+ /*
+ * check if sequence was not used already (see XEP-0047 Section 2.2)
+ */
+ if (data.getSeq() <= this.lastSequence) {
+ IQ unexpectedRequest = IQ.createErrorResponse((IQ) packet, new XMPPError(
+ XMPPError.Condition.unexpected_request));
+ connection.sendPacket(unexpectedRequest);
+ return;
+
+ }
+
+ // check if encoded data is valid (see XEP-0047 Section 2.2)
+ if (data.getDecodedData() == null) {
+ // data is invalid; respond with bad-request error
+ IQ badRequest = IQ.createErrorResponse((IQ) packet, new XMPPError(
+ XMPPError.Condition.bad_request));
+ connection.sendPacket(badRequest);
+ return;
+ }
+
+ // data is valid; add to data queue
+ dataQueue.offer(data);
+
+ // confirm IQ
+ IQ confirmData = IQ.createResultIQ((IQ) packet);
+ connection.sendPacket(confirmData);
+
+ // set last seen sequence
+ this.lastSequence = data.getSeq();
+ if (this.lastSequence == 65535) {
+ this.lastSequence = -1;
+ }
+
+ }
+
+ };
+ }
+
+ protected PacketFilter getDataPacketFilter() {
+ /*
+ * filter all IQ stanzas having type 'SET' (represented by Data class), containing a
+ * data packet extension, matching session ID and recipient
+ */
+ return new AndFilter(new PacketTypeFilter(Data.class), new IBBDataPacketFilter());
+ }
+
+ }
+
+ /**
+ * MessageIBBInputStream class implements IBBInputStream to be used with message stanzas
+ * encapsulating the data packets.
+ */
+ private class MessageIBBInputStream extends IBBInputStream {
+
+ protected PacketListener getDataPacketListener() {
+ return new PacketListener() {
+
+ public void processPacket(Packet packet) {
+ // get data packet extension
+ DataPacketExtension data = (DataPacketExtension) packet.getExtension(
+ DataPacketExtension.ELEMENT_NAME,
+ InBandBytestreamManager.NAMESPACE);
+
+ // check if encoded data is valid
+ if (data.getDecodedData() == null) {
+ /*
+ * TODO once a majority of XMPP server implementation support XEP-0079
+ * Advanced Message Processing the invalid message could be answered with an
+ * appropriate error. For now we just ignore the packet. Subsequent packets
+ * with an increased sequence will cause the input stream to close the
+ * stream/session.
+ */
+ return;
+ }
+
+ // data is valid; add to data queue
+ dataQueue.offer(data);
+
+ // TODO confirm packet once XMPP servers support XEP-0079
+ }
+
+ };
+ }
+
+ @Override
+ protected PacketFilter getDataPacketFilter() {
+ /*
+ * filter all message stanzas containing a data packet extension, matching session ID
+ * and recipient
+ */
+ return new AndFilter(new PacketTypeFilter(Message.class), new IBBDataPacketFilter());
+ }
+
+ }
+
+ /**
+ * IBBDataPacketFilter class filters all packets from the remote peer of this session,
+ * containing an In-Band Bytestream data packet extension whose session ID matches this sessions
+ * ID.
+ */
+ private class IBBDataPacketFilter implements PacketFilter {
+
+ public boolean accept(Packet packet) {
+ // sender equals remote peer
+ if (!packet.getFrom().equalsIgnoreCase(remoteJID)) {
+ return false;
+ }
+
+ // stanza contains data packet extension
+ PacketExtension packetExtension = packet.getExtension(DataPacketExtension.ELEMENT_NAME,
+ InBandBytestreamManager.NAMESPACE);
+ if (packetExtension == null || !(packetExtension instanceof DataPacketExtension)) {
+ return false;
+ }
+
+ // session ID equals this session ID
+ DataPacketExtension data = (DataPacketExtension) packetExtension;
+ if (!data.getSessionID().equals(byteStreamRequest.getSessionID())) {
+ return false;
+ }
+
+ return true;
+ }
+
+ }
+
+ /**
+ * IBBOutputStream class is the base implementation of an In-Band Bytestream output stream.
+ * Subclasses of this output stream must provide a method to send data over XMPP stream.
+ */
+ private abstract class IBBOutputStream extends OutputStream {
+
+ /* buffer with the size of this sessions block size */
+ protected final byte[] buffer;
+
+ /* pointer to next byte to write to buffer */
+ protected int bufferPointer = 0;
+
+ /* data packet sequence (range from 0 to 65535) */
+ protected long seq = 0;
+
+ /* flag to indicate if output stream is closed */
+ protected boolean isClosed = false;
+
+ /**
+ * Constructor.
+ */
+ public IBBOutputStream() {
+ this.buffer = new byte[byteStreamRequest.getBlockSize()];
+ }
+
+ /**
+ * Writes the given data packet to the XMPP stream.
+ *
+ * @param data the data packet
+ * @throws IOException if an I/O error occurred while sending or if the stream is closed
+ */
+ protected abstract void writeToXML(DataPacketExtension data) throws IOException;
+
+ public synchronized void write(int b) throws IOException {
+ if (this.isClosed) {
+ throw new IOException("Stream is closed");
+ }
+
+ // if buffer is full flush buffer
+ if (bufferPointer >= buffer.length) {
+ flushBuffer();
+ }
+
+ buffer[bufferPointer++] = (byte) b;
+ }
+
+ public synchronized void write(byte b[], int off, int len) throws IOException {
+ if (b == null) {
+ throw new NullPointerException();
+ }
+ else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length)
+ || ((off + len) < 0)) {
+ throw new IndexOutOfBoundsException();
+ }
+ else if (len == 0) {
+ return;
+ }
+
+ if (this.isClosed) {
+ throw new IOException("Stream is closed");
+ }
+
+ // is data to send greater than buffer size
+ if (len >= buffer.length) {
+
+ // "byte" off the first chunk to write out
+ writeOut(b, off, buffer.length);
+
+ // recursively call this method with the lesser amount
+ write(b, off + buffer.length, len - buffer.length);
+ }
+ else {
+ writeOut(b, off, len);
+ }
+ }
+
+ public synchronized void write(byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ /**
+ * Fills the buffer with the given data and sends it over the XMPP stream if the buffers
+ * capacity has been reached. This method is only called from this class so it is assured
+ * that the amount of data to send is <= buffer capacity
+ *
+ * @param b the data
+ * @param off the data
+ * @param len the number of bytes to write
+ * @throws IOException if an I/O error occurred while sending or if the stream is closed
+ */
+ private synchronized void writeOut(byte b[], int off, int len) throws IOException {
+ if (this.isClosed) {
+ throw new IOException("Stream is closed");
+ }
+
+ // set to 0 in case the next 'if' block is not executed
+ int available = 0;
+
+ // is data to send greater that buffer space left
+ if (len > buffer.length - bufferPointer) {
+ // fill buffer to capacity and send it
+ available = buffer.length - bufferPointer;
+ System.arraycopy(b, off, buffer, bufferPointer, available);
+ bufferPointer += available;
+ flushBuffer();
+ }
+
+ // copy the data left to buffer
+ System.arraycopy(b, off + available, buffer, bufferPointer, len - available);
+ bufferPointer += len - available;
+ }
+
+ public synchronized void flush() throws IOException {
+ if (this.isClosed) {
+ throw new IOException("Stream is closed");
+ }
+ flushBuffer();
+ }
+
+ private synchronized void flushBuffer() throws IOException {
+
+ // do nothing if no data to send available
+ if (bufferPointer == 0) {
+ return;
+ }
+
+ // create data packet
+ String enc = StringUtils.encodeBase64(buffer, 0, bufferPointer, false);
+ DataPacketExtension data = new DataPacketExtension(byteStreamRequest.getSessionID(),
+ this.seq, enc);
+
+ // write to XMPP stream
+ writeToXML(data);
+
+ // reset buffer pointer
+ bufferPointer = 0;
+
+ // increment sequence, considering sequence overflow
+ this.seq = (this.seq + 1 == 65535 ? 0 : this.seq + 1);
+
+ }
+
+ public void close() throws IOException {
+ if (isClosed) {
+ return;
+ }
+ InBandBytestreamSession.this.closeByLocal(false);
+ }
+
+ /**
+ * Sets the close flag and optionally flushes the stream.
+ *
+ * @param flush if true flushes the stream
+ */
+ protected void closeInternal(boolean flush) {
+ if (this.isClosed) {
+ return;
+ }
+ this.isClosed = true;
+
+ try {
+ if (flush) {
+ flushBuffer();
+ }
+ }
+ catch (IOException e) {
+ /*
+ * ignore, because writeToXML() will not throw an exception if stream is already
+ * closed
+ */
+ }
+ }
+
+ }
+
+ /**
+ * IQIBBOutputStream class implements IBBOutputStream to be used with IQ stanzas encapsulating
+ * the data packets.
+ */
+ private class IQIBBOutputStream extends IBBOutputStream {
+
+ @Override
+ protected synchronized void writeToXML(DataPacketExtension data) throws IOException {
+ // create IQ stanza containing data packet
+ IQ iq = new Data(data);
+ iq.setTo(remoteJID);
+
+ try {
+ SyncPacketSend.getReply(connection, iq);
+ }
+ catch (XMPPException e) {
+ // close session unless it is already closed
+ if (!this.isClosed) {
+ InBandBytestreamSession.this.close();
+ throw new IOException("Error while sending Data: " + e.getMessage());
+ }
+ }
+
+ }
+
+ }
+
+ /**
+ * MessageIBBOutputStream class implements IBBOutputStream to be used with message stanzas
+ * encapsulating the data packets.
+ */
+ private class MessageIBBOutputStream extends IBBOutputStream {
+
+ @Override
+ protected synchronized void writeToXML(DataPacketExtension data) {
+ // create message stanza containing data packet
+ Message message = new Message(remoteJID);
+ message.addExtension(data);
+
+ connection.sendPacket(message);
+
+ }
+
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/ibb/InitiationListener.java b/source/org/jivesoftware/smackx/ibb/InitiationListener.java
new file mode 100644
index 000000000..820db0387
--- /dev/null
+++ b/source/org/jivesoftware/smackx/ibb/InitiationListener.java
@@ -0,0 +1,127 @@
+/**
+ * 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.ibb;
+
+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.ibb.packet.Open;
+
+/**
+ * InitiationListener handles all incoming In-Band Bytestream open requests. If there are no
+ * listeners for a In-Band Bytestream request InitiationListener will always refuse the request and
+ * reply with a <not-acceptable/> error (XEP-0047 Section 2.1).
+ *
+ * All In-Band Bytestream request having a block size greater than the maximum allowed block size
+ * for this connection are rejected with an <resource-constraint/> error. The maximum block
+ * size can be set by invoking {@link InBandBytestreamManager#setMaximumBlockSize(int)}.
+ *
+ * @author Henning Staib
+ */
+class InitiationListener implements PacketListener {
+
+ /* manager containing the listeners and the XMPP connection */
+ private final InBandBytestreamManager manager;
+
+ /* packet filter for all In-Band Bytestream requests */
+ private final PacketFilter initFilter = new AndFilter(new PacketTypeFilter(Open.class),
+ new IQTypeFilter(IQ.Type.SET));
+
+ /* executor service to process incoming requests concurrently */
+ private final ExecutorService initiationListenerExecutor;
+
+ /**
+ * Constructor.
+ *
+ * @param manager the In-Band Bytestream manager
+ */
+ protected InitiationListener(InBandBytestreamManager 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) {
+ Open ibbRequest = (Open) packet;
+
+ // validate that block size is within allowed range
+ if (ibbRequest.getBlockSize() > this.manager.getMaximumBlockSize()) {
+ this.manager.replyResourceConstraintPacket(ibbRequest);
+ return;
+ }
+
+ // ignore request if in ignore list
+ if (this.manager.getIgnoredBytestreamRequests().remove(ibbRequest.getSessionID()))
+ return;
+
+ // build bytestream request from packet
+ InBandBytestreamRequest request = new InBandBytestreamRequest(this.manager, ibbRequest);
+
+ // notify listeners for bytestream initiation from a specific user
+ BytestreamListener userListener = this.manager.getUserListener(ibbRequest.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(ibbRequest);
+ }
+ }
+
+ /**
+ * Returns the packet filter for In-Band Bytestream open requests.
+ *
+ * @return the packet filter for In-Band Bytestream open requests
+ */
+ protected PacketFilter getFilter() {
+ return this.initFilter;
+ }
+
+ /**
+ * Shuts down the listeners executor service.
+ */
+ protected void shutdown() {
+ this.initiationListenerExecutor.shutdownNow();
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/ibb/packet/Close.java b/source/org/jivesoftware/smackx/ibb/packet/Close.java
new file mode 100644
index 000000000..ff0997ff1
--- /dev/null
+++ b/source/org/jivesoftware/smackx/ibb/packet/Close.java
@@ -0,0 +1,65 @@
+/**
+ * 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.ibb.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.ibb.InBandBytestreamManager;
+
+/**
+ * Represents a request to close an In-Band Bytestream.
+ *
+ * @author Henning Staib
+ */
+public class Close extends IQ {
+
+ /* unique session ID identifying this In-Band Bytestream */
+ private final String sessionID;
+
+ /**
+ * Creates a new In-Band Bytestream close request packet.
+ *
+ * @param sessionID unique session ID identifying this In-Band Bytestream
+ */
+ public Close(String sessionID) {
+ if (sessionID == null || "".equals(sessionID)) {
+ throw new IllegalArgumentException("Session ID must not be null or empty");
+ }
+ this.sessionID = sessionID;
+ setType(Type.SET);
+ }
+
+ /**
+ * Returns the unique session ID identifying this In-Band Bytestream.
+ *
+ * @return the unique session ID identifying this In-Band Bytestream
+ */
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ @Override
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("");
+ return buf.toString();
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/ibb/packet/Data.java b/source/org/jivesoftware/smackx/ibb/packet/Data.java
new file mode 100644
index 000000000..1653be802
--- /dev/null
+++ b/source/org/jivesoftware/smackx/ibb/packet/Data.java
@@ -0,0 +1,64 @@
+/**
+ * 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.ibb.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+
+/**
+ * Represents a chunk of data sent over an In-Band Bytestream encapsulated in an
+ * IQ stanza.
+ *
+ * @author Henning Staib
+ */
+public class Data extends IQ {
+
+ /* the data packet extension */
+ private final DataPacketExtension dataPacketExtension;
+
+ /**
+ * Constructor.
+ *
+ * @param data data packet extension containing the encoded data
+ */
+ public Data(DataPacketExtension data) {
+ if (data == null) {
+ throw new IllegalArgumentException("Data must not be null");
+ }
+ this.dataPacketExtension = data;
+
+ /*
+ * also set as packet extension so that data packet extension can be
+ * retrieved from IQ stanza and message stanza in the same way
+ */
+ addExtension(data);
+ setType(IQ.Type.SET);
+ }
+
+ /**
+ * Returns the data packet extension.
+ *
+ * Convenience method for packet.getExtension("data",
+ * "http://jabber.org/protocol/ibb").
+ *
+ * @return the data packet extension
+ */
+ public DataPacketExtension getDataPacketExtension() {
+ return this.dataPacketExtension;
+ }
+
+ public String getChildElementXML() {
+ return this.dataPacketExtension.toXML();
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/ibb/packet/DataPacketExtension.java b/source/org/jivesoftware/smackx/ibb/packet/DataPacketExtension.java
new file mode 100644
index 000000000..0013843ae
--- /dev/null
+++ b/source/org/jivesoftware/smackx/ibb/packet/DataPacketExtension.java
@@ -0,0 +1,149 @@
+/**
+ * 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.ibb.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.ibb.InBandBytestreamManager;
+
+/**
+ * Represents a chunk of data of an In-Band Bytestream within an IQ stanza or a
+ * message stanza
+ *
+ * @author Henning Staib
+ */
+public class DataPacketExtension implements PacketExtension {
+
+ /**
+ * The element name of the data packet extension.
+ */
+ public final static String ELEMENT_NAME = "data";
+
+ /* unique session ID identifying this In-Band Bytestream */
+ private final String sessionID;
+
+ /* sequence of this packet in regard to the other data packets */
+ private final long seq;
+
+ /* the data contained in this packet */
+ private final String data;
+
+ private byte[] decodedData;
+
+ /**
+ * Creates a new In-Band Bytestream data packet.
+ *
+ * @param sessionID unique session ID identifying this In-Band Bytestream
+ * @param seq sequence of this packet in regard to the other data packets
+ * @param data the base64 encoded data contained in this packet
+ */
+ public DataPacketExtension(String sessionID, long seq, String data) {
+ if (sessionID == null || "".equals(sessionID)) {
+ throw new IllegalArgumentException("Session ID must not be null or empty");
+ }
+ if (seq < 0 || seq > 65535) {
+ throw new IllegalArgumentException("Sequence must not be between 0 and 65535");
+ }
+ if (data == null) {
+ throw new IllegalArgumentException("Data must not be null");
+ }
+ this.sessionID = sessionID;
+ this.seq = seq;
+ this.data = data;
+ }
+
+ /**
+ * Returns the unique session ID identifying this In-Band Bytestream.
+ *
+ * @return the unique session ID identifying this In-Band Bytestream
+ */
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ /**
+ * Returns the sequence of this packet in regard to the other data packets.
+ *
+ * @return the sequence of this packet in regard to the other data packets.
+ */
+ public long getSeq() {
+ return seq;
+ }
+
+ /**
+ * Returns the data contained in this packet.
+ *
+ * @return the data contained in this packet.
+ */
+ public String getData() {
+ return data;
+ }
+
+ /**
+ * Returns the decoded data or null if data could not be decoded.
+ *
+ * The encoded data is invalid if it contains bad Base64 input characters or
+ * if it contains the pad ('=') character on a position other than the last
+ * character(s) of the data. See XEP-0047 Section
+ * 6.
+ *
+ * @return the decoded data
+ */
+ public byte[] getDecodedData() {
+ // return cached decoded data
+ if (this.decodedData != null) {
+ return this.decodedData;
+ }
+
+ // data must not contain the pad (=) other than end of data
+ if (data.matches(".*={1,2}+.+")) {
+ return null;
+ }
+
+ // decodeBase64 will return null if bad characters are included
+ this.decodedData = StringUtils.decodeBase64(data);
+ return this.decodedData;
+ }
+
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getNamespace() {
+ return InBandBytestreamManager.NAMESPACE;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<");
+ buf.append(getElementName());
+ buf.append(" ");
+ buf.append("xmlns=\"");
+ buf.append(InBandBytestreamManager.NAMESPACE);
+ buf.append("\" ");
+ buf.append("seq=\"");
+ buf.append(seq);
+ buf.append("\" ");
+ buf.append("sid=\"");
+ buf.append(sessionID);
+ buf.append("\">");
+ buf.append(data);
+ buf.append("");
+ buf.append(getElementName());
+ buf.append(">");
+ return buf.toString();
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/ibb/packet/Open.java b/source/org/jivesoftware/smackx/ibb/packet/Open.java
new file mode 100644
index 000000000..a77503a73
--- /dev/null
+++ b/source/org/jivesoftware/smackx/ibb/packet/Open.java
@@ -0,0 +1,126 @@
+/**
+ * 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.ibb.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.ibb.InBandBytestreamManager;
+import org.jivesoftware.smackx.ibb.InBandBytestreamManager.StanzaType;
+
+/**
+ * Represents a request to open an In-Band Bytestream.
+ *
+ * @author Henning Staib
+ */
+public class Open extends IQ {
+
+ /* unique session ID identifying this In-Band Bytestream */
+ private final String sessionID;
+
+ /* block size in which the data will be fragmented */
+ private final int blockSize;
+
+ /* stanza type used to encapsulate the data */
+ private final StanzaType stanza;
+
+ /**
+ * Creates a new In-Band Bytestream open request packet.
+ *
+ * The data sent over this In-Band Bytestream will be fragmented in blocks
+ * with the given block size. The block size should not be greater than
+ * 65535. A recommended default value is 4096.
+ *
+ * The data can be sent using IQ stanzas or message stanzas.
+ *
+ * @param sessionID unique session ID identifying this In-Band Bytestream
+ * @param blockSize block size in which the data will be fragmented
+ * @param stanza stanza type used to encapsulate the data
+ */
+ public Open(String sessionID, int blockSize, StanzaType stanza) {
+ if (sessionID == null || "".equals(sessionID)) {
+ throw new IllegalArgumentException("Session ID must not be null or empty");
+ }
+ if (blockSize <= 0) {
+ throw new IllegalArgumentException("Block size must be greater than zero");
+ }
+
+ this.sessionID = sessionID;
+ this.blockSize = blockSize;
+ this.stanza = stanza;
+ setType(Type.SET);
+ }
+
+ /**
+ * Creates a new In-Band Bytestream open request packet.
+ *
+ * The data sent over this In-Band Bytestream will be fragmented in blocks
+ * with the given block size. The block size should not be greater than
+ * 65535. A recommended default value is 4096.
+ *
+ * The data will be sent using IQ stanzas.
+ *
+ * @param sessionID unique session ID identifying this In-Band Bytestream
+ * @param blockSize block size in which the data will be fragmented
+ */
+ public Open(String sessionID, int blockSize) {
+ this(sessionID, blockSize, StanzaType.IQ);
+ }
+
+ /**
+ * Returns the unique session ID identifying this In-Band Bytestream.
+ *
+ * @return the unique session ID identifying this In-Band Bytestream
+ */
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ /**
+ * Returns the block size in which the data will be fragmented.
+ *
+ * @return the block size in which the data will be fragmented
+ */
+ public int getBlockSize() {
+ return blockSize;
+ }
+
+ /**
+ * Returns the stanza type used to encapsulate the data.
+ *
+ * @return the stanza type used to encapsulate the data
+ */
+ public StanzaType getStanza() {
+ return stanza;
+ }
+
+ @Override
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("");
+ return buf.toString();
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiatorManager.java b/source/org/jivesoftware/smackx/ibb/provider/CloseIQProvider.java
similarity index 52%
rename from source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiatorManager.java
rename to source/org/jivesoftware/smackx/ibb/provider/CloseIQProvider.java
index e63c9ebf6..2476b8d1a 100644
--- a/source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiatorManager.java
+++ b/source/org/jivesoftware/smackx/ibb/provider/CloseIQProvider.java
@@ -1,26 +1,33 @@
-/**
- * $Revision:$
- * $Date:$
- *
- * Copyright 2003-2007 Jive Software.
- *
- * 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.filetransfer;
-
-/**
- *
- */
-public interface FileTransferNegotiatorManager {
- StreamNegotiator createNegotiator();
-}
+/**
+ * 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.ibb.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smackx.ibb.packet.Close;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Parses a close In-Band Bytestream packet.
+ *
+ * @author Henning Staib
+ */
+public class CloseIQProvider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ String sid = parser.getAttributeValue("", "sid");
+ return new Close(sid);
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/ibb/provider/DataPacketProvider.java b/source/org/jivesoftware/smackx/ibb/provider/DataPacketProvider.java
new file mode 100644
index 000000000..63fc3126b
--- /dev/null
+++ b/source/org/jivesoftware/smackx/ibb/provider/DataPacketProvider.java
@@ -0,0 +1,45 @@
+/**
+ * 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.ibb.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.ibb.packet.Data;
+import org.jivesoftware.smackx.ibb.packet.DataPacketExtension;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Parses an In-Band Bytestream data packet which can be a packet extension of
+ * either an IQ stanza or a message stanza.
+ *
+ * @author Henning Staib
+ */
+public class DataPacketProvider implements PacketExtensionProvider, IQProvider {
+
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ String sessionID = parser.getAttributeValue("", "sid");
+ long seq = Long.parseLong(parser.getAttributeValue("", "seq"));
+ String data = parser.nextText();
+ return new DataPacketExtension(sessionID, seq, data);
+ }
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ DataPacketExtension data = (DataPacketExtension) parseExtension(parser);
+ IQ iq = new Data(data);
+ return iq;
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/ibb/provider/OpenIQProvider.java b/source/org/jivesoftware/smackx/ibb/provider/OpenIQProvider.java
new file mode 100644
index 000000000..b62d75387
--- /dev/null
+++ b/source/org/jivesoftware/smackx/ibb/provider/OpenIQProvider.java
@@ -0,0 +1,45 @@
+/**
+ * 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.ibb.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smackx.ibb.InBandBytestreamManager.StanzaType;
+import org.jivesoftware.smackx.ibb.packet.Open;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Parses an In-Band Bytestream open packet.
+ *
+ * @author Henning Staib
+ */
+public class OpenIQProvider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ String sessionID = parser.getAttributeValue("", "sid");
+ int blockSize = Integer.parseInt(parser.getAttributeValue("", "block-size"));
+
+ String stanzaValue = parser.getAttributeValue("", "stanza");
+ StanzaType stanza = null;
+ if (stanzaValue == null) {
+ stanza = StanzaType.IQ;
+ }
+ else {
+ stanza = StanzaType.valueOf(stanzaValue.toUpperCase());
+ }
+
+ return new Open(sessionID, blockSize, stanza);
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/packet/IBBExtensions.java b/source/org/jivesoftware/smackx/packet/IBBExtensions.java
deleted file mode 100644
index 3873cd492..000000000
--- a/source/org/jivesoftware/smackx/packet/IBBExtensions.java
+++ /dev/null
@@ -1,241 +0,0 @@
-/**
- * $RCSfile$
- * $Revision: $
- * $Date: $
- *
- * Copyright 2003-2006 Jive Software.
- *
- * 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.packet;
-
-import org.jivesoftware.smack.packet.IQ;
-import org.jivesoftware.smack.packet.PacketExtension;
-
-/**
- * The different extensions used throughtout the negotiation and transfer
- * process.
- *
- * @author Alexander Wenckus
- *
- */
-public class IBBExtensions {
-
- public static final String NAMESPACE = "http://jabber.org/protocol/ibb";
-
- private abstract static class IBB extends IQ {
- final String sid;
-
- private IBB(final String sid) {
- this.sid = sid;
- }
-
- /**
- * Returns the unique stream ID for this file transfer.
- *
- * @return Returns the unique stream ID for this file transfer.
- */
- public String getSessionID() {
- return sid;
- }
-
- public String getNamespace() {
- return NAMESPACE;
- }
- }
-
- /**
- * Represents a request to open the file transfer.
- *
- * @author Alexander Wenckus
- *
- */
- public static class Open extends IBB {
-
- public static final String ELEMENT_NAME = "open";
-
- private final int blockSize;
-
- /**
- * Constructs an open packet.
- *
- * @param sid
- * The streamID of the file transfer.
- * @param blockSize
- * The block size of the file transfer.
- */
- public Open(final String sid, final int blockSize) {
- super(sid);
- this.blockSize = blockSize;
- }
-
- /**
- * The size blocks in which the data will be sent.
- *
- * @return The size blocks in which the data will be sent.
- */
- public int getBlockSize() {
- return blockSize;
- }
-
- public String getElementName() {
- return ELEMENT_NAME;
- }
-
- public String getChildElementXML() {
- StringBuilder buf = new StringBuilder();
- buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append("\" ");
- buf.append("sid=\"").append(getSessionID()).append("\" ");
- buf.append("block-size=\"").append(getBlockSize()).append("\"");
- buf.append("/>");
- return buf.toString();
- }
- }
-
- /**
- * A data packet containing a portion of the file being sent encoded in
- * base64.
- *
- * @author Alexander Wenckus
- *
- */
- public static class Data implements PacketExtension {
-
- private long seq;
-
- private String data;
-
- public static final String ELEMENT_NAME = "data";
-
- final String sid;
-
- /**
- * Returns the unique stream ID identifying this file transfer.
- *
- * @return Returns the unique stream ID identifying this file transfer.
- */
- public String getSessionID() {
- return sid;
- }
-
- public String getNamespace() {
- return NAMESPACE;
- }
-
- /**
- * A constructor.
- *
- * @param sid
- * The stream ID.
- */
- public Data(final String sid) {
- this.sid = sid;
- }
-
- public Data(final String sid, final long seq, final String data) {
- this(sid);
- this.seq = seq;
- this.data = data;
- }
-
- public String getElementName() {
- return ELEMENT_NAME;
- }
-
- /**
- * Returns the data contained in this packet.
- *
- * @return Returns the data contained in this packet.
- */
- public String getData() {
- return data;
- }
-
- /**
- * Sets the data contained in this packet.
- *
- * @param data
- * The data encoded in base65
- */
- public void setData(final String data) {
- this.data = data;
- }
-
- /**
- * Returns the sequence of this packet in regard to the other data
- * packets.
- *
- * @return Returns the sequence of this packet in regard to the other
- * data packets.
- */
- public long getSeq() {
- return seq;
- }
-
- /**
- * Sets the sequence of this packet.
- *
- * @param seq
- * A number between 0 and 65535
- */
- public void setSeq(final long seq) {
- this.seq = seq;
- }
-
- public String toXML() {
- StringBuilder buf = new StringBuilder();
- buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace())
- .append("\" ");
- buf.append("sid=\"").append(getSessionID()).append("\" ");
- buf.append("seq=\"").append(getSeq()).append("\"");
- buf.append(">");
- buf.append(getData());
- buf.append("").append(getElementName()).append(">");
- return buf.toString();
- }
- }
-
- /**
- * Represents the closing of the file transfer.
- *
- *
- * @author Alexander Wenckus
- *
- */
- public static class Close extends IBB {
- public static final String ELEMENT_NAME = "close";
-
- /**
- * The constructor.
- *
- * @param sid
- * The unique stream ID identifying this file transfer.
- */
- public Close(String sid) {
- super(sid);
- }
-
- public String getElementName() {
- return ELEMENT_NAME;
- }
-
- public String getChildElementXML() {
- StringBuilder buf = new StringBuilder();
- buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append("\" ");
- buf.append("sid=\"").append(getSessionID()).append("\"");
- buf.append("/>");
- return buf.toString();
- }
-
- }
-}
diff --git a/source/org/jivesoftware/smackx/provider/BytestreamsProvider.java b/source/org/jivesoftware/smackx/provider/BytestreamsProvider.java
deleted file mode 100644
index b45a97f52..000000000
--- a/source/org/jivesoftware/smackx/provider/BytestreamsProvider.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/**
- * $RCSfile$
- * $Revision: $
- * $Date: $
- *
- * Copyright 2003-2006 Jive Software.
- *
- * 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.provider;
-
-import org.jivesoftware.smack.packet.IQ;
-import org.jivesoftware.smack.provider.IQProvider;
-import org.jivesoftware.smackx.packet.Bytestream;
-import org.xmlpull.v1.XmlPullParser;
-
-/**
- * Parses a bytestream packet.
- *
- * @author Alexander Wenckus
- */
-public class BytestreamsProvider implements IQProvider {
-
- /*
- * (non-Javadoc)
- *
- * @see org.jivesoftware.smack.provider.IQProvider#parseIQ(org.xmlpull.v1.XmlPullParser)
- */
- 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;
- // String namespace;
- while (!done) {
- eventType = parser.next();
- elementName = parser.getName();
- // namespace = parser.getNamespace();
- 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;
- }
-
-}
diff --git a/source/org/jivesoftware/smackx/provider/IBBProviders.java b/source/org/jivesoftware/smackx/provider/IBBProviders.java
deleted file mode 100644
index 522eb8e17..000000000
--- a/source/org/jivesoftware/smackx/provider/IBBProviders.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * $RCSfile$
- * $Revision: $
- * $Date: $
- *
- * Copyright 2003-2006 Jive Software.
- *
- * 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.provider;
-
-import org.jivesoftware.smack.packet.IQ;
-import org.jivesoftware.smack.packet.PacketExtension;
-import org.jivesoftware.smack.provider.IQProvider;
-import org.jivesoftware.smack.provider.PacketExtensionProvider;
-import org.jivesoftware.smackx.packet.IBBExtensions;
-import org.xmlpull.v1.XmlPullParser;
-
-/**
- *
- * Parses an IBB packet.
- *
- * @author Alexander Wenckus
- */
-public class IBBProviders {
-
- /**
- * Parses an open IBB packet.
- *
- * @author Alexander Wenckus
- *
- */
- public static class Open implements IQProvider {
- public IQ parseIQ(XmlPullParser parser) throws Exception {
- final String sid = parser.getAttributeValue("", "sid");
- final int blockSize = Integer.parseInt(parser.getAttributeValue("",
- "block-size"));
-
- return new IBBExtensions.Open(sid, blockSize);
- }
- }
-
- /**
- * Parses a data IBB packet.
- *
- * @author Alexander Wenckus
- *
- */
- public static class Data implements PacketExtensionProvider {
- public PacketExtension parseExtension(XmlPullParser parser)
- throws Exception {
- final String sid = parser.getAttributeValue("", "sid");
- final long seq = Long
- .parseLong(parser.getAttributeValue("", "seq"));
- final String data = parser.nextText();
-
- return new IBBExtensions.Data(sid, seq, data);
- }
- }
-
- /**
- * Parses a close IBB packet.
- *
- * @author Alexander Wenckus
- *
- */
- public static class Close implements IQProvider {
- public IQ parseIQ(XmlPullParser parser) throws Exception {
- final String sid = parser.getAttributeValue("", "sid");
-
- return new IBBExtensions.Close(sid);
- }
- }
-
-}
diff --git a/source/org/jivesoftware/smackx/socks5bytestream/InitiationListener.java b/source/org/jivesoftware/smackx/socks5bytestream/InitiationListener.java
new file mode 100644
index 000000000..130e65bbf
--- /dev/null
+++ b/source/org/jivesoftware/smackx/socks5bytestream/InitiationListener.java
@@ -0,0 +1,119 @@
+/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.socks5bytestream;
+
+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.socks5bytestream.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 <not-acceptable/> error (XEP-0065 Section 5.2.A2).
+ *
+ * @author Henning Staib
+ */
+final class InitiationListener implements PacketListener {
+
+ /* manager containing the listeners and the XMPP connection */
+ private final Socks5BytestreamManager manager;
+
+ /* packet filter for all SOCKS5 Bytestream requests */
+ private final PacketFilter initFilter = new AndFilter(new PacketTypeFilter(Bytestream.class),
+ new IQTypeFilter(IQ.Type.SET));
+
+ /* executor service to process incoming requests concurrently */
+ private final ExecutorService initiationListenerExecutor;
+
+ /**
+ * Constructor
+ *
+ * @param manager the SOCKS5 Bytestream manager
+ */
+ protected InitiationListener(Socks5BytestreamManager manager) {
+ this.manager = manager;
+ initiationListenerExecutor = Executors.newCachedThreadPool();
+ }
+
+ public void processPacket(final Packet packet) {
+ initiationListenerExecutor.execute(new Runnable() {
+
+ public void run() {
+ processRequest(packet);
+ }
+ });
+ }
+
+ private void processRequest(Packet packet) {
+ Bytestream byteStreamRequest = (Bytestream) packet;
+
+ // ignore request if in ignore list
+ if (this.manager.getIgnoredBytestreamRequests().remove(byteStreamRequest.getSessionID())) {
+ return;
+ }
+
+ // build bytestream request from packet
+ Socks5BytestreamRequest request = new Socks5BytestreamRequest(this.manager,
+ byteStreamRequest);
+
+ // notify listeners for bytestream initiation from a specific user
+ BytestreamListener userListener = this.manager.getUserListener(byteStreamRequest.getFrom());
+ if (userListener != null) {
+ userListener.incomingBytestreamRequest(request);
+
+ }
+ else if (!this.manager.getAllRequestListeners().isEmpty()) {
+ /*
+ * if there is no user specific listener inform listeners for all initiation requests
+ */
+ for (BytestreamListener listener : this.manager.getAllRequestListeners()) {
+ listener.incomingBytestreamRequest(request);
+ }
+
+ }
+ else {
+ /*
+ * if there is no listener for this initiation request, reply with reject message
+ */
+ this.manager.replyRejectPacket(byteStreamRequest);
+ }
+ }
+
+ /**
+ * Returns the packet filter for SOCKS5 Bytestream initialization requests.
+ *
+ * @return the packet filter for SOCKS5 Bytestream initialization requests
+ */
+ protected PacketFilter getFilter() {
+ return this.initFilter;
+ }
+
+ /**
+ * Shuts down the listeners executor service.
+ */
+ protected void shutdown() {
+ this.initiationListenerExecutor.shutdownNow();
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/socks5bytestream/Socks5BytestreamListener.java b/source/org/jivesoftware/smackx/socks5bytestream/Socks5BytestreamListener.java
new file mode 100644
index 000000000..a0365010e
--- /dev/null
+++ b/source/org/jivesoftware/smackx/socks5bytestream/Socks5BytestreamListener.java
@@ -0,0 +1,43 @@
+/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.socks5bytestream;
+
+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.
+ *
+ * There are two ways to add this listener. See
+ * {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and
+ * {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)} for
+ * further details.
+ *
+ * @author Henning Staib
+ */
+public abstract class Socks5BytestreamListener implements BytestreamListener {
+
+ public void incomingBytestreamRequest(BytestreamRequest request) {
+ incomingBytestreamRequest((Socks5BytestreamRequest) request);
+ }
+
+ /**
+ * This listener is notified if a SOCKS5 Bytestream request from another user has been received.
+ *
+ * @param request the incoming SOCKS5 Bytestream request
+ */
+ public abstract void incomingBytestreamRequest(Socks5BytestreamRequest request);
+
+}
diff --git a/source/org/jivesoftware/smackx/socks5bytestream/Socks5BytestreamManager.java b/source/org/jivesoftware/smackx/socks5bytestream/Socks5BytestreamManager.java
new file mode 100644
index 000000000..b953e9adb
--- /dev/null
+++ b/source/org/jivesoftware/smackx/socks5bytestream/Socks5BytestreamManager.java
@@ -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.socks5bytestream;
+
+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.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;
+import org.jivesoftware.smackx.socks5bytestream.packet.Bytestream;
+import org.jivesoftware.smackx.socks5bytestream.packet.Bytestream.StreamHost;
+import org.jivesoftware.smackx.socks5bytestream.packet.Bytestream.StreamHostUsed;
+
+/**
+ * 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 managers = new HashMap();
+
+ /* 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 userListeners = new ConcurrentHashMap();
+
+ /*
+ * list of listeners that respond to all bytestream requests if there are not user specific
+ * listeners for that request
+ */
+ private final List allRequestListeners = Collections.synchronizedList(new LinkedList());
+
+ /* 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 proxyBlacklist = Collections.synchronizedList(new LinkedList());
+
+ /* 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 ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList());
+
+ /**
+ * Returns the Socks5BytestreamManager to handle SOCKS5 Bytestreams for a given
+ * {@link Connection}.
+ *
+ * If no manager exists a new is created and initialized.
+ *
+ * @param connection the XMPP connection or null 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.
+ *
+ * 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 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.
+ *
+ * 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
+ List proxies = determineProxies();
+
+ // determine address and port of each proxy
+ List 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 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 determineProxies() throws XMPPException {
+ ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
+
+ List proxies = new ArrayList();
+
+ // get all items form XMPP server
+ DiscoverItems discoverItems = serviceDiscoveryManager.discoverItems(this.connection.getServiceName());
+ Iterator 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 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 determineStreamHostInfos(List proxies) {
+ List streamHosts = new ArrayList();
+
+ // add local proxy on first position if exists
+ List 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 getLocalStreamHost() {
+
+ // get local proxy singleton
+ Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy();
+
+ if (socks5Server.isRunning()) {
+ List addresses = socks5Server.getLocalAddresses();
+ int port = socks5Server.getPort();
+
+ if (addresses.size() >= 1) {
+ List streamHosts = new ArrayList();
+ 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 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 getAllRequestListeners() {
+ return this.allRequestListeners;
+ }
+
+ /**
+ * Returns the list of session IDs that should be ignored by the InitialtionListener
+ *
+ * @return list of session IDs
+ */
+ protected List getIgnoredBytestreamRequests() {
+ return ignoredBytestreamRequests;
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/socks5bytestream/Socks5BytestreamRequest.java b/source/org/jivesoftware/smackx/socks5bytestream/Socks5BytestreamRequest.java
new file mode 100644
index 000000000..740bcc2ac
--- /dev/null
+++ b/source/org/jivesoftware/smackx/socks5bytestream/Socks5BytestreamRequest.java
@@ -0,0 +1,316 @@
+/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.socks5bytestream;
+
+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.socks5bytestream.packet.Bytestream;
+import org.jivesoftware.smackx.socks5bytestream.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 ADDRESS_BLACKLIST = new Cache(
+ 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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 streamHosts = this.bytestreamRequest.getStreamHosts();
+
+ // throw exceptions if request contains no stream hosts
+ if (streamHosts.size() == 0) {
+ cancelRequest();
+ }
+
+ StreamHost selectedHost = null;
+ Socket socket = null;
+
+ String digest = Socks5Utils.createDigest(this.bytestreamRequest.getSessionID(),
+ this.bytestreamRequest.getFrom(), this.manager.getConnection().getUser());
+
+ /*
+ * determine timeout for each connection attempt; each SOCKS5 proxy has the same amount of
+ * time so that the first does not consume the whole timeout
+ */
+ int timeout = Math.max(getTotalConnectTimeout() / streamHosts.size(),
+ getMinimumConnectTimeout());
+
+ for (StreamHost streamHost : streamHosts) {
+ String address = streamHost.getAddress() + ":" + streamHost.getPort();
+
+ // check to see if this address has been blacklisted
+ int failures = getConnectionFailures(address);
+ if (CONNECTION_FAILURE_THRESHOLD > 0 && failures >= CONNECTION_FAILURE_THRESHOLD) {
+ continue;
+ }
+
+ // establish socket
+ try {
+
+ // build SOCKS5 client
+ final Socks5Client socks5Client = new Socks5Client(streamHost, digest);
+
+ // connect to SOCKS5 proxy with a timeout
+ socket = socks5Client.getSocket(timeout);
+
+ // set selected host
+ selectedHost = streamHost;
+ break;
+
+ }
+ catch (TimeoutException e) {
+ incrementConnectionFailures(address);
+ }
+ catch (IOException e) {
+ incrementConnectionFailures(address);
+ }
+ catch (XMPPException e) {
+ incrementConnectionFailures(address);
+ }
+
+ }
+
+ // throw exception if connecting to all SOCKS5 proxies failed
+ if (selectedHost == null || socket == null) {
+ cancelRequest();
+ }
+
+ // send used-host confirmation
+ Bytestream response = createUsedHostResponse(selectedHost);
+ this.manager.getConnection().sendPacket(response);
+
+ return new Socks5BytestreamSession(socket, selectedHost.getJID().equals(
+ this.bytestreamRequest.getFrom()));
+
+ }
+
+ /**
+ * Rejects the SOCKS5 Bytestream request by sending a reject error to the initiator.
+ */
+ public void reject() {
+ this.manager.replyRejectPacket(this.bytestreamRequest);
+ }
+
+ /**
+ * Cancels the SOCKS5 Bytestream request by sending an error to the initiator and building a
+ * XMPP exception.
+ *
+ * @throws XMPPException XMPP exception containing the XMPP error
+ */
+ private void cancelRequest() throws XMPPException {
+ String errorMessage = "Could not establish socket with any provided host";
+ XMPPError error = new XMPPError(XMPPError.Condition.item_not_found, errorMessage);
+ IQ errorIQ = IQ.createErrorResponse(this.bytestreamRequest, error);
+ this.manager.getConnection().sendPacket(errorIQ);
+ throw new XMPPException(errorMessage, error);
+ }
+
+ /**
+ * Returns the response to the SOCKS5 Bytestream request containing the SOCKS5 proxy used.
+ *
+ * @param selectedHost the used SOCKS5 proxy
+ * @return the response to the SOCKS5 Bytestream request
+ */
+ private Bytestream createUsedHostResponse(StreamHost selectedHost) {
+ Bytestream response = new Bytestream(this.bytestreamRequest.getSessionID());
+ response.setTo(this.bytestreamRequest.getFrom());
+ response.setType(IQ.Type.RESULT);
+ response.setPacketID(this.bytestreamRequest.getPacketID());
+ response.setUsedHost(selectedHost.getJID());
+ return response;
+ }
+
+ /**
+ * Increments the connection failure counter by one for the given address.
+ *
+ * @param address the address the connection failure counter should be increased
+ */
+ private void incrementConnectionFailures(String address) {
+ Integer count = ADDRESS_BLACKLIST.get(address);
+ ADDRESS_BLACKLIST.put(address, count == null ? 1 : count + 1);
+ }
+
+ /**
+ * Returns how often the connection to the given address failed.
+ *
+ * @param address the address
+ * @return number of connection failures
+ */
+ private int getConnectionFailures(String address) {
+ Integer count = ADDRESS_BLACKLIST.get(address);
+ return count != null ? count : 0;
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/socks5bytestream/Socks5BytestreamSession.java b/source/org/jivesoftware/smackx/socks5bytestream/Socks5BytestreamSession.java
new file mode 100644
index 000000000..770a1f4b8
--- /dev/null
+++ b/source/org/jivesoftware/smackx/socks5bytestream/Socks5BytestreamSession.java
@@ -0,0 +1,81 @@
+package org.jivesoftware.smackx.socks5bytestream;
+
+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 true if the session is established through a direct connection between
+ * the initiator and target, false if the session is mediated over a SOCKS proxy.
+ *
+ * @return true if session is a direct connection, false if session is
+ * mediated over a SOCKS5 proxy
+ */
+ public boolean isDirect() {
+ return this.isDirect;
+ }
+
+ /**
+ * Returns true if the session is mediated over a SOCKS proxy, false
+ * if this session is established through a direct connection between the initiator and target.
+ *
+ * @return true if session is mediated over a SOCKS5 proxy, false if
+ * session is a direct connection
+ */
+ public boolean isMediated() {
+ return !this.isDirect;
+ }
+
+ public InputStream getInputStream() throws IOException {
+ return this.socket.getInputStream();
+ }
+
+ public OutputStream getOutputStream() throws IOException {
+ return this.socket.getOutputStream();
+ }
+
+ public int getReadTimeout() throws IOException {
+ try {
+ return this.socket.getSoTimeout();
+ }
+ catch (SocketException e) {
+ throw new IOException("Error on underlying Socket");
+ }
+ }
+
+ public void setReadTimeout(int timeout) throws IOException {
+ try {
+ this.socket.setSoTimeout(timeout);
+ }
+ catch (SocketException e) {
+ throw new IOException("Error on underlying Socket");
+ }
+ }
+
+ public void close() throws IOException {
+ this.socket.close();
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/socks5bytestream/Socks5Client.java b/source/org/jivesoftware/smackx/socks5bytestream/Socks5Client.java
new file mode 100644
index 000000000..28c3a58c3
--- /dev/null
+++ b/source/org/jivesoftware/smackx/socks5bytestream/Socks5Client.java
@@ -0,0 +1,204 @@
+/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.socks5bytestream;
+
+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.socks5bytestream.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 futureTask = new FutureTask(new Callable() {
+
+ 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.
+ *
+ * Returns true if a stream could be established, otherwise false. If
+ * false is returned the given Socket should be closed.
+ *
+ * @param socket connected to a SOCKS5 proxy
+ * @return true if if a stream could be established, otherwise false.
+ * If false 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.
+ *
+ * (see RFC1928)
+ *
+ * @return SOCKS5 connection request message
+ */
+ private byte[] createSocks5ConnectRequest() {
+ byte addr[] = this.digest.getBytes();
+
+ byte[] data = new byte[7 + addr.length];
+ data[0] = (byte) 0x05; // version (SOCKS5)
+ data[1] = (byte) 0x01; // command (1 - connect)
+ data[2] = (byte) 0x00; // reserved byte (always 0)
+ data[3] = (byte) 0x03; // address type (3 - domain name)
+ data[4] = (byte) addr.length; // address length
+ System.arraycopy(addr, 0, data, 5, addr.length); // address
+ data[data.length - 2] = (byte) 0; // address port (2 bytes always 0)
+ data[data.length - 1] = (byte) 0;
+
+ return data;
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/socks5bytestream/Socks5ClientForInitiator.java b/source/org/jivesoftware/smackx/socks5bytestream/Socks5ClientForInitiator.java
new file mode 100644
index 000000000..d8a69186c
--- /dev/null
+++ b/source/org/jivesoftware/smackx/socks5bytestream/Socks5ClientForInitiator.java
@@ -0,0 +1,117 @@
+/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.socks5bytestream;
+
+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.packet.SyncPacketSend;
+import org.jivesoftware.smackx.socks5bytestream.packet.Bytestream;
+import org.jivesoftware.smackx.socks5bytestream.packet.Bytestream.StreamHost;
+
+/**
+ * Implementation of a SOCKS5 client used on the initiators side. This is needed because connecting
+ * to the local SOCKS5 proxy differs form the regular way to connect to a SOCKS5 proxy. Additionally
+ * a remote SOCKS5 proxy has to be activated by the initiator before data can be transferred between
+ * the peers.
+ *
+ * @author Henning Staib
+ */
+class Socks5ClientForInitiator extends Socks5Client {
+
+ /* the XMPP connection used to communicate with the SOCKS5 proxy */
+ private Connection connection;
+
+ /* the session ID used to activate SOCKS5 stream */
+ private String sessionID;
+
+ /* the target JID used to activate SOCKS5 stream */
+ private String target;
+
+ /**
+ * Creates a new SOCKS5 client for the initiators side.
+ *
+ * @param streamHost containing network settings of the SOCKS5 proxy
+ * @param digest identifying the SOCKS5 Bytestream
+ * @param connection the XMPP connection
+ * @param sessionID the session ID of the SOCKS5 Bytestream
+ * @param target the target JID of the SOCKS5 Bytestream
+ */
+ public Socks5ClientForInitiator(StreamHost streamHost, String digest, Connection connection,
+ String sessionID, String target) {
+ super(streamHost, digest);
+ this.connection = connection;
+ this.sessionID = sessionID;
+ this.target = target;
+ }
+
+ public Socket getSocket(int timeout) throws IOException, XMPPException, InterruptedException,
+ TimeoutException {
+ Socket socket = null;
+
+ // check if stream host is the local SOCKS5 proxy
+ if (this.streamHost.getJID().equals(this.connection.getUser())) {
+ Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy();
+ socket = socks5Server.getSocket(this.digest);
+ if (socket == null) {
+ throw new XMPPException("target is not connected to SOCKS5 proxy");
+ }
+ }
+ else {
+ socket = super.getSocket(timeout);
+
+ try {
+ activate();
+ }
+ catch (XMPPException e) {
+ socket.close();
+ throw new XMPPException("activating SOCKS5 Bytestream failed", e);
+ }
+
+ }
+
+ return socket;
+ }
+
+ /**
+ * Activates the SOCKS5 Bytestream by sending a XMPP SOCKS5 Bytestream activation packet to the
+ * SOCKS5 proxy.
+ */
+ private void activate() throws XMPPException {
+ Bytestream activate = createStreamHostActivation();
+ // if activation fails #getReply throws an exception
+ SyncPacketSend.getReply(this.connection, activate);
+ }
+
+ /**
+ * Returns a SOCKS5 Bytestream activation packet.
+ *
+ * @return SOCKS5 Bytestream activation packet
+ */
+ private Bytestream createStreamHostActivation() {
+ Bytestream activate = new Bytestream(this.sessionID);
+ activate.setMode(null);
+ activate.setType(IQ.Type.SET);
+ activate.setTo(this.streamHost.getJID());
+
+ activate.setToActivate(this.target);
+
+ return activate;
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/socks5bytestream/Socks5Proxy.java b/source/org/jivesoftware/smackx/socks5bytestream/Socks5Proxy.java
new file mode 100644
index 000000000..b2b26c330
--- /dev/null
+++ b/source/org/jivesoftware/smackx/socks5bytestream/Socks5Proxy.java
@@ -0,0 +1,423 @@
+/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.socks5bytestream;
+
+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 localSocks5ProxyEnabled flag in the smack-config.xml or by
+ * invoking {@link SmackConfiguration#setLocalSocks5ProxyEnabled(boolean)}. The proxy is enabled by
+ * default.
+ *
+ * The port of the local SOCKS5 proxy can be configured by setting localSocks5ProxyPort
+ * in the smack-config.xml 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.
+ *
+ * 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.
+ *
+ * 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)}).
+ *
+ * This Implementation has the following limitations:
+ *
+ *
only supports the no-authentication authentication method
+ *
only supports the connect command and will not answer correctly to other
+ * commands
+ *
only supports requests with the domain address type and will not correctly answer to requests
+ * with other address types
+ *
+ * (see RFC 1928)
+ *
+ * @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 connectionMap = new ConcurrentHashMap();
+
+ /* list of digests connections should be stored */
+ private final List allowedConnections = Collections.synchronizedList(new LinkedList());
+
+ private final Set localAddresses = Collections.synchronizedSet(new LinkedHashSet());
+
+ /**
+ * 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.
+ *
+ * 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.
+ *
+ * The order of the addresses used is determined by the order you add addresses.
+ *
+ * Note that the list of addresses initially contains the address returned by
+ * InetAddress.getLocalHost().getHostAddress(). 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 getLocalAddresses() {
+ return Collections.unmodifiableList(new ArrayList(this.localAddresses));
+ }
+
+ /**
+ * Replaces the list of local network addresses.
+ *
+ * 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 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.
+ *
+ * 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 true if the local SOCKS5 proxy server is running, otherwise
+ * false.
+ *
+ * @return true if the local SOCKS5 proxy server is running, otherwise
+ * false
+ */
+ public boolean isRunning() {
+ return this.serverSocket != null;
+ }
+
+ /**
+ * Implementation of a simplified SOCKS5 proxy server.
+ */
+ private class Socks5ServerProcess implements Runnable {
+
+ public void run() {
+ while (true) {
+ Socket socket = null;
+
+ try {
+
+ if (Socks5Proxy.this.serverSocket.isClosed()
+ || Thread.currentThread().isInterrupted()) {
+ return;
+ }
+
+ // accept connection
+ socket = Socks5Proxy.this.serverSocket.accept();
+
+ // initialize connection
+ establishConnection(socket);
+
+ }
+ catch (SocketException e) {
+ /*
+ * do nothing, if caused by closing the server socket, thread will terminate in
+ * next loop
+ */
+ }
+ catch (Exception e) {
+ try {
+ if (socket != null) {
+ socket.close();
+ }
+ }
+ catch (IOException e1) {
+ /* do nothing */
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Negotiates a SOCKS5 connection and stores it on success.
+ *
+ * @param socket connection to the client
+ * @throws XMPPException if client requests a connection in an unsupported way
+ * @throws IOException if a network error occurred
+ */
+ private void establishConnection(Socket socket) throws XMPPException, IOException {
+ DataOutputStream out = new DataOutputStream(socket.getOutputStream());
+ DataInputStream in = new DataInputStream(socket.getInputStream());
+
+ // first byte is version should be 5
+ int b = in.read();
+ if (b != 5) {
+ throw new XMPPException("Only SOCKS5 supported");
+ }
+
+ // second byte number of authentication methods supported
+ b = in.read();
+
+ // read list of supported authentication methods
+ byte[] auth = new byte[b];
+ in.readFully(auth);
+
+ byte[] authMethodSelectionResponse = new byte[2];
+ authMethodSelectionResponse[0] = (byte) 0x05; // protocol version
+
+ // only authentication method 0, no authentication, supported
+ boolean noAuthMethodFound = false;
+ for (int i = 0; i < auth.length; i++) {
+ if (auth[i] == (byte) 0x00) {
+ noAuthMethodFound = true;
+ break;
+ }
+ }
+
+ if (!noAuthMethodFound) {
+ authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods
+ out.write(authMethodSelectionResponse);
+ out.flush();
+ throw new XMPPException("Authentication method not supported");
+ }
+
+ authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method
+ out.write(authMethodSelectionResponse);
+ out.flush();
+
+ // receive connection request
+ byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in);
+
+ // extract digest
+ String responseDigest = new String(connectionRequest, 5, connectionRequest[4]);
+
+ // return error if digest is not allowed
+ if (!Socks5Proxy.this.allowedConnections.contains(responseDigest)) {
+ connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused)
+ out.write(connectionRequest);
+ out.flush();
+
+ throw new XMPPException("Connection is not allowed");
+ }
+
+ connectionRequest[1] = (byte) 0x00; // set return status to 0 (success)
+ out.write(connectionRequest);
+ out.flush();
+
+ // store connection
+ Socks5Proxy.this.connectionMap.put(responseDigest, socket);
+ }
+
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/socks5bytestream/Socks5Utils.java b/source/org/jivesoftware/smackx/socks5bytestream/Socks5Utils.java
new file mode 100644
index 000000000..33485900a
--- /dev/null
+++ b/source/org/jivesoftware/smackx/socks5bytestream/Socks5Utils.java
@@ -0,0 +1,73 @@
+/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.socks5bytestream;
+
+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 XEP-0065.
+ *
+ * @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.
+ *
+ * (see RFC1928)
+ *
+ * @param in the DataInputStream to read the message from
+ * @return the SOCKS5 message
+ * @throws IOException if a network error occurred
+ * @throws XMPPException if the SOCKS5 message contains an unsupported address type
+ */
+ public static byte[] receiveSocks5Message(DataInputStream in) throws IOException, XMPPException {
+ byte[] header = new byte[5];
+ in.readFully(header, 0, 5);
+
+ if (header[3] != (byte) 0x03) {
+ throw new XMPPException("Unsupported SOCKS5 address type");
+ }
+
+ int addressLength = header[4];
+
+ byte[] response = new byte[7 + addressLength];
+ System.arraycopy(header, 0, response, 0, header.length);
+
+ in.readFully(response, header.length, addressLength + 2);
+
+ return response;
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/packet/Bytestream.java b/source/org/jivesoftware/smackx/socks5bytestream/packet/Bytestream.java
similarity index 73%
rename from source/org/jivesoftware/smackx/packet/Bytestream.java
rename to source/org/jivesoftware/smackx/socks5bytestream/packet/Bytestream.java
index d37d9f6fb..6d255720c 100644
--- a/source/org/jivesoftware/smackx/packet/Bytestream.java
+++ b/source/org/jivesoftware/smackx/socks5bytestream/packet/Bytestream.java
@@ -1,10 +1,4 @@
/**
- * $RCSfile$
- * $Revision: $
- * $Date: $
- *
- * Copyright 2003-2006 Jive Software.
- *
* 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
@@ -17,16 +11,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.jivesoftware.smackx.packet;
+package org.jivesoftware.smackx.socks5bytestream.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;
-import java.util.*;
-
/**
- * A packet representing part of a Socks5 Bytestream negotiation.
- *
+ * A packet representing part of a SOCKS5 Bytestream negotiation.
+ *
* @author Alexander Wenckus
*/
public class Bytestream extends IQ {
@@ -50,7 +47,7 @@ public class Bytestream extends IQ {
/**
* A constructor where the session ID can be specified.
- *
+ *
* @param SID The session ID related to the negotiation.
* @see #setSessionID(String)
*/
@@ -60,9 +57,9 @@ public class Bytestream extends IQ {
}
/**
- * Set the session ID related to the Byte Stream. The session ID is a unique
- * identifier used to differentiate between stream negotations.
- *
+ * 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) {
@@ -70,9 +67,9 @@ public class Bytestream extends IQ {
}
/**
- * Returns the session ID related to the Byte Stream negotiation.
- *
- * @return Returns the session ID related to the Byte Stream negotiation.
+ * 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() {
@@ -80,9 +77,8 @@ public class Bytestream extends IQ {
}
/**
- * Set the transport mode. This should be put in the initiation of the
- * interaction.
- *
+ * 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
*/
@@ -92,7 +88,7 @@ public class Bytestream extends IQ {
/**
* Returns the transport mode.
- *
+ *
* @return Returns the transport mode.
* @see #setMode(Mode)
*/
@@ -101,10 +97,9 @@ public class Bytestream extends IQ {
}
/**
- * Adds a potential stream host that the remote user can connect to to
- * receive the file.
- *
- * @param JID The jabber ID of the stream host.
+ * 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.
*/
@@ -113,16 +108,14 @@ public class Bytestream extends IQ {
}
/**
- * Adds a potential stream host that the remote user can connect to to
- * receive the file.
- *
- * @param JID The jabber ID of the stream host.
+ * 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.
+ * @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) {
+ public StreamHost addStreamHost(final String JID, final String address, final int port) {
StreamHost host = new StreamHost(JID, address);
host.setPort(port);
addStreamHost(host);
@@ -131,9 +124,8 @@ public class Bytestream extends IQ {
}
/**
- * Adds a potential stream host that the remote user can transfer the file
- * through.
- *
+ * 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) {
@@ -142,7 +134,7 @@ public class Bytestream extends IQ {
/**
* Returns the list of stream hosts contained in the packet.
- *
+ *
* @return Returns the list of stream hosts contained in the packet.
*/
public Collection getStreamHosts() {
@@ -150,15 +142,13 @@ public class Bytestream extends IQ {
}
/**
- * Returns the stream host related to the given jabber ID, or null if there
- * is none.
- *
- * @param JID The jabber ID of the desired stream host.
- * @return Returns the stream host related to the given jabber ID, or null
- * if there is none.
+ * 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) {
+ if (JID == null) {
return null;
}
for (StreamHost host : streamHosts) {
@@ -172,7 +162,7 @@ public class Bytestream extends IQ {
/**
* Returns the count of stream hosts contained in this packet.
- *
+ *
* @return Returns the count of stream hosts contained in this packet.
*/
public int countStreamHosts() {
@@ -180,44 +170,41 @@ public class Bytestream extends IQ {
}
/**
- * Upon connecting to the stream host the target of the stream replys to the
- * initiator with the jabber id of the Socks5 host that they used.
- *
- * @param JID The jabber ID of the used host.
+ * 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.
+ * 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.
+ * 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 jabber ID of the target of the file transfer.
+ * 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);
@@ -228,10 +215,12 @@ public class Bytestream extends IQ {
buf.append("");
if (getToActivate() == null) {
for (StreamHost streamHost : getStreamHosts()) {
@@ -244,8 +233,9 @@ public class Bytestream extends IQ {
}
else if (this.getType().equals(IQ.Type.RESULT)) {
buf.append(">");
- if (getUsedHost() != null)
+ 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) {
@@ -253,6 +243,9 @@ public class Bytestream extends IQ {
}
}
}
+ else if (this.getType().equals(IQ.Type.GET)) {
+ return buf.append("/>").toString();
+ }
else {
return null;
}
@@ -262,10 +255,9 @@ public class Bytestream extends IQ {
}
/**
- * Packet extension that represents a potential Socks5 proxy for the file
- * transfer. Stream hosts are forwared to the target of the file transfer
- * who then chooses and connects to one.
- *
+ * 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 {
@@ -282,8 +274,8 @@ public class Bytestream extends IQ {
/**
* Default constructor.
- *
- * @param JID The jabber ID of the stream host.
+ *
+ * @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) {
@@ -292,9 +284,9 @@ public class Bytestream extends IQ {
}
/**
- * Returns the jabber ID of the stream host.
- *
- * @return Returns the jabber ID of the stream host.
+ * Returns the JID of the stream host.
+ *
+ * @return Returns the JID of the stream host.
*/
public String getJID() {
return JID;
@@ -302,7 +294,7 @@ public class Bytestream extends IQ {
/**
* Returns the internet address of the stream host.
- *
+ *
* @return Returns the internet address of the stream host.
*/
public String getAddress() {
@@ -311,20 +303,17 @@ public class Bytestream extends IQ {
/**
* Sets the port of the stream host.
- *
- * @param port The port on which the potential stream host would accept
- * the connection.
+ *
+ * @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.
+ * 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;
@@ -344,10 +333,12 @@ public class Bytestream extends IQ {
buf.append("<").append(getElementName()).append(" ");
buf.append("jid=\"").append(getJID()).append("\" ");
buf.append("host=\"").append(getAddress()).append("\" ");
- if (getPort() != 0)
+ if (getPort() != 0) {
buf.append("port=\"").append(getPort()).append("\"");
- else
+ }
+ else {
buf.append("zeroconf=\"_jabber.bytestreams\"");
+ }
buf.append("/>");
return buf.toString();
@@ -355,10 +346,9 @@ public class Bytestream extends IQ {
}
/**
- * 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.
- *
+ * 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 {
@@ -371,17 +361,17 @@ public class Bytestream extends IQ {
/**
* Default constructor.
- *
- * @param JID The jabber ID of the selected stream host.
+ *
+ * @param JID The JID of the selected stream host.
*/
public StreamHostUsed(final String JID) {
this.JID = JID;
}
/**
- * Returns the jabber ID of the selected stream host.
- *
- * @return Returns the jabber ID of the selected stream host.
+ * Returns the JID of the selected stream host.
+ *
+ * @return Returns the JID of the selected stream host.
*/
public String getJID() {
return JID;
@@ -405,9 +395,8 @@ public class Bytestream extends IQ {
}
/**
- * The packet sent by the stream initiator to the stream proxy to activate
- * the connection.
- *
+ * The packet sent by the stream initiator to the stream proxy to activate the connection.
+ *
* @author Alexander Wenckus
*/
public static class Activate implements PacketExtension {
@@ -420,7 +409,7 @@ public class Bytestream extends IQ {
/**
* Default constructor specifying the target of the stream.
- *
+ *
* @param target The target of the stream.
*/
public Activate(final String target) {
@@ -429,7 +418,7 @@ public class Bytestream extends IQ {
/**
* Returns the target of the activation.
- *
+ *
* @return Returns the target of the activation.
*/
public String getTarget() {
@@ -455,7 +444,7 @@ public class Bytestream extends IQ {
/**
* The stream can be either a TCP stream or a UDP stream.
- *
+ *
* @author Alexander Wenckus
*/
public enum Mode {
@@ -475,7 +464,7 @@ public class Bytestream extends IQ {
try {
mode = Mode.valueOf(name);
}
- catch(Exception ex) {
+ catch (Exception ex) {
mode = tcp;
}
diff --git a/source/org/jivesoftware/smackx/socks5bytestream/provider/BytestreamsProvider.java b/source/org/jivesoftware/smackx/socks5bytestream/provider/BytestreamsProvider.java
new file mode 100644
index 000000000..4b9af1404
--- /dev/null
+++ b/source/org/jivesoftware/smackx/socks5bytestream/provider/BytestreamsProvider.java
@@ -0,0 +1,82 @@
+/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.socks5bytestream.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smackx.socks5bytestream.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;
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/ibb/CloseListenerTest.java b/test-unit/org/jivesoftware/smackx/ibb/CloseListenerTest.java
new file mode 100644
index 000000000..eb17aa225
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/ibb/CloseListenerTest.java
@@ -0,0 +1,77 @@
+/**
+ * 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.ibb;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smackx.ibb.packet.Close;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.powermock.reflect.Whitebox;
+
+/**
+ * Test for the CloseListener class.
+ *
+ * @author Henning Staib
+ */
+public class CloseListenerTest {
+
+ String initiatorJID = "initiator@xmpp-server/Smack";
+ String targetJID = "target@xmpp-server/Smack";
+
+ /**
+ * If a close request to an unknown session is received it should be replied
+ * with an <item-not-found/> error.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldReplyErrorIfSessionIsUnknown() throws Exception {
+
+ // mock connection
+ Connection connection = mock(Connection.class);
+
+ // initialize InBandBytestreamManager to get the CloseListener
+ InBandBytestreamManager byteStreamManager = InBandBytestreamManager.getByteStreamManager(connection);
+
+ // get the CloseListener from InBandByteStreamManager
+ CloseListener closeListener = Whitebox.getInternalState(byteStreamManager,
+ CloseListener.class);
+
+ Close close = new Close("unknownSessionId");
+ close.setFrom(initiatorJID);
+ close.setTo(targetJID);
+
+ closeListener.processPacket(close);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // capture reply to the In-Band Bytestream close request
+ ArgumentCaptor argument = ArgumentCaptor.forClass(IQ.class);
+ verify(connection).sendPacket(argument.capture());
+
+ // assert that reply is the correct error packet
+ assertEquals(initiatorJID, argument.getValue().getTo());
+ assertEquals(IQ.Type.ERROR, argument.getValue().getType());
+ assertEquals(XMPPError.Condition.item_not_found.toString(),
+ argument.getValue().getError().getCondition());
+
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/ibb/DataListenerTest.java b/test-unit/org/jivesoftware/smackx/ibb/DataListenerTest.java
new file mode 100644
index 000000000..eb876817f
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/ibb/DataListenerTest.java
@@ -0,0 +1,79 @@
+/**
+ * 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.ibb;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smackx.ibb.packet.Data;
+import org.jivesoftware.smackx.ibb.packet.DataPacketExtension;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.powermock.reflect.Whitebox;
+
+/**
+ * Test for the CloseListener class.
+ *
+ * @author Henning Staib
+ */
+public class DataListenerTest {
+
+ String initiatorJID = "initiator@xmpp-server/Smack";
+ String targetJID = "target@xmpp-server/Smack";
+
+ /**
+ * If a data packet of an unknown session is received it should be replied
+ * with an <item-not-found/> error.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldReplyErrorIfSessionIsUnknown() throws Exception {
+
+ // mock connection
+ Connection connection = mock(Connection.class);
+
+ // initialize InBandBytestreamManager to get the DataListener
+ InBandBytestreamManager byteStreamManager = InBandBytestreamManager.getByteStreamManager(connection);
+
+ // get the DataListener from InBandByteStreamManager
+ DataListener dataListener = Whitebox.getInternalState(byteStreamManager,
+ DataListener.class);
+
+ DataPacketExtension dpe = new DataPacketExtension("unknownSessionID", 0, "Data");
+ Data data = new Data(dpe);
+ data.setFrom(initiatorJID);
+ data.setTo(targetJID);
+
+ dataListener.processPacket(data);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // capture reply to the In-Band Bytestream close request
+ ArgumentCaptor argument = ArgumentCaptor.forClass(IQ.class);
+ verify(connection).sendPacket(argument.capture());
+
+ // assert that reply is the correct error packet
+ assertEquals(initiatorJID, argument.getValue().getTo());
+ assertEquals(IQ.Type.ERROR, argument.getValue().getType());
+ assertEquals(XMPPError.Condition.item_not_found.toString(),
+ argument.getValue().getError().getCondition());
+
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/ibb/IBBPacketUtils.java b/test-unit/org/jivesoftware/smackx/ibb/IBBPacketUtils.java
new file mode 100644
index 000000000..162d889b4
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/ibb/IBBPacketUtils.java
@@ -0,0 +1,57 @@
+package org.jivesoftware.smackx.ibb;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.XMPPError;
+
+/**
+ * Utility methods to create packets.
+ *
+ * @author Henning Staib
+ */
+public class IBBPacketUtils {
+
+ /**
+ * Returns an error IQ.
+ *
+ * @param from the senders JID
+ * @param to the recipients JID
+ * @param xmppError the XMPP error
+ * @return an error IQ
+ */
+ public static IQ createErrorIQ(String from, String to, XMPPError xmppError) {
+ IQ errorIQ = new IQ() {
+
+ public String getChildElementXML() {
+ return null;
+ }
+
+ };
+ errorIQ.setType(IQ.Type.ERROR);
+ errorIQ.setFrom(from);
+ errorIQ.setTo(to);
+ errorIQ.setError(xmppError);
+ return errorIQ;
+ }
+
+ /**
+ * Returns a result IQ.
+ *
+ * @param from the senders JID
+ * @param to the recipients JID
+ * @return a result IQ
+ */
+ public static IQ createResultIQ(String from, String to) {
+ IQ result = new IQ() {
+
+ public String getChildElementXML() {
+ return null;
+ }
+
+ };
+ result.setType(IQ.Type.RESULT);
+ result.setFrom(from);
+ result.setTo(to);
+ return result;
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/ibb/IBBTestsSuite.java b/test-unit/org/jivesoftware/smackx/ibb/IBBTestsSuite.java
new file mode 100644
index 000000000..3068664b1
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/ibb/IBBTestsSuite.java
@@ -0,0 +1,21 @@
+package org.jivesoftware.smackx.ibb;
+
+import org.jivesoftware.smackx.ibb.packet.CloseTest;
+import org.jivesoftware.smackx.ibb.packet.DataPacketExtensionTest;
+import org.jivesoftware.smackx.ibb.packet.DataTest;
+import org.jivesoftware.smackx.ibb.packet.OpenTest;
+import org.jivesoftware.smackx.ibb.provider.OpenIQProviderTest;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses( { CloseTest.class, DataPacketExtensionTest.class, DataTest.class,
+ OpenTest.class, OpenIQProviderTest.class, CloseListenerTest.class,
+ DataListenerTest.class, InBandBytestreamManagerTest.class,
+ InBandBytestreamRequestTest.class,
+ InBandBytestreamSessionMessageTest.class,
+ InBandBytestreamSessionTest.class, InitiationListenerTest.class })
+public class IBBTestsSuite {
+ // the class remains completely empty,
+ // being used only as a holder for the above annotations
+}
diff --git a/test-unit/org/jivesoftware/smackx/ibb/InBandBytestreamManagerTest.java b/test-unit/org/jivesoftware/smackx/ibb/InBandBytestreamManagerTest.java
new file mode 100644
index 000000000..30fe445a5
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/ibb/InBandBytestreamManagerTest.java
@@ -0,0 +1,185 @@
+/**
+ * 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.ibb;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smackx.ibb.InBandBytestreamManager.StanzaType;
+import org.jivesoftware.smackx.ibb.packet.Open;
+import org.jivesoftware.util.ConnectionUtils;
+import org.jivesoftware.util.Protocol;
+import org.jivesoftware.util.Verification;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test for InBandBytestreamManager.
+ *
+ * @author Henning Staib
+ */
+public class InBandBytestreamManagerTest {
+
+ // settings
+ String initiatorJID = "initiator@xmpp-server/Smack";
+ String targetJID = "target@xmpp-server/Smack";
+ String xmppServer = "xmpp-server";
+ String sessionID = "session_id";
+
+ // protocol verifier
+ Protocol protocol;
+
+ // mocked XMPP connection
+ Connection connection;
+
+ /**
+ * Initialize fields used in the tests.
+ */
+ @Before
+ public void setup() {
+
+ // build protocol verifier
+ protocol = new Protocol();
+
+ // create mocked XMPP connection
+ connection = ConnectionUtils.createMockedConnection(protocol, initiatorJID,
+ xmppServer);
+
+ }
+
+ /**
+ * Test that
+ * {@link InBandBytestreamManager#getByteStreamManager(Connection)} returns
+ * one bytestream manager for every connection
+ */
+ @Test
+ public void shouldHaveOneManagerForEveryConnection() {
+
+ // mock two connections
+ Connection connection1 = mock(Connection.class);
+ Connection connection2 = mock(Connection.class);
+
+ // get bytestream manager for the first connection twice
+ InBandBytestreamManager conn1ByteStreamManager1 = InBandBytestreamManager.getByteStreamManager(connection1);
+ InBandBytestreamManager conn1ByteStreamManager2 = InBandBytestreamManager.getByteStreamManager(connection1);
+
+ // get bytestream manager for second connection
+ InBandBytestreamManager conn2ByteStreamManager1 = InBandBytestreamManager.getByteStreamManager(connection2);
+
+ // assertions
+ assertEquals(conn1ByteStreamManager1, conn1ByteStreamManager2);
+ assertNotSame(conn1ByteStreamManager1, conn2ByteStreamManager1);
+
+ }
+
+ /**
+ * Invoking {@link InBandBytestreamManager#establishSession(String)} should
+ * throw an exception if the given target does not support in-band
+ * bytestream.
+ */
+ @Test
+ public void shouldFailIfTargetDoesNotSupportIBB() {
+ InBandBytestreamManager byteStreamManager = InBandBytestreamManager.getByteStreamManager(connection);
+
+ try {
+ XMPPError xmppError = new XMPPError(
+ XMPPError.Condition.feature_not_implemented);
+ IQ errorIQ = IBBPacketUtils.createErrorIQ(targetJID, initiatorJID, xmppError);
+ protocol.addResponse(errorIQ);
+
+ // start In-Band Bytestream
+ byteStreamManager.establishSession(targetJID);
+
+ fail("exception should be thrown");
+ }
+ catch (XMPPException e) {
+ assertEquals(XMPPError.Condition.feature_not_implemented.toString(),
+ e.getXMPPError().getCondition());
+ }
+
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotAllowTooBigDefaultBlockSize() {
+ InBandBytestreamManager byteStreamManager = InBandBytestreamManager.getByteStreamManager(connection);
+ byteStreamManager.setDefaultBlockSize(1000000);
+ }
+
+ @Test
+ public void shouldCorrectlySetDefaultBlockSize() {
+ InBandBytestreamManager byteStreamManager = InBandBytestreamManager.getByteStreamManager(connection);
+ byteStreamManager.setDefaultBlockSize(1024);
+ assertEquals(1024, byteStreamManager.getDefaultBlockSize());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotAllowTooBigMaximumBlockSize() {
+ InBandBytestreamManager byteStreamManager = InBandBytestreamManager.getByteStreamManager(connection);
+ byteStreamManager.setMaximumBlockSize(1000000);
+ }
+
+ @Test
+ public void shouldCorrectlySetMaximumBlockSize() {
+ InBandBytestreamManager byteStreamManager = InBandBytestreamManager.getByteStreamManager(connection);
+ byteStreamManager.setMaximumBlockSize(1024);
+ assertEquals(1024, byteStreamManager.getMaximumBlockSize());
+ }
+
+ @Test
+ public void shouldUseConfiguredStanzaType() {
+ InBandBytestreamManager byteStreamManager = InBandBytestreamManager.getByteStreamManager(connection);
+ byteStreamManager.setStanza(StanzaType.MESSAGE);
+
+ protocol.addResponse(null, new Verification() {
+
+ public void verify(Open request, IQ response) {
+ assertEquals(StanzaType.MESSAGE, request.getStanza());
+ }
+
+ });
+
+ try {
+ // start In-Band Bytestream
+ byteStreamManager.establishSession(targetJID);
+ }
+ catch (XMPPException e) {
+ protocol.verifyAll();
+ }
+
+ }
+
+ @Test
+ public void shouldReturnSession() throws Exception {
+ InBandBytestreamManager byteStreamManager = InBandBytestreamManager.getByteStreamManager(connection);
+
+ IQ success = IBBPacketUtils.createResultIQ(targetJID, initiatorJID);
+ protocol.addResponse(success, Verification.correspondingSenderReceiver,
+ Verification.requestTypeSET);
+
+ // start In-Band Bytestream
+ InBandBytestreamSession session = byteStreamManager.establishSession(targetJID);
+
+ assertNotNull(session);
+ assertNotNull(session.getInputStream());
+ assertNotNull(session.getOutputStream());
+
+ protocol.verifyAll();
+
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/ibb/InBandBytestreamRequestTest.java b/test-unit/org/jivesoftware/smackx/ibb/InBandBytestreamRequestTest.java
new file mode 100644
index 000000000..b0ba6d6df
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/ibb/InBandBytestreamRequestTest.java
@@ -0,0 +1,98 @@
+package org.jivesoftware.smackx.ibb;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smackx.ibb.packet.Open;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+/**
+ * Test for InBandBytestreamRequest.
+ *
+ * @author Henning Staib
+ */
+public class InBandBytestreamRequestTest {
+
+ String initiatorJID = "initiator@xmpp-server/Smack";
+ String targetJID = "target@xmpp-server/Smack";
+ String sessionID = "session_id";
+
+ Connection connection;
+ InBandBytestreamManager byteStreamManager;
+ Open initBytestream;
+
+ /**
+ * Initialize fields used in the tests.
+ */
+ @Before
+ public void setup() {
+
+ // mock connection
+ connection = mock(Connection.class);
+
+ // initialize InBandBytestreamManager to get the InitiationListener
+ byteStreamManager = InBandBytestreamManager.getByteStreamManager(connection);
+
+ // create a In-Band Bytestream open packet
+ initBytestream = new Open(sessionID, 4096);
+ initBytestream.setFrom(initiatorJID);
+ initBytestream.setTo(targetJID);
+
+ }
+
+ /**
+ * Test reject() method.
+ */
+ @Test
+ public void shouldReplyWithErrorIfRequestIsRejected() {
+ InBandBytestreamRequest ibbRequest = new InBandBytestreamRequest(
+ byteStreamManager, initBytestream);
+
+ // reject request
+ ibbRequest.reject();
+
+ // capture reply to the In-Band Bytestream open request
+ ArgumentCaptor argument = ArgumentCaptor.forClass(IQ.class);
+ verify(connection).sendPacket(argument.capture());
+
+ // assert that reply is the correct error packet
+ assertEquals(initiatorJID, argument.getValue().getTo());
+ assertEquals(IQ.Type.ERROR, argument.getValue().getType());
+ assertEquals(XMPPError.Condition.no_acceptable.toString(),
+ argument.getValue().getError().getCondition());
+
+ }
+
+ /**
+ * Test accept() method.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldReturnSessionIfRequestIsAccepted() throws Exception {
+ InBandBytestreamRequest ibbRequest = new InBandBytestreamRequest(
+ byteStreamManager, initBytestream);
+
+ // accept request
+ InBandBytestreamSession session = ibbRequest.accept();
+
+ // capture reply to the In-Band Bytestream open request
+ ArgumentCaptor argument = ArgumentCaptor.forClass(IQ.class);
+ verify(connection).sendPacket(argument.capture());
+
+ // assert that reply is the correct acknowledgment packet
+ assertEquals(initiatorJID, argument.getValue().getTo());
+ assertEquals(IQ.Type.RESULT, argument.getValue().getType());
+
+ assertNotNull(session);
+ assertNotNull(session.getInputStream());
+ assertNotNull(session.getOutputStream());
+
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/ibb/InBandBytestreamSessionMessageTest.java b/test-unit/org/jivesoftware/smackx/ibb/InBandBytestreamSessionMessageTest.java
new file mode 100644
index 000000000..75a910636
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/ibb/InBandBytestreamSessionMessageTest.java
@@ -0,0 +1,354 @@
+package org.jivesoftware.smackx.ibb;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Random;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.ibb.InBandBytestreamManager.StanzaType;
+import org.jivesoftware.smackx.ibb.packet.DataPacketExtension;
+import org.jivesoftware.smackx.ibb.packet.Open;
+import org.jivesoftware.util.ConnectionUtils;
+import org.jivesoftware.util.Protocol;
+import org.jivesoftware.util.Verification;
+import org.junit.Before;
+import org.junit.Test;
+import org.powermock.reflect.Whitebox;
+
+/**
+ * Test for InBandBytestreamSession.
+ *
+ * Tests sending data encapsulated in message stanzas.
+ *
+ * @author Henning Staib
+ */
+public class InBandBytestreamSessionMessageTest {
+
+ // settings
+ String initiatorJID = "initiator@xmpp-server/Smack";
+ String targetJID = "target@xmpp-server/Smack";
+ String xmppServer = "xmpp-server";
+ String sessionID = "session_id";
+
+ int blockSize = 10;
+
+ // protocol verifier
+ Protocol protocol;
+
+ // mocked XMPP connection
+ Connection connection;
+
+ InBandBytestreamManager byteStreamManager;
+
+ Open initBytestream;
+
+ Verification incrementingSequence;
+
+ /**
+ * Initialize fields used in the tests.
+ */
+ @Before
+ public void setup() {
+
+ // build protocol verifier
+ protocol = new Protocol();
+
+ // create mocked XMPP connection
+ connection = ConnectionUtils.createMockedConnection(protocol, initiatorJID, xmppServer);
+
+ // initialize InBandBytestreamManager to get the InitiationListener
+ byteStreamManager = InBandBytestreamManager.getByteStreamManager(connection);
+
+ // create a In-Band Bytestream open packet with message stanza
+ initBytestream = new Open(sessionID, blockSize, StanzaType.MESSAGE);
+ initBytestream.setFrom(initiatorJID);
+ initBytestream.setTo(targetJID);
+
+ incrementingSequence = new Verification() {
+
+ long lastSeq = 0;
+
+ public void verify(Message request, IQ response) {
+ DataPacketExtension dpe = (DataPacketExtension) request.getExtension(
+ DataPacketExtension.ELEMENT_NAME, InBandBytestreamManager.NAMESPACE);
+ assertEquals(lastSeq++, dpe.getSeq());
+ }
+
+ };
+
+ }
+
+ /**
+ * Test the output stream write(byte[]) method.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldSendThreeDataPackets1() throws Exception {
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+
+ // verify the data packets
+ protocol.addResponse(null, incrementingSequence);
+ protocol.addResponse(null, incrementingSequence);
+ protocol.addResponse(null, incrementingSequence);
+
+ byte[] controlData = new byte[blockSize * 3];
+
+ OutputStream outputStream = session.getOutputStream();
+ outputStream.write(controlData);
+ outputStream.flush();
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * Test the output stream write(byte) method.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldSendThreeDataPackets2() throws Exception {
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+
+ // verify the data packets
+ protocol.addResponse(null, incrementingSequence);
+ protocol.addResponse(null, incrementingSequence);
+ protocol.addResponse(null, incrementingSequence);
+
+ byte[] controlData = new byte[blockSize * 3];
+
+ OutputStream outputStream = session.getOutputStream();
+ for (byte b : controlData) {
+ outputStream.write(b);
+ }
+ outputStream.flush();
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * Test the output stream write(byte[], int, int) method.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldSendThreeDataPackets3() throws Exception {
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+
+ // verify the data packets
+ protocol.addResponse(null, incrementingSequence);
+ protocol.addResponse(null, incrementingSequence);
+ protocol.addResponse(null, incrementingSequence);
+
+ byte[] controlData = new byte[(blockSize * 3) - 2];
+
+ OutputStream outputStream = session.getOutputStream();
+ int off = 0;
+ for (int i = 1; i <= 7; i++) {
+ outputStream.write(controlData, off, i);
+ off += i;
+ }
+ outputStream.flush();
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * Test the output stream flush() method.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldSendThirtyDataPackets() throws Exception {
+ byte[] controlData = new byte[blockSize * 3];
+
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+
+ // verify the data packets
+ for (int i = 0; i < controlData.length; i++) {
+ protocol.addResponse(null, incrementingSequence);
+ }
+
+ OutputStream outputStream = session.getOutputStream();
+ for (byte b : controlData) {
+ outputStream.write(b);
+ outputStream.flush();
+ }
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * Test successive calls to the output stream flush() method.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldSendNothingOnSuccessiveCallsToFlush() throws Exception {
+ byte[] controlData = new byte[blockSize * 3];
+
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+
+ // verify the data packets
+ protocol.addResponse(null, incrementingSequence);
+ protocol.addResponse(null, incrementingSequence);
+ protocol.addResponse(null, incrementingSequence);
+
+ OutputStream outputStream = session.getOutputStream();
+ outputStream.write(controlData);
+
+ outputStream.flush();
+ outputStream.flush();
+ outputStream.flush();
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * If a data packet is received out of order the session should be closed. See XEP-0047 Section
+ * 2.2.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldSendCloseRequestIfInvalidSequenceReceived() throws Exception {
+ // confirm close request
+ IQ resultIQ = IBBPacketUtils.createResultIQ(initiatorJID, targetJID);
+ protocol.addResponse(resultIQ, Verification.requestTypeSET,
+ Verification.correspondingSenderReceiver);
+
+ // get IBB sessions data packet listener
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+ InputStream inputStream = session.getInputStream();
+ PacketListener listener = Whitebox.getInternalState(inputStream, PacketListener.class);
+
+ // build invalid packet with out of order sequence
+ String base64Data = StringUtils.encodeBase64("Data");
+ DataPacketExtension dpe = new DataPacketExtension(sessionID, 123, base64Data);
+ Message dataMessage = new Message();
+ dataMessage.addExtension(dpe);
+
+ // add data packets
+ listener.processPacket(dataMessage);
+
+ // read until exception is thrown
+ try {
+ inputStream.read();
+ fail("exception should be thrown");
+ }
+ catch (IOException e) {
+ assertTrue(e.getMessage().contains("Packets out of sequence"));
+ }
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * Test the input stream read(byte[], int, int) method.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldReadAllReceivedData1() throws Exception {
+ // create random data
+ Random rand = new Random();
+ byte[] controlData = new byte[3 * blockSize];
+ rand.nextBytes(controlData);
+
+ // get IBB sessions data packet listener
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+ InputStream inputStream = session.getInputStream();
+ PacketListener listener = Whitebox.getInternalState(inputStream, PacketListener.class);
+
+ // verify data packet and notify listener
+ for (int i = 0; i < controlData.length / blockSize; i++) {
+ String base64Data = StringUtils.encodeBase64(controlData, i * blockSize, blockSize,
+ false);
+ DataPacketExtension dpe = new DataPacketExtension(sessionID, i, base64Data);
+ Message dataMessage = new Message();
+ dataMessage.addExtension(dpe);
+ listener.processPacket(dataMessage);
+ }
+
+ byte[] bytes = new byte[3 * blockSize];
+ int read = 0;
+ read = inputStream.read(bytes, 0, blockSize);
+ assertEquals(blockSize, read);
+ read = inputStream.read(bytes, 10, blockSize);
+ assertEquals(blockSize, read);
+ read = inputStream.read(bytes, 20, blockSize);
+ assertEquals(blockSize, read);
+
+ // verify data
+ for (int i = 0; i < bytes.length; i++) {
+ assertEquals(controlData[i], bytes[i]);
+ }
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * Test the input stream read() method.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldReadAllReceivedData2() throws Exception {
+ // create random data
+ Random rand = new Random();
+ byte[] controlData = new byte[3 * blockSize];
+ rand.nextBytes(controlData);
+
+ // get IBB sessions data packet listener
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+ InputStream inputStream = session.getInputStream();
+ PacketListener listener = Whitebox.getInternalState(inputStream, PacketListener.class);
+
+ // verify data packet and notify listener
+ for (int i = 0; i < controlData.length / blockSize; i++) {
+ String base64Data = StringUtils.encodeBase64(controlData, i * blockSize, blockSize,
+ false);
+ DataPacketExtension dpe = new DataPacketExtension(sessionID, i, base64Data);
+ Message dataMessage = new Message();
+ dataMessage.addExtension(dpe);
+ listener.processPacket(dataMessage);
+ }
+
+ // read data
+ byte[] bytes = new byte[3 * blockSize];
+ for (int i = 0; i < bytes.length; i++) {
+ bytes[i] = (byte) inputStream.read();
+ }
+
+ // verify data
+ for (int i = 0; i < bytes.length; i++) {
+ assertEquals(controlData[i], bytes[i]);
+ }
+
+ protocol.verifyAll();
+
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/ibb/InBandBytestreamSessionTest.java b/test-unit/org/jivesoftware/smackx/ibb/InBandBytestreamSessionTest.java
new file mode 100644
index 000000000..9645e6899
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/ibb/InBandBytestreamSessionTest.java
@@ -0,0 +1,698 @@
+package org.jivesoftware.smackx.ibb;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Random;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.ibb.packet.Data;
+import org.jivesoftware.smackx.ibb.packet.DataPacketExtension;
+import org.jivesoftware.smackx.ibb.packet.Open;
+import org.jivesoftware.util.ConnectionUtils;
+import org.jivesoftware.util.Protocol;
+import org.jivesoftware.util.Verification;
+import org.junit.Before;
+import org.junit.Test;
+import org.powermock.reflect.Whitebox;
+
+/**
+ * Test for InBandBytestreamSession.
+ *
+ * Tests the basic behavior of an In-Band Bytestream session along with sending data encapsulated in
+ * IQ stanzas.
+ *
+ * @author Henning Staib
+ */
+public class InBandBytestreamSessionTest {
+
+ // settings
+ String initiatorJID = "initiator@xmpp-server/Smack";
+ String targetJID = "target@xmpp-server/Smack";
+ String xmppServer = "xmpp-server";
+ String sessionID = "session_id";
+
+ int blockSize = 10;
+
+ // protocol verifier
+ Protocol protocol;
+
+ // mocked XMPP connection
+ Connection connection;
+
+ InBandBytestreamManager byteStreamManager;
+
+ Open initBytestream;
+
+ Verification incrementingSequence;
+
+ /**
+ * Initialize fields used in the tests.
+ */
+ @Before
+ public void setup() {
+
+ // build protocol verifier
+ protocol = new Protocol();
+
+ // create mocked XMPP connection
+ connection = ConnectionUtils.createMockedConnection(protocol, initiatorJID, xmppServer);
+
+ // initialize InBandBytestreamManager to get the InitiationListener
+ byteStreamManager = InBandBytestreamManager.getByteStreamManager(connection);
+
+ // create a In-Band Bytestream open packet
+ initBytestream = new Open(sessionID, blockSize);
+ initBytestream.setFrom(initiatorJID);
+ initBytestream.setTo(targetJID);
+
+ incrementingSequence = new Verification() {
+
+ long lastSeq = 0;
+
+ public void verify(Data request, IQ response) {
+ assertEquals(lastSeq++, request.getDataPacketExtension().getSeq());
+ }
+
+ };
+
+ }
+
+ /**
+ * Test the output stream write(byte[]) method.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldSendThreeDataPackets1() throws Exception {
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+
+ // set acknowledgments for the data packets
+ IQ resultIQ = IBBPacketUtils.createResultIQ(initiatorJID, targetJID);
+ protocol.addResponse(resultIQ, incrementingSequence);
+ protocol.addResponse(resultIQ, incrementingSequence);
+ protocol.addResponse(resultIQ, incrementingSequence);
+
+ byte[] controlData = new byte[blockSize * 3];
+
+ OutputStream outputStream = session.getOutputStream();
+ outputStream.write(controlData);
+ outputStream.flush();
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * Test the output stream write(byte) method.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldSendThreeDataPackets2() throws Exception {
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+
+ // set acknowledgments for the data packets
+ IQ resultIQ = IBBPacketUtils.createResultIQ(initiatorJID, targetJID);
+ protocol.addResponse(resultIQ, incrementingSequence);
+ protocol.addResponse(resultIQ, incrementingSequence);
+ protocol.addResponse(resultIQ, incrementingSequence);
+
+ byte[] controlData = new byte[blockSize * 3];
+
+ OutputStream outputStream = session.getOutputStream();
+ for (byte b : controlData) {
+ outputStream.write(b);
+ }
+ outputStream.flush();
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * Test the output stream write(byte[], int, int) method.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldSendThreeDataPackets3() throws Exception {
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+
+ // set acknowledgments for the data packets
+ IQ resultIQ = IBBPacketUtils.createResultIQ(initiatorJID, targetJID);
+ protocol.addResponse(resultIQ, incrementingSequence);
+ protocol.addResponse(resultIQ, incrementingSequence);
+ protocol.addResponse(resultIQ, incrementingSequence);
+
+ byte[] controlData = new byte[(blockSize * 3) - 2];
+
+ OutputStream outputStream = session.getOutputStream();
+ int off = 0;
+ for (int i = 1; i <= 7; i++) {
+ outputStream.write(controlData, off, i);
+ off += i;
+ }
+ outputStream.flush();
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * Test the output stream flush() method.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldSendThirtyDataPackets() throws Exception {
+ byte[] controlData = new byte[blockSize * 3];
+
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+
+ // set acknowledgments for the data packets
+ IQ resultIQ = IBBPacketUtils.createResultIQ(initiatorJID, targetJID);
+ for (int i = 0; i < controlData.length; i++) {
+ protocol.addResponse(resultIQ, incrementingSequence);
+ }
+
+ OutputStream outputStream = session.getOutputStream();
+ for (byte b : controlData) {
+ outputStream.write(b);
+ outputStream.flush();
+ }
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * Test successive calls to the output stream flush() method.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldSendNothingOnSuccessiveCallsToFlush() throws Exception {
+ byte[] controlData = new byte[blockSize * 3];
+
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+
+ // set acknowledgments for the data packets
+ IQ resultIQ = IBBPacketUtils.createResultIQ(initiatorJID, targetJID);
+ protocol.addResponse(resultIQ, incrementingSequence);
+ protocol.addResponse(resultIQ, incrementingSequence);
+ protocol.addResponse(resultIQ, incrementingSequence);
+
+ OutputStream outputStream = session.getOutputStream();
+ outputStream.write(controlData);
+
+ outputStream.flush();
+ outputStream.flush();
+ outputStream.flush();
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * Test that the data is correctly chunked.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldSendDataCorrectly() throws Exception {
+ // create random data
+ Random rand = new Random();
+ final byte[] controlData = new byte[256 * blockSize];
+ rand.nextBytes(controlData);
+
+ // compares the data of each packet with the control data
+ Verification dataVerification = new Verification() {
+
+ public void verify(Data request, IQ response) {
+ byte[] decodedData = request.getDataPacketExtension().getDecodedData();
+ int seq = (int) request.getDataPacketExtension().getSeq();
+ for (int i = 0; i < decodedData.length; i++) {
+ assertEquals(controlData[(seq * blockSize) + i], decodedData[i]);
+ }
+ }
+
+ };
+
+ // set acknowledgments for the data packets
+ IQ resultIQ = IBBPacketUtils.createResultIQ(initiatorJID, targetJID);
+ for (int i = 0; i < controlData.length / blockSize; i++) {
+ protocol.addResponse(resultIQ, incrementingSequence, dataVerification);
+ }
+
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+
+ OutputStream outputStream = session.getOutputStream();
+ outputStream.write(controlData);
+ outputStream.flush();
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * If the input stream is closed the output stream should not be closed as well.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldNotCloseBothStreamsIfOutputStreamIsClosed() throws Exception {
+
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+ OutputStream outputStream = session.getOutputStream();
+ outputStream.close();
+
+ // verify data packet confirmation is of type RESULT
+ protocol.addResponse(null, Verification.requestTypeRESULT);
+
+ // insert data to read
+ InputStream inputStream = session.getInputStream();
+ PacketListener listener = Whitebox.getInternalState(inputStream, PacketListener.class);
+ String base64Data = StringUtils.encodeBase64("Data");
+ DataPacketExtension dpe = new DataPacketExtension(sessionID, 0, base64Data);
+ Data data = new Data(dpe);
+ listener.processPacket(data);
+
+ // verify no packet send
+ protocol.verifyAll();
+
+ try {
+ outputStream.flush();
+ fail("should throw an exception");
+ }
+ catch (IOException e) {
+ assertTrue(e.getMessage().contains("closed"));
+ }
+
+ assertTrue(inputStream.read() != 0);
+
+ }
+
+ /**
+ * Valid data packets should be confirmed.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldConfirmReceivedDataPacket() throws Exception {
+ // verify data packet confirmation is of type RESULT
+ protocol.addResponse(null, Verification.requestTypeRESULT);
+
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+ InputStream inputStream = session.getInputStream();
+ PacketListener listener = Whitebox.getInternalState(inputStream, PacketListener.class);
+
+ String base64Data = StringUtils.encodeBase64("Data");
+ DataPacketExtension dpe = new DataPacketExtension(sessionID, 0, base64Data);
+ Data data = new Data(dpe);
+
+ listener.processPacket(data);
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * If the data packet has a sequence that is already used an 'unexpected-request' error should
+ * be returned. See XEP-0047 Section 2.2.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldReplyWithErrorIfAlreadyUsedSequenceIsReceived() throws Exception {
+ // verify reply to first valid data packet is of type RESULT
+ protocol.addResponse(null, Verification.requestTypeRESULT);
+
+ // verify reply to invalid data packet is an error
+ protocol.addResponse(null, Verification.requestTypeERROR, new Verification() {
+
+ public void verify(IQ request, IQ response) {
+ assertEquals(XMPPError.Condition.unexpected_request.toString(),
+ request.getError().getCondition());
+ }
+
+ });
+
+ // get IBB sessions data packet listener
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+ InputStream inputStream = session.getInputStream();
+ PacketListener listener = Whitebox.getInternalState(inputStream, PacketListener.class);
+
+ // build data packets
+ String base64Data = StringUtils.encodeBase64("Data");
+ DataPacketExtension dpe = new DataPacketExtension(sessionID, 0, base64Data);
+ Data data1 = new Data(dpe);
+ Data data2 = new Data(dpe);
+
+ // notify listener
+ listener.processPacket(data1);
+ listener.processPacket(data2);
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * If the data packet contains invalid Base64 encoding an 'bad-request' error should be
+ * returned. See XEP-0047 Section 2.2.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldReplyWithErrorIfDataIsInvalid() throws Exception {
+ // verify reply to invalid data packet is an error
+ protocol.addResponse(null, Verification.requestTypeERROR, new Verification() {
+
+ public void verify(IQ request, IQ response) {
+ assertEquals(XMPPError.Condition.bad_request.toString(),
+ request.getError().getCondition());
+ }
+
+ });
+
+ // get IBB sessions data packet listener
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+ InputStream inputStream = session.getInputStream();
+ PacketListener listener = Whitebox.getInternalState(inputStream, PacketListener.class);
+
+ // build data packets
+ DataPacketExtension dpe = new DataPacketExtension(sessionID, 0, "AA=BB");
+ Data data = new Data(dpe);
+
+ // notify listener
+ listener.processPacket(data);
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * If a data packet is received out of order the session should be closed. See XEP-0047 Section
+ * 2.2.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldSendCloseRequestIfInvalidSequenceReceived() throws Exception {
+ IQ resultIQ = IBBPacketUtils.createResultIQ(initiatorJID, targetJID);
+
+ // confirm data packet with invalid sequence
+ protocol.addResponse(resultIQ);
+
+ // confirm close request
+ protocol.addResponse(resultIQ, Verification.requestTypeSET,
+ Verification.correspondingSenderReceiver);
+
+ // get IBB sessions data packet listener
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+ InputStream inputStream = session.getInputStream();
+ PacketListener listener = Whitebox.getInternalState(inputStream, PacketListener.class);
+
+ // build invalid packet with out of order sequence
+ String base64Data = StringUtils.encodeBase64("Data");
+ DataPacketExtension dpe = new DataPacketExtension(sessionID, 123, base64Data);
+ Data data = new Data(dpe);
+
+ // add data packets
+ listener.processPacket(data);
+
+ // read until exception is thrown
+ try {
+ inputStream.read();
+ fail("exception should be thrown");
+ }
+ catch (IOException e) {
+ assertTrue(e.getMessage().contains("Packets out of sequence"));
+ }
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * Test the input stream read(byte[], int, int) method.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldReadAllReceivedData1() throws Exception {
+ // create random data
+ Random rand = new Random();
+ byte[] controlData = new byte[3 * blockSize];
+ rand.nextBytes(controlData);
+
+ IQ resultIQ = IBBPacketUtils.createResultIQ(initiatorJID, targetJID);
+
+ // get IBB sessions data packet listener
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+ InputStream inputStream = session.getInputStream();
+ PacketListener listener = Whitebox.getInternalState(inputStream, PacketListener.class);
+
+ // set data packet acknowledgment and notify listener
+ for (int i = 0; i < controlData.length / blockSize; i++) {
+ protocol.addResponse(resultIQ);
+ String base64Data = StringUtils.encodeBase64(controlData, i * blockSize, blockSize,
+ false);
+ DataPacketExtension dpe = new DataPacketExtension(sessionID, i, base64Data);
+ Data data = new Data(dpe);
+ listener.processPacket(data);
+ }
+
+ byte[] bytes = new byte[3 * blockSize];
+ int read = 0;
+ read = inputStream.read(bytes, 0, blockSize);
+ assertEquals(blockSize, read);
+ read = inputStream.read(bytes, 10, blockSize);
+ assertEquals(blockSize, read);
+ read = inputStream.read(bytes, 20, blockSize);
+ assertEquals(blockSize, read);
+
+ // verify data
+ for (int i = 0; i < bytes.length; i++) {
+ assertEquals(controlData[i], bytes[i]);
+ }
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * Test the input stream read() method.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldReadAllReceivedData2() throws Exception {
+ // create random data
+ Random rand = new Random();
+ byte[] controlData = new byte[3 * blockSize];
+ rand.nextBytes(controlData);
+
+ IQ resultIQ = IBBPacketUtils.createResultIQ(initiatorJID, targetJID);
+
+ // get IBB sessions data packet listener
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+ InputStream inputStream = session.getInputStream();
+ PacketListener listener = Whitebox.getInternalState(inputStream, PacketListener.class);
+
+ // set data packet acknowledgment and notify listener
+ for (int i = 0; i < controlData.length / blockSize; i++) {
+ protocol.addResponse(resultIQ);
+ String base64Data = StringUtils.encodeBase64(controlData, i * blockSize, blockSize,
+ false);
+ DataPacketExtension dpe = new DataPacketExtension(sessionID, i, base64Data);
+ Data data = new Data(dpe);
+ listener.processPacket(data);
+ }
+
+ // read data
+ byte[] bytes = new byte[3 * blockSize];
+ for (int i = 0; i < bytes.length; i++) {
+ bytes[i] = (byte) inputStream.read();
+ }
+
+ // verify data
+ for (int i = 0; i < bytes.length; i++) {
+ assertEquals(controlData[i], bytes[i]);
+ }
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * If the output stream is closed the input stream should not be closed as well.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldNotCloseBothStreamsIfInputStreamIsClosed() throws Exception {
+ // acknowledgment for data packet
+ IQ resultIQ = IBBPacketUtils.createResultIQ(initiatorJID, targetJID);
+ protocol.addResponse(resultIQ);
+
+ // get IBB sessions data packet listener
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+ InputStream inputStream = session.getInputStream();
+ PacketListener listener = Whitebox.getInternalState(inputStream, PacketListener.class);
+
+ // build data packet
+ String base64Data = StringUtils.encodeBase64("Data");
+ DataPacketExtension dpe = new DataPacketExtension(sessionID, 0, base64Data);
+ Data data = new Data(dpe);
+
+ // add data packets
+ listener.processPacket(data);
+
+ inputStream.close();
+
+ protocol.verifyAll();
+
+ try {
+ while (inputStream.read() != -1) {
+ }
+ inputStream.read();
+ fail("should throw an exception");
+ }
+ catch (IOException e) {
+ assertTrue(e.getMessage().contains("closed"));
+ }
+
+ session.getOutputStream().flush();
+
+ }
+
+ /**
+ * If the session is closed the input stream and output stream should be closed as well.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldCloseBothStreamsIfSessionIsClosed() throws Exception {
+ // acknowledgment for data packet
+ IQ resultIQ = IBBPacketUtils.createResultIQ(initiatorJID, targetJID);
+ protocol.addResponse(resultIQ);
+
+ // acknowledgment for close request
+ protocol.addResponse(resultIQ, Verification.correspondingSenderReceiver,
+ Verification.requestTypeSET);
+
+ // get IBB sessions data packet listener
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+ InputStream inputStream = session.getInputStream();
+ PacketListener listener = Whitebox.getInternalState(inputStream, PacketListener.class);
+
+ // build data packet
+ String base64Data = StringUtils.encodeBase64("Data");
+ DataPacketExtension dpe = new DataPacketExtension(sessionID, 0, base64Data);
+ Data data = new Data(dpe);
+
+ // add data packets
+ listener.processPacket(data);
+
+ session.close();
+
+ protocol.verifyAll();
+
+ try {
+ while (inputStream.read() != -1) {
+ }
+ inputStream.read();
+ fail("should throw an exception");
+ }
+ catch (IOException e) {
+ assertTrue(e.getMessage().contains("closed"));
+ }
+
+ try {
+ session.getOutputStream().flush();
+ fail("should throw an exception");
+ }
+ catch (IOException e) {
+ assertTrue(e.getMessage().contains("closed"));
+ }
+
+ }
+
+ /**
+ * If the input stream is closed concurrently there should be no deadlock.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldNotDeadlockIfInputStreamIsClosed() throws Exception {
+ // acknowledgment for data packet
+ IQ resultIQ = IBBPacketUtils.createResultIQ(initiatorJID, targetJID);
+ protocol.addResponse(resultIQ);
+
+ // get IBB sessions data packet listener
+ InBandBytestreamSession session = new InBandBytestreamSession(connection, initBytestream,
+ initiatorJID);
+ final InputStream inputStream = session.getInputStream();
+ PacketListener listener = Whitebox.getInternalState(inputStream, PacketListener.class);
+
+ // build data packet
+ String base64Data = StringUtils.encodeBase64("Data");
+ DataPacketExtension dpe = new DataPacketExtension(sessionID, 0, base64Data);
+ Data data = new Data(dpe);
+
+ // add data packets
+ listener.processPacket(data);
+
+ Thread closer = new Thread(new Runnable() {
+
+ public void run() {
+ try {
+ Thread.sleep(200);
+ inputStream.close();
+ }
+ catch (Exception e) {
+ fail(e.getMessage());
+ }
+ }
+
+ });
+ closer.start();
+
+ try {
+ byte[] bytes = new byte[20];
+ while (inputStream.read(bytes) != -1) {
+ }
+ inputStream.read();
+ fail("should throw an exception");
+ }
+ catch (IOException e) {
+ assertTrue(e.getMessage().contains("closed"));
+ }
+
+ protocol.verifyAll();
+
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/ibb/InitiationListenerTest.java b/test-unit/org/jivesoftware/smackx/ibb/InitiationListenerTest.java
new file mode 100644
index 000000000..86c7336b8
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/ibb/InitiationListenerTest.java
@@ -0,0 +1,327 @@
+/**
+ * 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.ibb;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
+import org.jivesoftware.smackx.ibb.packet.Open;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.powermock.reflect.Whitebox;
+
+/**
+ * Test for the InitiationListener class.
+ *
+ * @author Henning Staib
+ */
+public class InitiationListenerTest {
+
+ String initiatorJID = "initiator@xmpp-server/Smack";
+ String targetJID = "target@xmpp-server/Smack";
+ String sessionID = "session_id";
+
+ Connection connection;
+ InBandBytestreamManager byteStreamManager;
+ InitiationListener initiationListener;
+ Open initBytestream;
+
+ /**
+ * Initialize fields used in the tests.
+ */
+ @Before
+ public void setup() {
+
+ // mock connection
+ connection = mock(Connection.class);
+
+ // initialize InBandBytestreamManager to get the InitiationListener
+ byteStreamManager = InBandBytestreamManager.getByteStreamManager(connection);
+
+ // get the InitiationListener from InBandByteStreamManager
+ initiationListener = Whitebox.getInternalState(byteStreamManager, InitiationListener.class);
+
+ // create a In-Band Bytestream open packet
+ initBytestream = new Open(sessionID, 4096);
+ initBytestream.setFrom(initiatorJID);
+ initBytestream.setTo(targetJID);
+
+ }
+
+ /**
+ * If no listeners are registered for incoming In-Band Bytestream requests, all request should
+ * be rejected with an error.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldRespondWithError() throws Exception {
+
+ // run the listener with the initiation packet
+ initiationListener.processPacket(initBytestream);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // capture reply to the In-Band Bytestream open request
+ ArgumentCaptor argument = ArgumentCaptor.forClass(IQ.class);
+ verify(connection).sendPacket(argument.capture());
+
+ // assert that reply is the correct error packet
+ assertEquals(initiatorJID, argument.getValue().getTo());
+ assertEquals(IQ.Type.ERROR, argument.getValue().getType());
+ assertEquals(XMPPError.Condition.no_acceptable.toString(),
+ argument.getValue().getError().getCondition());
+
+ }
+
+ /**
+ * Open request with a block size that exceeds the maximum block size should be replied with an
+ * resource-constraint error.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldRejectRequestWithTooBigBlockSize() throws Exception {
+ byteStreamManager.setMaximumBlockSize(1024);
+
+ // run the listener with the initiation packet
+ initiationListener.processPacket(initBytestream);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // capture reply to the In-Band Bytestream open request
+ ArgumentCaptor argument = ArgumentCaptor.forClass(IQ.class);
+ verify(connection).sendPacket(argument.capture());
+
+ // assert that reply is the correct error packet
+ assertEquals(initiatorJID, argument.getValue().getTo());
+ assertEquals(IQ.Type.ERROR, argument.getValue().getType());
+ assertEquals(XMPPError.Condition.resource_constraint.toString(),
+ argument.getValue().getError().getCondition());
+
+ }
+
+ /**
+ * If a listener for all requests is registered it should be notified on incoming requests.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldInvokeListenerForAllRequests() throws Exception {
+
+ // add listener
+ InBandBytestreamListener listener = mock(InBandBytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(listener);
+
+ // run the listener with the initiation packet
+ initiationListener.processPacket(initBytestream);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // assert listener is called once
+ ArgumentCaptor byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(listener).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ // assert that listener is called for the correct request
+ assertEquals(initiatorJID, byteStreamRequest.getValue().getFrom());
+
+ }
+
+ /**
+ * If a listener for a specific user in registered it should be notified on incoming requests
+ * for that user.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldInvokeListenerForUser() throws Exception {
+
+ // add listener
+ InBandBytestreamListener listener = mock(InBandBytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(listener, initiatorJID);
+
+ // run the listener with the initiation packet
+ initiationListener.processPacket(initBytestream);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // assert listener is called once
+ ArgumentCaptor byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(listener).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ // assert that reply is the correct error packet
+ assertEquals(initiatorJID, byteStreamRequest.getValue().getFrom());
+
+ }
+
+ /**
+ * If listener for a specific user is registered it should not be notified on incoming requests
+ * from other users.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldNotInvokeListenerForUser() throws Exception {
+
+ // add listener for request of user "other_initiator"
+ InBandBytestreamListener listener = mock(InBandBytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(listener, "other_" + initiatorJID);
+
+ // run the listener with the initiation packet
+ initiationListener.processPacket(initBytestream);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // assert listener is not called
+ ArgumentCaptor byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(listener, never()).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ // capture reply to the In-Band Bytestream open request
+ ArgumentCaptor argument = ArgumentCaptor.forClass(IQ.class);
+ verify(connection).sendPacket(argument.capture());
+
+ // assert that reply is the correct error packet
+ assertEquals(initiatorJID, argument.getValue().getTo());
+ assertEquals(IQ.Type.ERROR, argument.getValue().getType());
+ assertEquals(XMPPError.Condition.no_acceptable.toString(),
+ argument.getValue().getError().getCondition());
+ }
+
+ /**
+ * If a user specific listener and an all requests listener is registered only the user specific
+ * listener should be notified.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldNotInvokeAllRequestsListenerIfUserListenerExists() throws Exception {
+
+ // add listener for all request
+ InBandBytestreamListener allRequestsListener = mock(InBandBytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(allRequestsListener);
+
+ // add listener for request of user "initiator"
+ InBandBytestreamListener userRequestsListener = mock(InBandBytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(userRequestsListener, initiatorJID);
+
+ // run the listener with the initiation packet
+ initiationListener.processPacket(initBytestream);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // assert user request listener is called once
+ ArgumentCaptor byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(userRequestsListener).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ // assert all requests listener is not called
+ byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(allRequestsListener, never()).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ }
+
+ /**
+ * If a user specific listener and an all requests listener is registered only the all requests
+ * listener should be notified on an incoming request for another user.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldInvokeAllRequestsListenerIfUserListenerExists() throws Exception {
+
+ // add listener for all request
+ InBandBytestreamListener allRequestsListener = mock(InBandBytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(allRequestsListener);
+
+ // add listener for request of user "other_initiator"
+ InBandBytestreamListener userRequestsListener = mock(InBandBytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(userRequestsListener, "other_"
+ + initiatorJID);
+
+ // run the listener with the initiation packet
+ initiationListener.processPacket(initBytestream);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // assert user request listener is not called
+ ArgumentCaptor byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(userRequestsListener, never()).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ // assert all requests listener is called
+ byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(allRequestsListener).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ }
+
+ /**
+ * If a request with a specific session ID should be ignored no listeners should be notified.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldIgnoreInBandBytestreamRequestOnce() throws Exception {
+
+ // add listener for all request
+ InBandBytestreamListener allRequestsListener = mock(InBandBytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(allRequestsListener);
+
+ // add listener for request of user "initiator"
+ InBandBytestreamListener userRequestsListener = mock(InBandBytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(userRequestsListener, initiatorJID);
+
+ // ignore session ID
+ byteStreamManager.ignoreBytestreamRequestOnce(sessionID);
+
+ // run the listener with the initiation packet
+ initiationListener.processPacket(initBytestream);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // assert user request listener is not called
+ ArgumentCaptor byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(userRequestsListener, never()).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ // assert all requests listener is not called
+ byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(allRequestsListener, never()).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ // run the listener with the initiation packet again
+ initiationListener.processPacket(initBytestream);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // assert user request listener is called on the second request with the
+ // same session ID
+ verify(userRequestsListener).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ // assert all requests listener is not called
+ byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(allRequestsListener, never()).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/ibb/packet/CloseTest.java b/test-unit/org/jivesoftware/smackx/ibb/packet/CloseTest.java
new file mode 100644
index 000000000..319a5dae1
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/ibb/packet/CloseTest.java
@@ -0,0 +1,80 @@
+/**
+ * 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.ibb.packet;
+
+import static junit.framework.Assert.*;
+import static org.custommonkey.xmlunit.XMLAssert.*;
+
+import java.util.Properties;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.junit.Test;
+
+import com.jamesmurty.utils.XMLBuilder;
+
+/**
+ * Test for the Close class.
+ *
+ * @author Henning Staib
+ */
+public class CloseTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotInstantiateWithInvalidArguments1() {
+ new Close(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotInstantiateWithInvalidArguments2() {
+ new Close("");
+ }
+
+ @Test
+ public void shouldBeOfIQTypeSET() {
+ Close close = new Close("sessionID");
+ assertEquals(IQ.Type.SET, close.getType());
+ }
+
+ @Test
+ public void shouldSetAllFieldsCorrectly() {
+ Close close = new Close("sessionID");
+ assertEquals("sessionID", close.getSessionID());
+ }
+
+ private static Properties outputProperties = new Properties();
+ {
+ outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes");
+ }
+
+ @Test
+ public void shouldReturnValidIQStanzaXML() throws Exception {
+ String control = XMLBuilder.create("iq")
+ .a("from", "romeo@montague.lit/orchard")
+ .a("to", "juliet@capulet.lit/balcony")
+ .a("id", "us71g45j")
+ .a("type", "set")
+ .e("close")
+ .a("xmlns", "http://jabber.org/protocol/ibb")
+ .a("sid", "i781hf64")
+ .asString(outputProperties);
+
+ Close close = new Close("i781hf64");
+ close.setFrom("romeo@montague.lit/orchard");
+ close.setTo("juliet@capulet.lit/balcony");
+ close.setPacketID("us71g45j");
+
+ assertXMLEqual(control, close.toXML());
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/ibb/packet/DataPacketExtensionTest.java b/test-unit/org/jivesoftware/smackx/ibb/packet/DataPacketExtensionTest.java
new file mode 100644
index 000000000..9b0bb2a29
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/ibb/packet/DataPacketExtensionTest.java
@@ -0,0 +1,94 @@
+/**
+ * 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.ibb.packet;
+
+import static junit.framework.Assert.*;
+import static org.custommonkey.xmlunit.XMLAssert.*;
+
+import java.util.Properties;
+
+import org.junit.Test;
+
+import com.jamesmurty.utils.XMLBuilder;
+
+/**
+ * Test for the DataPacketExtension class.
+ *
+ * @author Henning Staib
+ */
+public class DataPacketExtensionTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotInstantiateWithInvalidArgument1() {
+ new DataPacketExtension(null, 0, "data");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotInstantiateWithInvalidArgument2() {
+ new DataPacketExtension("", 0, "data");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotInstantiateWithInvalidArgument3() {
+ new DataPacketExtension("sessionID", -1, "data");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotInstantiateWithInvalidArgument4() {
+ new DataPacketExtension("sessionID", 70000, "data");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotInstantiateWithInvalidArgument5() {
+ new DataPacketExtension("sessionID", 0, null);
+ }
+
+ @Test
+ public void shouldSetAllFieldsCorrectly() {
+ DataPacketExtension data = new DataPacketExtension("sessionID", 0, "data");
+ assertEquals("sessionID", data.getSessionID());
+ assertEquals(0, data.getSeq());
+ assertEquals("data", data.getData());
+ }
+
+ @Test
+ public void shouldReturnNullIfDataIsInvalid() {
+ // pad character is not at end of data
+ DataPacketExtension data = new DataPacketExtension("sessionID", 0, "BBBB=CCC");
+ assertNull(data.getDecodedData());
+
+ // invalid Base64 character
+ data = new DataPacketExtension("sessionID", 0, new String(new byte[] { 123 }));
+ assertNull(data.getDecodedData());
+ }
+
+ private static Properties outputProperties = new Properties();
+ {
+ outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes");
+ }
+
+ @Test
+ public void shouldReturnValidIQStanzaXML() throws Exception {
+ String control = XMLBuilder.create("data")
+ .a("xmlns", "http://jabber.org/protocol/ibb")
+ .a("seq", "0")
+ .a("sid", "i781hf64")
+ .t("DATA")
+ .asString(outputProperties);
+
+ DataPacketExtension data = new DataPacketExtension("i781hf64", 0, "DATA");
+ assertXMLEqual(control, data.toXML());
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/ibb/packet/DataTest.java b/test-unit/org/jivesoftware/smackx/ibb/packet/DataTest.java
new file mode 100644
index 000000000..be35ddc24
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/ibb/packet/DataTest.java
@@ -0,0 +1,84 @@
+/**
+ * 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.ibb.packet;
+
+import static junit.framework.Assert.*;
+import static org.custommonkey.xmlunit.XMLAssert.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Properties;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.util.Base64;
+import org.junit.Test;
+
+import com.jamesmurty.utils.XMLBuilder;
+
+/**
+ * Test for the Data class.
+ *
+ * @author Henning Staib
+ */
+public class DataTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotInstantiateWithInvalidArgument() {
+ new Data(null);
+ }
+
+ @Test
+ public void shouldBeOfIQTypeSET() {
+ DataPacketExtension dpe = mock(DataPacketExtension.class);
+ Data data = new Data(dpe);
+ assertEquals(IQ.Type.SET, data.getType());
+ }
+
+ private static Properties outputProperties = new Properties();
+ {
+ outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes");
+ }
+
+ @Test
+ public void shouldReturnValidIQStanzaXML() throws Exception {
+ String encodedData = Base64.encodeBytes("Test".getBytes());
+
+ String control = XMLBuilder.create("iq")
+ .a("from", "romeo@montague.lit/orchard")
+ .a("to", "juliet@capulet.lit/balcony")
+ .a("id", "kr91n475")
+ .a("type", "set")
+ .e("data")
+ .a("xmlns", "http://jabber.org/protocol/ibb")
+ .a("seq", "0")
+ .a("sid", "i781hf64")
+ .t(encodedData)
+ .asString(outputProperties);
+
+ DataPacketExtension dpe = mock(DataPacketExtension.class);
+ String dataTag = XMLBuilder.create("data")
+ .a("xmlns", "http://jabber.org/protocol/ibb")
+ .a("seq", "0")
+ .a("sid", "i781hf64")
+ .t(encodedData)
+ .asString(outputProperties);
+ when(dpe.toXML()).thenReturn(dataTag);
+ Data data = new Data(dpe);
+ data.setFrom("romeo@montague.lit/orchard");
+ data.setTo("juliet@capulet.lit/balcony");
+ data.setPacketID("kr91n475");
+
+ assertXMLEqual(control, data.toXML());
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/ibb/packet/OpenTest.java b/test-unit/org/jivesoftware/smackx/ibb/packet/OpenTest.java
new file mode 100644
index 000000000..029b9a413
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/ibb/packet/OpenTest.java
@@ -0,0 +1,103 @@
+/**
+ * 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.ibb.packet;
+
+import static junit.framework.Assert.*;
+import static org.custommonkey.xmlunit.XMLAssert.*;
+
+import java.util.Properties;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.ibb.InBandBytestreamManager.StanzaType;
+import org.junit.Test;
+
+import com.jamesmurty.utils.XMLBuilder;
+
+/**
+ * Test for the Open class.
+ *
+ * @author Henning Staib
+ */
+public class OpenTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotInstantiateWithInvalidArguments1() {
+ new Open(null, 1);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotInstantiateWithInvalidArguments2() {
+ new Open("", 1);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void shouldNotInstantiateWithInvalidArguments3() {
+ new Open("sessionID", -1);
+ }
+
+ @Test
+ public void shouldSetIQStanzaAsDefault() {
+ Open open = new Open("sessionID", 4096);
+ assertEquals(StanzaType.IQ, open.getStanza());
+ }
+
+ @Test
+ public void shouldUseMessageStanzaIfGiven() {
+ Open open = new Open("sessionID", 4096, StanzaType.MESSAGE);
+ assertEquals(StanzaType.MESSAGE, open.getStanza());
+ }
+
+ @Test
+ public void shouldBeOfIQTypeSET() {
+ Open open = new Open("sessionID", 4096);
+ assertEquals(IQ.Type.SET, open.getType());
+ }
+
+ @Test
+ public void shouldSetAllFieldsCorrectly() {
+ Open open = new Open("sessionID", 4096, StanzaType.MESSAGE);
+ assertEquals("sessionID", open.getSessionID());
+ assertEquals(4096, open.getBlockSize());
+ assertEquals(StanzaType.MESSAGE, open.getStanza());
+ }
+
+ private static Properties outputProperties = new Properties();
+ {
+ outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes");
+ }
+
+ @Test
+ public void shouldReturnValidIQStanzaXML() throws Exception {
+ String control = XMLBuilder.create("iq")
+ .a("from", "romeo@montague.lit/orchard")
+ .a("to", "juliet@capulet.lit/balcony")
+ .a("id", "jn3h8g65")
+ .a("type", "set")
+ .e("open")
+ .a("xmlns", "http://jabber.org/protocol/ibb")
+ .a("block-size", "4096")
+ .a("sid", "i781hf64")
+ .a("stanza", "iq")
+ .asString(outputProperties);
+
+ Open open = new Open("i781hf64", 4096, StanzaType.IQ);
+ open.setFrom("romeo@montague.lit/orchard");
+ open.setTo("juliet@capulet.lit/balcony");
+ open.setPacketID("jn3h8g65");
+
+ assertXMLEqual(control, open.toXML());
+ }
+
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/ibb/provider/OpenIQProviderTest.java b/test-unit/org/jivesoftware/smackx/ibb/provider/OpenIQProviderTest.java
new file mode 100644
index 000000000..a243baad1
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/ibb/provider/OpenIQProviderTest.java
@@ -0,0 +1,86 @@
+/**
+ * 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.ibb.provider;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Properties;
+
+import org.jivesoftware.smackx.ibb.InBandBytestreamManager.StanzaType;
+import org.jivesoftware.smackx.ibb.packet.Open;
+import org.junit.Test;
+import org.xmlpull.mxp1.MXParser;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import com.jamesmurty.utils.XMLBuilder;
+
+/**
+ * Test for the OpenIQProvider class.
+ *
+ * @author Henning Staib
+ */
+public class OpenIQProviderTest {
+
+ private static Properties outputProperties = new Properties();
+ {
+ outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes");
+ }
+
+ @Test
+ public void shouldCorrectlyParseIQStanzaAttribute() throws Exception {
+ String control = XMLBuilder.create("open")
+ .a("xmlns", "http://jabber.org/protocol/ibb")
+ .a("block-size", "4096")
+ .a("sid", "i781hf64")
+ .a("stanza", "iq")
+ .asString(outputProperties);
+
+ OpenIQProvider oip = new OpenIQProvider();
+ Open open = (Open) oip.parseIQ(getParser(control));
+
+ assertEquals(StanzaType.IQ, open.getStanza());
+ }
+
+ @Test
+ public void shouldCorrectlyParseMessageStanzaAttribute() throws Exception {
+ String control = XMLBuilder.create("open")
+ .a("xmlns", "http://jabber.org/protocol/ibb")
+ .a("block-size", "4096")
+ .a("sid", "i781hf64")
+ .a("stanza", "message")
+ .asString(outputProperties);
+
+ OpenIQProvider oip = new OpenIQProvider();
+ Open open = (Open) oip.parseIQ(getParser(control));
+
+ assertEquals(StanzaType.MESSAGE, open.getStanza());
+ }
+
+ private XmlPullParser getParser(String control) throws XmlPullParserException,
+ IOException {
+ XmlPullParser parser = new MXParser();
+ parser.setInput(new StringReader(control));
+ while (true) {
+ if (parser.next() == XmlPullParser.START_TAG
+ && parser.getName().equals("open")) {
+ break;
+ }
+ }
+ return parser;
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/socks5bytestream/InitiationListenerTest.java b/test-unit/org/jivesoftware/smackx/socks5bytestream/InitiationListenerTest.java
new file mode 100644
index 000000000..4358650ce
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/socks5bytestream/InitiationListenerTest.java
@@ -0,0 +1,305 @@
+/**
+ * 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.socks5bytestream;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
+import org.jivesoftware.smackx.socks5bytestream.packet.Bytestream;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.powermock.reflect.Whitebox;
+
+/**
+ * Test for the InitiationListener class.
+ *
+ * @author Henning Staib
+ */
+public class InitiationListenerTest {
+
+ String initiatorJID = "initiator@xmpp-server/Smack";
+ String targetJID = "target@xmpp-server/Smack";
+ String xmppServer = "xmpp-server";
+ String proxyJID = "proxy.xmpp-server";
+ String proxyAddress = "127.0.0.1";
+ String sessionID = "session_id";
+
+ Connection connection;
+ Socks5BytestreamManager byteStreamManager;
+ InitiationListener initiationListener;
+ Bytestream initBytestream;
+
+ /**
+ * Initialize fields used in the tests.
+ */
+ @Before
+ public void setup() {
+
+ // mock connection
+ connection = mock(Connection.class);
+
+ // create service discovery manager for mocked connection
+ new ServiceDiscoveryManager(connection);
+
+ // initialize Socks5ByteStreamManager to get the InitiationListener
+ byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ // get the InitiationListener from Socks5ByteStreamManager
+ initiationListener = Whitebox.getInternalState(byteStreamManager, InitiationListener.class);
+
+ // create a SOCKS5 Bytestream initiation packet
+ initBytestream = Socks5PacketUtils.createBytestreamInitiation(initiatorJID, targetJID,
+ sessionID);
+ initBytestream.addStreamHost(proxyJID, proxyAddress, 7777);
+
+ }
+
+ /**
+ * If no listeners are registered for incoming SOCKS5 Bytestream requests, all request should be
+ * rejected with an error.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldRespondWithError() throws Exception {
+
+ // run the listener with the initiation packet
+ initiationListener.processPacket(initBytestream);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // capture reply to the SOCKS5 Bytestream initiation
+ ArgumentCaptor argument = ArgumentCaptor.forClass(IQ.class);
+ verify(connection).sendPacket(argument.capture());
+
+ // assert that reply is the correct error packet
+ assertEquals(initiatorJID, argument.getValue().getTo());
+ assertEquals(IQ.Type.ERROR, argument.getValue().getType());
+ assertEquals(XMPPError.Condition.no_acceptable.toString(),
+ argument.getValue().getError().getCondition());
+
+ }
+
+ /**
+ * If a listener for all requests is registered it should be notified on incoming requests.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldInvokeListenerForAllRequests() throws Exception {
+
+ // add listener
+ Socks5BytestreamListener listener = mock(Socks5BytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(listener);
+
+ // run the listener with the initiation packet
+ initiationListener.processPacket(initBytestream);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // assert listener is called once
+ ArgumentCaptor byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(listener).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ // assert that listener is called for the correct request
+ assertEquals(initiatorJID, byteStreamRequest.getValue().getFrom());
+
+ }
+
+ /**
+ * If a listener for a specific user in registered it should be notified on incoming requests
+ * for that user.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldInvokeListenerForUser() throws Exception {
+
+ // add listener
+ Socks5BytestreamListener listener = mock(Socks5BytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(listener, initiatorJID);
+
+ // run the listener with the initiation packet
+ initiationListener.processPacket(initBytestream);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // assert listener is called once
+ ArgumentCaptor byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(listener).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ // assert that reply is the correct error packet
+ assertEquals(initiatorJID, byteStreamRequest.getValue().getFrom());
+
+ }
+
+ /**
+ * If listener for a specific user is registered it should not be notified on incoming requests
+ * from other users.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldNotInvokeListenerForUser() throws Exception {
+
+ // add listener for request of user "other_initiator"
+ Socks5BytestreamListener listener = mock(Socks5BytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(listener, "other_" + initiatorJID);
+
+ // run the listener with the initiation packet
+ initiationListener.processPacket(initBytestream);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // assert listener is not called
+ ArgumentCaptor byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(listener, never()).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ // capture reply to the SOCKS5 Bytestream initiation
+ ArgumentCaptor argument = ArgumentCaptor.forClass(IQ.class);
+ verify(connection).sendPacket(argument.capture());
+
+ // assert that reply is the correct error packet
+ assertEquals(initiatorJID, argument.getValue().getTo());
+ assertEquals(IQ.Type.ERROR, argument.getValue().getType());
+ assertEquals(XMPPError.Condition.no_acceptable.toString(),
+ argument.getValue().getError().getCondition());
+ }
+
+ /**
+ * If a user specific listener and an all requests listener is registered only the user specific
+ * listener should be notified.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldNotInvokeAllRequestsListenerIfUserListenerExists() throws Exception {
+
+ // add listener for all request
+ Socks5BytestreamListener allRequestsListener = mock(Socks5BytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(allRequestsListener);
+
+ // add listener for request of user "initiator"
+ Socks5BytestreamListener userRequestsListener = mock(Socks5BytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(userRequestsListener, initiatorJID);
+
+ // run the listener with the initiation packet
+ initiationListener.processPacket(initBytestream);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // assert user request listener is called once
+ ArgumentCaptor byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(userRequestsListener).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ // assert all requests listener is not called
+ byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(allRequestsListener, never()).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ }
+
+ /**
+ * If a user specific listener and an all requests listener is registered only the all requests
+ * listener should be notified on an incoming request for another user.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldInvokeAllRequestsListenerIfUserListenerExists() throws Exception {
+
+ // add listener for all request
+ Socks5BytestreamListener allRequestsListener = mock(Socks5BytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(allRequestsListener);
+
+ // add listener for request of user "other_initiator"
+ Socks5BytestreamListener userRequestsListener = mock(Socks5BytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(userRequestsListener, "other_"
+ + initiatorJID);
+
+ // run the listener with the initiation packet
+ initiationListener.processPacket(initBytestream);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // assert user request listener is not called
+ ArgumentCaptor byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(userRequestsListener, never()).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ // assert all requests listener is called
+ byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(allRequestsListener).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ }
+
+ /**
+ * If a request with a specific session ID should be ignored no listeners should be notified.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldIgnoreSocks5BytestreamRequestOnce() throws Exception {
+
+ // add listener for all request
+ Socks5BytestreamListener allRequestsListener = mock(Socks5BytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(allRequestsListener);
+
+ // add listener for request of user "initiator"
+ Socks5BytestreamListener userRequestsListener = mock(Socks5BytestreamListener.class);
+ byteStreamManager.addIncomingBytestreamListener(userRequestsListener, initiatorJID);
+
+ // ignore session ID
+ byteStreamManager.ignoreBytestreamRequestOnce(sessionID);
+
+ // run the listener with the initiation packet
+ initiationListener.processPacket(initBytestream);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // assert user request listener is not called
+ ArgumentCaptor byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(userRequestsListener, never()).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ // assert all requests listener is not called
+ byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(allRequestsListener, never()).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ // run the listener with the initiation packet again
+ initiationListener.processPacket(initBytestream);
+
+ // wait because packet is processed in an extra thread
+ Thread.sleep(200);
+
+ // assert user request listener is called on the second request with the same session ID
+ verify(userRequestsListener).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ // assert all requests listener is not called
+ byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
+ verify(allRequestsListener, never()).incomingBytestreamRequest(byteStreamRequest.capture());
+
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5ByteStreamManagerTest.java b/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5ByteStreamManagerTest.java
new file mode 100644
index 000000000..d20e2f3a5
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5ByteStreamManagerTest.java
@@ -0,0 +1,1098 @@
+/**
+ * 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.socks5bytestream;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ConnectException;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.packet.IQ.Type;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+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;
+import org.jivesoftware.smackx.socks5bytestream.packet.Bytestream;
+import org.jivesoftware.smackx.socks5bytestream.packet.Bytestream.StreamHost;
+import org.jivesoftware.util.ConnectionUtils;
+import org.jivesoftware.util.Protocol;
+import org.jivesoftware.util.Verification;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test for Socks5BytestreamManager.
+ *
+ * @author Henning Staib
+ */
+public class Socks5ByteStreamManagerTest {
+
+ // settings
+ String initiatorJID = "initiator@xmpp-server/Smack";
+ String targetJID = "target@xmpp-server/Smack";
+ String xmppServer = "xmpp-server";
+ String proxyJID = "proxy.xmpp-server";
+ String proxyAddress = "127.0.0.1";
+ String sessionID = "session_id";
+
+ // protocol verifier
+ Protocol protocol;
+
+ // mocked XMPP connection
+ Connection connection;
+
+ /**
+ * Initialize fields used in the tests.
+ */
+ @Before
+ public void setup() {
+
+ // build protocol verifier
+ protocol = new Protocol();
+
+ // create mocked XMPP connection
+ connection = ConnectionUtils.createMockedConnection(protocol, initiatorJID, xmppServer);
+
+ }
+
+ /**
+ * Test that {@link Socks5BytestreamManager#getBytestreamManager(Connection)} returns one
+ * bytestream manager for every connection
+ */
+ @Test
+ public void shouldHaveOneManagerForEveryConnection() {
+
+ // mock two connections
+ Connection connection1 = mock(Connection.class);
+ Connection connection2 = mock(Connection.class);
+
+ /*
+ * create service discovery managers for the connections because the
+ * ConnectionCreationListener is not called when creating mocked connections
+ */
+ new ServiceDiscoveryManager(connection1);
+ new ServiceDiscoveryManager(connection2);
+
+ // get bytestream manager for the first connection twice
+ Socks5BytestreamManager conn1ByteStreamManager1 = Socks5BytestreamManager.getBytestreamManager(connection1);
+ Socks5BytestreamManager conn1ByteStreamManager2 = Socks5BytestreamManager.getBytestreamManager(connection1);
+
+ // get bytestream manager for second connection
+ Socks5BytestreamManager conn2ByteStreamManager1 = Socks5BytestreamManager.getBytestreamManager(connection2);
+
+ // assertions
+ assertEquals(conn1ByteStreamManager1, conn1ByteStreamManager2);
+ assertNotSame(conn1ByteStreamManager1, conn2ByteStreamManager1);
+
+ }
+
+ /**
+ * The SOCKS5 Bytestream feature should be removed form the service discovery manager if Socks5
+ * bytestream feature is disabled.
+ */
+ @Test
+ public void shouldDisableService() {
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+ ServiceDiscoveryManager discoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
+
+ assertTrue(discoveryManager.includesFeature(Socks5BytestreamManager.NAMESPACE));
+
+ byteStreamManager.disableService();
+
+ assertFalse(discoveryManager.includesFeature(Socks5BytestreamManager.NAMESPACE));
+ }
+
+ /**
+ * Invoking {@link Socks5BytestreamManager#establishSession(String)} should throw an exception
+ * if the given target does not support SOCKS5 Bytestream.
+ */
+ @Test
+ public void shouldFailIfTargetDoesNotSupportSocks5() {
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ try {
+ // build empty discover info as reply if targets features are queried
+ DiscoverInfo discoverInfo = new DiscoverInfo();
+ protocol.addResponse(discoverInfo);
+
+ // start SOCKS5 Bytestream
+ byteStreamManager.establishSession(targetJID);
+
+ fail("exception should be thrown");
+ }
+ catch (XMPPException e) {
+ assertTrue(e.getMessage().contains("doesn't support SOCKS5 Bytestream"));
+ }
+ catch (IOException e) {
+ fail(e.getMessage());
+ }
+ catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+
+ }
+
+ /**
+ * Invoking {@link Socks5BytestreamManager#establishSession(String, String)} should fail if XMPP
+ * server doesn't return any proxies.
+ */
+ @Test
+ public void shouldFailIfNoSocks5ProxyFound1() {
+
+ // disable clients local SOCKS5 proxy
+ SmackConfiguration.setLocalSocks5ProxyEnabled(false);
+
+ // get Socks5ByteStreamManager for connection
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ /**
+ * create responses in the order they should be queried specified by the XEP-0065
+ * specification
+ */
+
+ // build discover info that supports the SOCKS5 feature
+ DiscoverInfo discoverInfo = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
+ discoverInfo.addFeature(Socks5BytestreamManager.NAMESPACE);
+
+ // return that SOCKS5 is supported if target is queried
+ protocol.addResponse(discoverInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build discover items with no proxy items
+ DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
+ initiatorJID);
+
+ // return the item with no proxy if XMPP server is queried
+ protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ try {
+
+ // start SOCKS5 Bytestream
+ byteStreamManager.establishSession(targetJID, sessionID);
+
+ fail("exception should be thrown");
+ }
+ catch (XMPPException e) {
+ protocol.verifyAll();
+ assertTrue(e.getMessage().contains("no SOCKS5 proxies available"));
+ }
+ catch (Exception e) {
+ fail(e.getMessage());
+ }
+
+ }
+
+ /**
+ * Invoking {@link Socks5BytestreamManager#establishSession(String, String)} should fail if no
+ * proxy is a SOCKS5 proxy.
+ */
+ @Test
+ public void shouldFailIfNoSocks5ProxyFound2() {
+
+ // disable clients local SOCKS5 proxy
+ SmackConfiguration.setLocalSocks5ProxyEnabled(false);
+
+ // get Socks5ByteStreamManager for connection
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ /**
+ * create responses in the order they should be queried specified by the XEP-0065
+ * specification
+ */
+
+ // build discover info that supports the SOCKS5 feature
+ DiscoverInfo discoverInfo = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
+ discoverInfo.addFeature(Socks5BytestreamManager.NAMESPACE);
+
+ // return that SOCKS5 is supported if target is queried
+ protocol.addResponse(discoverInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build discover items containing a proxy item
+ DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
+ initiatorJID);
+ Item item = new Item(proxyJID);
+ discoverItems.addItem(item);
+
+ // return the proxy item if XMPP server is queried
+ protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build discover info for proxy containing information about NOT being a Socks5
+ // proxy
+ DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
+ Identity identity = new Identity("noproxy", proxyJID);
+ identity.setType("bytestreams");
+ proxyInfo.addIdentity(identity);
+
+ // return the proxy identity if proxy is queried
+ protocol.addResponse(proxyInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ try {
+
+ // start SOCKS5 Bytestream
+ byteStreamManager.establishSession(targetJID, sessionID);
+
+ fail("exception should be thrown");
+ }
+ catch (XMPPException e) {
+ protocol.verifyAll();
+ assertTrue(e.getMessage().contains("no SOCKS5 proxies available"));
+ }
+ catch (Exception e) {
+ fail(e.getMessage());
+ }
+
+ }
+
+ /**
+ * Invoking {@link Socks5BytestreamManager#establishSession(String, String)} should fail if no
+ * SOCKS5 proxy can be found. If it turns out that a proxy is not a SOCKS5 proxy it should not
+ * be queried again.
+ */
+ @Test
+ public void shouldBlacklistNonSocks5Proxies() {
+
+ // disable clients local SOCKS5 proxy
+ SmackConfiguration.setLocalSocks5ProxyEnabled(false);
+
+ // get Socks5ByteStreamManager for connection
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ /**
+ * create responses in the order they should be queried specified by the XEP-0065
+ * specification
+ */
+
+ // build discover info that supports the SOCKS5 feature
+ DiscoverInfo discoverInfo = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
+ discoverInfo.addFeature(Socks5BytestreamManager.NAMESPACE);
+
+ // return that SOCKS5 is supported if target is queried
+ protocol.addResponse(discoverInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build discover items containing a proxy item
+ DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
+ initiatorJID);
+ Item item = new Item(proxyJID);
+ discoverItems.addItem(item);
+
+ // return the proxy item if XMPP server is queried
+ protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build discover info for proxy containing information about NOT being a Socks5
+ // proxy
+ DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
+ Identity identity = new Identity("noproxy", proxyJID);
+ identity.setType("bytestreams");
+ proxyInfo.addIdentity(identity);
+
+ // return the proxy identity if proxy is queried
+ protocol.addResponse(proxyInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ try {
+
+ // start SOCKS5 Bytestream
+ byteStreamManager.establishSession(targetJID, sessionID);
+
+ fail("exception should be thrown");
+ }
+ catch (XMPPException e) {
+ protocol.verifyAll();
+ assertTrue(e.getMessage().contains("no SOCKS5 proxies available"));
+ }
+ catch (Exception e) {
+ fail(e.getMessage());
+ }
+
+ /* retry to establish SOCKS5 Bytestream */
+
+ // add responses for service discovery again
+ protocol.addResponse(discoverInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+ protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ try {
+
+ // start SOCKS5 Bytestream
+ byteStreamManager.establishSession(targetJID, sessionID);
+
+ fail("exception should be thrown");
+ }
+ catch (XMPPException e) {
+ /*
+ * #verifyAll() tests if the number of requests and responses corresponds and should
+ * fail if the invalid proxy is queried again
+ */
+ protocol.verifyAll();
+ assertTrue(e.getMessage().contains("no SOCKS5 proxies available"));
+ }
+ catch (Exception e) {
+ fail(e.getMessage());
+ }
+
+ }
+
+ /**
+ * Invoking {@link Socks5BytestreamManager#establishSession(String, String)} should fail if the
+ * target does not accept a SOCKS5 Bytestream. See XEP-0065 Section 5.2 A2
+ */
+ @Test
+ public void shouldFailIfTargetDoesNotAcceptSocks5Bytestream() {
+
+ // disable clients local SOCKS5 proxy
+ SmackConfiguration.setLocalSocks5ProxyEnabled(false);
+
+ // get Socks5ByteStreamManager for connection
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ /**
+ * create responses in the order they should be queried specified by the XEP-0065
+ * specification
+ */
+
+ // build discover info that supports the SOCKS5 feature
+ DiscoverInfo discoverInfo = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
+ discoverInfo.addFeature(Socks5BytestreamManager.NAMESPACE);
+
+ // return that SOCKS5 is supported if target is queried
+ protocol.addResponse(discoverInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build discover items containing a proxy item
+ DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
+ initiatorJID);
+ Item item = new Item(proxyJID);
+ discoverItems.addItem(item);
+
+ // return the proxy item if XMPP server is queried
+ protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build discover info for proxy containing information about being a SOCKS5 proxy
+ DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
+ Identity identity = new Identity("proxy", proxyJID);
+ identity.setType("bytestreams");
+ proxyInfo.addIdentity(identity);
+
+ // return the socks5 bytestream proxy identity if proxy is queried
+ protocol.addResponse(proxyInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build a socks5 stream host info containing the address and the port of the
+ // proxy
+ Bytestream streamHostInfo = Socks5PacketUtils.createBytestreamResponse(proxyJID,
+ initiatorJID);
+ streamHostInfo.addStreamHost(proxyJID, proxyAddress, 7778);
+
+ // return stream host info if it is queried
+ protocol.addResponse(streamHostInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build error packet to reject SOCKS5 Bytestream
+ XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable);
+ IQ rejectPacket = new IQ() {
+
+ public String getChildElementXML() {
+ return null;
+ }
+
+ };
+ rejectPacket.setType(Type.ERROR);
+ rejectPacket.setFrom(targetJID);
+ rejectPacket.setTo(initiatorJID);
+ rejectPacket.setError(xmppError);
+
+ // return error packet as response to the bytestream initiation
+ protocol.addResponse(rejectPacket, Verification.correspondingSenderReceiver,
+ Verification.requestTypeSET);
+
+ try {
+
+ // start SOCKS5 Bytestream
+ byteStreamManager.establishSession(targetJID, sessionID);
+
+ fail("exception should be thrown");
+ }
+ catch (XMPPException e) {
+ protocol.verifyAll();
+ assertEquals(xmppError, e.getXMPPError());
+ }
+ catch (Exception e) {
+ fail(e.getMessage());
+ }
+
+ }
+
+ /**
+ * Invoking {@link Socks5BytestreamManager#establishSession(String, String)} should fail if the
+ * proxy used by target is invalid.
+ */
+ @Test
+ public void shouldFailIfTargetUsesInvalidSocks5Proxy() {
+
+ // disable clients local SOCKS5 proxy
+ SmackConfiguration.setLocalSocks5ProxyEnabled(false);
+
+ // get Socks5ByteStreamManager for connection
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ /**
+ * create responses in the order they should be queried specified by the XEP-0065
+ * specification
+ */
+
+ // build discover info that supports the SOCKS5 feature
+ DiscoverInfo discoverInfo = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
+ discoverInfo.addFeature(Socks5BytestreamManager.NAMESPACE);
+
+ // return that SOCKS5 is supported if target is queried
+ protocol.addResponse(discoverInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build discover items containing a proxy item
+ DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
+ initiatorJID);
+ Item item = new Item(proxyJID);
+ discoverItems.addItem(item);
+
+ // return the proxy item if XMPP server is queried
+ protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build discover info for proxy containing information about being a SOCKS5 proxy
+ DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
+ Identity identity = new Identity("proxy", proxyJID);
+ identity.setType("bytestreams");
+ proxyInfo.addIdentity(identity);
+
+ // return the socks5 bytestream proxy identity if proxy is queried
+ protocol.addResponse(proxyInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build a socks5 stream host info containing the address and the port of the
+ // proxy
+ Bytestream streamHostInfo = Socks5PacketUtils.createBytestreamResponse(proxyJID,
+ initiatorJID);
+ streamHostInfo.addStreamHost(proxyJID, proxyAddress, 7778);
+
+ // return stream host info if it is queried
+ protocol.addResponse(streamHostInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build used stream host response with unknown proxy
+ Bytestream streamHostUsedPacket = Socks5PacketUtils.createBytestreamResponse(targetJID,
+ initiatorJID);
+ streamHostUsedPacket.setSessionID(sessionID);
+ streamHostUsedPacket.setUsedHost("invalid.proxy");
+
+ // return used stream host info as response to the bytestream initiation
+ protocol.addResponse(streamHostUsedPacket, Verification.correspondingSenderReceiver,
+ Verification.requestTypeSET);
+
+ try {
+
+ // start SOCKS5 Bytestream
+ byteStreamManager.establishSession(targetJID, sessionID);
+
+ fail("exception should be thrown");
+ }
+ catch (XMPPException e) {
+ protocol.verifyAll();
+ assertTrue(e.getMessage().contains("Remote user responded with unknown host"));
+ }
+ catch (Exception e) {
+ fail(e.getMessage());
+ }
+
+ }
+
+ /**
+ * Invoking {@link Socks5BytestreamManager#establishSession(String, String)} should fail if
+ * initiator can not connect to the SOCKS5 proxy used by target.
+ */
+ @Test
+ public void shouldFailIfInitiatorCannotConnectToSocks5Proxy() {
+
+ // disable clients local SOCKS5 proxy
+ SmackConfiguration.setLocalSocks5ProxyEnabled(false);
+
+ // get Socks5ByteStreamManager for connection
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ /**
+ * create responses in the order they should be queried specified by the XEP-0065
+ * specification
+ */
+
+ // build discover info that supports the SOCKS5 feature
+ DiscoverInfo discoverInfo = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
+ discoverInfo.addFeature(Socks5BytestreamManager.NAMESPACE);
+
+ // return that SOCKS5 is supported if target is queried
+ protocol.addResponse(discoverInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build discover items containing a proxy item
+ DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
+ initiatorJID);
+ Item item = new Item(proxyJID);
+ discoverItems.addItem(item);
+
+ // return the proxy item if XMPP server is queried
+ protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build discover info for proxy containing information about being a SOCKS5 proxy
+ DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
+ Identity identity = new Identity("proxy", proxyJID);
+ identity.setType("bytestreams");
+ proxyInfo.addIdentity(identity);
+
+ // return the socks5 bytestream proxy identity if proxy is queried
+ protocol.addResponse(proxyInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build a socks5 stream host info containing the address and the port of the
+ // proxy
+ Bytestream streamHostInfo = Socks5PacketUtils.createBytestreamResponse(proxyJID,
+ initiatorJID);
+ streamHostInfo.addStreamHost(proxyJID, proxyAddress, 7778);
+
+ // return stream host info if it is queried
+ protocol.addResponse(streamHostInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build used stream host response
+ Bytestream streamHostUsedPacket = Socks5PacketUtils.createBytestreamResponse(targetJID,
+ initiatorJID);
+ streamHostUsedPacket.setSessionID(sessionID);
+ streamHostUsedPacket.setUsedHost(proxyJID);
+
+ // return used stream host info as response to the bytestream initiation
+ protocol.addResponse(streamHostUsedPacket, new Verification() {
+
+ public void verify(Bytestream request, Bytestream response) {
+ // verify SOCKS5 Bytestream request
+ assertEquals(response.getSessionID(), request.getSessionID());
+ assertEquals(1, request.getStreamHosts().size());
+ StreamHost streamHost = (StreamHost) request.getStreamHosts().toArray()[0];
+ assertEquals(response.getUsedHost().getJID(), streamHost.getJID());
+ }
+
+ }, Verification.correspondingSenderReceiver, Verification.requestTypeSET);
+
+ try {
+
+ // start SOCKS5 Bytestream
+ byteStreamManager.establishSession(targetJID, sessionID);
+
+ fail("exception should be thrown");
+ }
+ catch (IOException e) {
+ // initiator can't connect to proxy because it is not running
+ protocol.verifyAll();
+ assertEquals(ConnectException.class, e.getClass());
+ }
+ catch (Exception e) {
+ fail(e.getMessage());
+ }
+
+ }
+
+ /**
+ * Invoking {@link Socks5BytestreamManager#establishSession(String, String)} should successfully
+ * negotiate and return a SOCKS5 Bytestream connection.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldNegotiateSocks5BytestreamAndTransferData() throws Exception {
+
+ // disable clients local SOCKS5 proxy
+ SmackConfiguration.setLocalSocks5ProxyEnabled(false);
+
+ // get Socks5ByteStreamManager for connection
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ /**
+ * create responses in the order they should be queried specified by the XEP-0065
+ * specification
+ */
+
+ // build discover info that supports the SOCKS5 feature
+ DiscoverInfo discoverInfo = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
+ discoverInfo.addFeature(Socks5BytestreamManager.NAMESPACE);
+
+ // return that SOCKS5 is supported if target is queried
+ protocol.addResponse(discoverInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build discover items containing a proxy item
+ DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
+ initiatorJID);
+ Item item = new Item(proxyJID);
+ discoverItems.addItem(item);
+
+ // return the proxy item if XMPP server is queried
+ protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build discover info for proxy containing information about being a SOCKS5 proxy
+ DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
+ Identity identity = new Identity("proxy", proxyJID);
+ identity.setType("bytestreams");
+ proxyInfo.addIdentity(identity);
+
+ // return the socks5 bytestream proxy identity if proxy is queried
+ protocol.addResponse(proxyInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build a socks5 stream host info containing the address and the port of the
+ // proxy
+ Bytestream streamHostInfo = Socks5PacketUtils.createBytestreamResponse(proxyJID,
+ initiatorJID);
+ streamHostInfo.addStreamHost(proxyJID, proxyAddress, 7778);
+
+ // return stream host info if it is queried
+ protocol.addResponse(streamHostInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build used stream host response
+ Bytestream streamHostUsedPacket = Socks5PacketUtils.createBytestreamResponse(targetJID,
+ initiatorJID);
+ streamHostUsedPacket.setSessionID(sessionID);
+ streamHostUsedPacket.setUsedHost(proxyJID);
+
+ // return used stream host info as response to the bytestream initiation
+ protocol.addResponse(streamHostUsedPacket, new Verification() {
+
+ public void verify(Bytestream request, Bytestream response) {
+ assertEquals(response.getSessionID(), request.getSessionID());
+ assertEquals(1, request.getStreamHosts().size());
+ StreamHost streamHost = (StreamHost) request.getStreamHosts().toArray()[0];
+ assertEquals(response.getUsedHost().getJID(), streamHost.getJID());
+ }
+
+ }, Verification.correspondingSenderReceiver, Verification.requestTypeSET);
+
+ // build response to proxy activation
+ IQ activationResponse = Socks5PacketUtils.createActivationConfirmation(proxyJID,
+ initiatorJID);
+
+ // return proxy activation response if proxy should be activated
+ protocol.addResponse(activationResponse, new Verification() {
+
+ public void verify(Bytestream request, IQ response) {
+ assertEquals(targetJID, request.getToActivate().getTarget());
+ }
+
+ }, Verification.correspondingSenderReceiver, Verification.requestTypeSET);
+
+ // start a local SOCKS5 proxy
+ Socks5TestProxy socks5Proxy = Socks5TestProxy.getProxy(7778);
+ socks5Proxy.start();
+
+ // create digest to get the socket opened by target
+ String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);
+
+ // finally call the method that should be tested
+ OutputStream outputStream = byteStreamManager.establishSession(targetJID, sessionID).getOutputStream();
+
+ // test the established bytestream
+ InputStream inputStream = socks5Proxy.getSocket(digest).getInputStream();
+
+ byte[] data = new byte[] { 1, 2, 3 };
+ outputStream.write(data);
+
+ byte[] result = new byte[3];
+ inputStream.read(result);
+
+ assertArrayEquals(data, result);
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * If multiple network addresses are added to the local SOCKS5 proxy, all of them should be
+ * contained in the SOCKS5 Bytestream request.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldUseMultipleAddressesForLocalSocks5Proxy() throws Exception {
+
+ // enable clients local SOCKS5 proxy on port 7778
+ SmackConfiguration.setLocalSocks5ProxyEnabled(true);
+ SmackConfiguration.setLocalSocks5ProxyPort(7778);
+
+ // start a local SOCKS5 proxy
+ Socks5Proxy socks5Proxy = Socks5Proxy.getSocks5Proxy();
+ socks5Proxy.start();
+ assertTrue(socks5Proxy.isRunning());
+
+ // get Socks5ByteStreamManager for connection
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ /**
+ * create responses in the order they should be queried specified by the XEP-0065
+ * specification
+ */
+
+ // build discover info that supports the SOCKS5 feature
+ DiscoverInfo discoverInfo = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
+ discoverInfo.addFeature(Socks5BytestreamManager.NAMESPACE);
+
+ // return that SOCKS5 is supported if target is queried
+ protocol.addResponse(discoverInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build discover items containing no proxy item
+ DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
+ initiatorJID);
+
+ // return the discover item if XMPP server is queried
+ protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build used stream host response
+ Bytestream streamHostUsedPacket = Socks5PacketUtils.createBytestreamResponse(targetJID,
+ initiatorJID);
+ streamHostUsedPacket.setSessionID(sessionID);
+ streamHostUsedPacket.setUsedHost(initiatorJID); // local proxy used
+
+ // return used stream host info as response to the bytestream initiation
+ protocol.addResponse(streamHostUsedPacket, new Verification() {
+
+ public void verify(Bytestream request, Bytestream response) {
+ assertEquals(response.getSessionID(), request.getSessionID());
+ assertEquals(2, request.getStreamHosts().size());
+ StreamHost streamHost1 = (StreamHost) request.getStreamHosts().toArray()[0];
+ assertEquals(response.getUsedHost().getJID(), streamHost1.getJID());
+ StreamHost streamHost2 = (StreamHost) request.getStreamHosts().toArray()[1];
+ assertEquals(response.getUsedHost().getJID(), streamHost2.getJID());
+ assertEquals("localAddress", streamHost2.getAddress());
+ }
+
+ }, Verification.correspondingSenderReceiver, Verification.requestTypeSET);
+
+ // create digest to get the socket opened by target
+ String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);
+
+ // connect to proxy as target
+ socks5Proxy.addTransfer(digest);
+ StreamHost streamHost = new StreamHost(targetJID, socks5Proxy.getLocalAddresses().get(0));
+ streamHost.setPort(socks5Proxy.getPort());
+ Socks5Client socks5Client = new Socks5Client(streamHost, digest);
+ InputStream inputStream = socks5Client.getSocket(2000).getInputStream();
+
+ // add another network address before establishing SOCKS5 Bytestream
+ socks5Proxy.addLocalAddress("localAddress");
+
+ // finally call the method that should be tested
+ OutputStream outputStream = byteStreamManager.establishSession(targetJID, sessionID).getOutputStream();
+
+ // test the established bytestream
+ byte[] data = new byte[] { 1, 2, 3 };
+ outputStream.write(data);
+
+ byte[] result = new byte[3];
+ inputStream.read(result);
+
+ assertArrayEquals(data, result);
+
+ protocol.verifyAll();
+
+ // reset proxy settings
+ socks5Proxy.stop();
+ socks5Proxy.removeLocalAddress("localAddress");
+ SmackConfiguration.setLocalSocks5ProxyPort(7777);
+
+ }
+
+ /**
+ * Invoking {@link Socks5BytestreamManager#establishSession(String, String)} the first time
+ * should successfully negotiate a SOCKS5 Bytestream via the second SOCKS5 proxy and should
+ * prioritize this proxy for a second SOCKS5 Bytestream negotiation.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldPrioritizeSecondSocks5ProxyOnSecondAttempt() throws Exception {
+
+ // disable clients local SOCKS5 proxy
+ SmackConfiguration.setLocalSocks5ProxyEnabled(false);
+
+ // get Socks5ByteStreamManager for connection
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ assertTrue(byteStreamManager.isProxyPrioritizationEnabled());
+
+ Verification streamHostUsedVerification1 = new Verification() {
+
+ public void verify(Bytestream request, Bytestream response) {
+ assertEquals(response.getSessionID(), request.getSessionID());
+ assertEquals(2, request.getStreamHosts().size());
+ // verify that the used stream host is the second in list
+ StreamHost streamHost = (StreamHost) request.getStreamHosts().toArray()[1];
+ assertEquals(response.getUsedHost().getJID(), streamHost.getJID());
+ }
+
+ };
+ createResponses(streamHostUsedVerification1);
+
+ // start a local SOCKS5 proxy
+ Socks5TestProxy socks5Proxy = Socks5TestProxy.getProxy(7778);
+ socks5Proxy.start();
+
+ // create digest to get the socket opened by target
+ String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);
+
+ // call the method that should be tested
+ OutputStream outputStream = byteStreamManager.establishSession(targetJID, sessionID).getOutputStream();
+
+ // test the established bytestream
+ InputStream inputStream = socks5Proxy.getSocket(digest).getInputStream();
+
+ byte[] data = new byte[] { 1, 2, 3 };
+ outputStream.write(data);
+
+ byte[] result = new byte[3];
+ inputStream.read(result);
+
+ assertArrayEquals(data, result);
+
+ protocol.verifyAll();
+
+ Verification streamHostUsedVerification2 = new Verification() {
+
+ public void verify(Bytestream request, Bytestream response) {
+ assertEquals(response.getSessionID(), request.getSessionID());
+ assertEquals(2, request.getStreamHosts().size());
+ // verify that the used stream host is the first in list
+ StreamHost streamHost = (StreamHost) request.getStreamHosts().toArray()[0];
+ assertEquals(response.getUsedHost().getJID(), streamHost.getJID());
+ }
+
+ };
+ createResponses(streamHostUsedVerification2);
+
+ // call the method that should be tested again
+ outputStream = byteStreamManager.establishSession(targetJID, sessionID).getOutputStream();
+
+ // test the established bytestream
+ inputStream = socks5Proxy.getSocket(digest).getInputStream();
+
+ outputStream.write(data);
+
+ inputStream.read(result);
+
+ assertArrayEquals(data, result);
+
+ protocol.verifyAll();
+
+ }
+
+ /**
+ * Invoking {@link Socks5BytestreamManager#establishSession(String, String)} the first time
+ * should successfully negotiate a SOCKS5 Bytestream via the second SOCKS5 proxy. The second
+ * negotiation should run in the same manner if prioritization is disabled.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldNotPrioritizeSocks5ProxyIfPrioritizationDisabled() throws Exception {
+
+ // disable clients local SOCKS5 proxy
+ SmackConfiguration.setLocalSocks5ProxyEnabled(false);
+
+ // get Socks5ByteStreamManager for connection
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+ byteStreamManager.setProxyPrioritizationEnabled(false);
+
+ assertFalse(byteStreamManager.isProxyPrioritizationEnabled());
+
+ Verification streamHostUsedVerification = new Verification() {
+
+ public void verify(Bytestream request, Bytestream response) {
+ assertEquals(response.getSessionID(), request.getSessionID());
+ assertEquals(2, request.getStreamHosts().size());
+ // verify that the used stream host is the second in list
+ StreamHost streamHost = (StreamHost) request.getStreamHosts().toArray()[1];
+ assertEquals(response.getUsedHost().getJID(), streamHost.getJID());
+ }
+
+ };
+ createResponses(streamHostUsedVerification);
+
+ // start a local SOCKS5 proxy
+ Socks5TestProxy socks5Proxy = Socks5TestProxy.getProxy(7778);
+ socks5Proxy.start();
+
+ // create digest to get the socket opened by target
+ String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);
+
+ // call the method that should be tested
+ OutputStream outputStream = byteStreamManager.establishSession(targetJID, sessionID).getOutputStream();
+
+ // test the established bytestream
+ InputStream inputStream = socks5Proxy.getSocket(digest).getInputStream();
+
+ byte[] data = new byte[] { 1, 2, 3 };
+ outputStream.write(data);
+
+ byte[] result = new byte[3];
+ inputStream.read(result);
+
+ assertArrayEquals(data, result);
+
+ protocol.verifyAll();
+
+ createResponses(streamHostUsedVerification);
+
+ // call the method that should be tested again
+ outputStream = byteStreamManager.establishSession(targetJID, sessionID).getOutputStream();
+
+ // test the established bytestream
+ inputStream = socks5Proxy.getSocket(digest).getInputStream();
+
+ outputStream.write(data);
+
+ inputStream.read(result);
+
+ assertArrayEquals(data, result);
+
+ protocol.verifyAll();
+
+ byteStreamManager.setProxyPrioritizationEnabled(true);
+
+ }
+
+ private void createResponses(Verification streamHostUsedVerification) {
+ // build discover info that supports the SOCKS5 feature
+ DiscoverInfo discoverInfo = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
+ discoverInfo.addFeature(Socks5BytestreamManager.NAMESPACE);
+
+ // return that SOCKS5 is supported if target is queried
+ protocol.addResponse(discoverInfo, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build discover items containing a proxy item
+ DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
+ initiatorJID);
+ discoverItems.addItem(new Item("proxy2.xmpp-server"));
+ discoverItems.addItem(new Item(proxyJID));
+
+ // return the proxy item if XMPP server is queried
+ protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ /*
+ * build discover info for proxy "proxy2.xmpp-server" containing information about being a
+ * SOCKS5 proxy
+ */
+ DiscoverInfo proxyInfo1 = Socks5PacketUtils.createDiscoverInfo("proxy2.xmpp-server",
+ initiatorJID);
+ Identity identity1 = new Identity("proxy", "proxy2.xmpp-server");
+ identity1.setType("bytestreams");
+ proxyInfo1.addIdentity(identity1);
+
+ // return the SOCKS5 bytestream proxy identity if proxy is queried
+ protocol.addResponse(proxyInfo1, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build discover info for proxy containing information about being a SOCKS5 proxy
+ DiscoverInfo proxyInfo2 = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
+ Identity identity2 = new Identity("proxy", proxyJID);
+ identity2.setType("bytestreams");
+ proxyInfo2.addIdentity(identity2);
+
+ // return the SOCKS5 bytestream proxy identity if proxy is queried
+ protocol.addResponse(proxyInfo2, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ /*
+ * build a SOCKS5 stream host info for "proxy2.xmpp-server" containing the address and the
+ * port of the proxy
+ */
+ Bytestream streamHostInfo1 = Socks5PacketUtils.createBytestreamResponse(
+ "proxy2.xmpp-server", initiatorJID);
+ streamHostInfo1.addStreamHost("proxy2.xmpp-server", proxyAddress, 7778);
+
+ // return stream host info if it is queried
+ protocol.addResponse(streamHostInfo1, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build a SOCKS5 stream host info containing the address and the port of the proxy
+ Bytestream streamHostInfo2 = Socks5PacketUtils.createBytestreamResponse(proxyJID,
+ initiatorJID);
+ streamHostInfo2.addStreamHost(proxyJID, proxyAddress, 7778);
+
+ // return stream host info if it is queried
+ protocol.addResponse(streamHostInfo2, Verification.correspondingSenderReceiver,
+ Verification.requestTypeGET);
+
+ // build used stream host response
+ Bytestream streamHostUsedPacket = Socks5PacketUtils.createBytestreamResponse(targetJID,
+ initiatorJID);
+ streamHostUsedPacket.setSessionID(sessionID);
+ streamHostUsedPacket.setUsedHost(proxyJID);
+
+ // return used stream host info as response to the bytestream initiation
+ protocol.addResponse(streamHostUsedPacket, streamHostUsedVerification,
+ Verification.correspondingSenderReceiver, Verification.requestTypeSET);
+
+ // build response to proxy activation
+ IQ activationResponse = Socks5PacketUtils.createActivationConfirmation(proxyJID,
+ initiatorJID);
+
+ // return proxy activation response if proxy should be activated
+ protocol.addResponse(activationResponse, new Verification() {
+
+ public void verify(Bytestream request, IQ response) {
+ assertEquals(targetJID, request.getToActivate().getTarget());
+ }
+
+ }, Verification.correspondingSenderReceiver, Verification.requestTypeSET);
+
+ }
+
+ /**
+ * Stop eventually started local SOCKS5 test proxy.
+ */
+ @After
+ public void cleanUp() {
+ Socks5TestProxy.stopProxy();
+ SmackConfiguration.setLocalSocks5ProxyEnabled(true);
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5ByteStreamRequestTest.java b/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5ByteStreamRequestTest.java
new file mode 100644
index 000000000..c802424e3
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5ByteStreamRequestTest.java
@@ -0,0 +1,426 @@
+/**
+ * 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.socks5bytestream;
+
+import static org.junit.Assert.*;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.SmackConfiguration;
+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.socks5bytestream.packet.Bytestream;
+import org.jivesoftware.util.ConnectionUtils;
+import org.jivesoftware.util.Protocol;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for the Socks5BytestreamRequest class.
+ *
+ * @author Henning Staib
+ */
+public class Socks5ByteStreamRequestTest {
+
+ // settings
+ String initiatorJID = "initiator@xmpp-server/Smack";
+ String targetJID = "target@xmpp-server/Smack";
+ String xmppServer = "xmpp-server";
+ String proxyJID = "proxy.xmpp-server";
+ String proxyAddress = "127.0.0.1";
+ String sessionID = "session_id";
+
+ Protocol protocol;
+
+ Connection connection;
+
+ /**
+ * Initialize fields used in the tests.
+ */
+ @Before
+ public void setup() {
+
+ // build protocol verifier
+ protocol = new Protocol();
+
+ // create mocked XMPP connection
+ connection = ConnectionUtils.createMockedConnection(protocol, targetJID, xmppServer);
+
+ }
+
+ /**
+ * Accepting a SOCKS5 Bytestream request should fail if the request doesn't contain any Socks5
+ * proxies.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldFailIfRequestHasNoStreamHosts() throws Exception {
+
+ try {
+
+ // build SOCKS5 Bytestream initialization request with no SOCKS5 proxies
+ Bytestream bytestreamInitialization = Socks5PacketUtils.createBytestreamInitiation(
+ initiatorJID, targetJID, sessionID);
+
+ // get SOCKS5 Bytestream manager for connection
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ // build SOCKS5 Bytestream request with the bytestream initialization
+ Socks5BytestreamRequest byteStreamRequest = new Socks5BytestreamRequest(
+ byteStreamManager, bytestreamInitialization);
+
+ // accept the stream (this is the call that is tested here)
+ byteStreamRequest.accept();
+
+ fail("exception should be thrown");
+ }
+ catch (XMPPException e) {
+ assertTrue(e.getMessage().contains("Could not establish socket with any provided host"));
+ }
+
+ // verify targets response
+ assertEquals(1, protocol.getRequests().size());
+ Packet targetResponse = protocol.getRequests().remove(0);
+ assertTrue(IQ.class.isInstance(targetResponse));
+ assertEquals(initiatorJID, targetResponse.getTo());
+ assertEquals(IQ.Type.ERROR, ((IQ) targetResponse).getType());
+ assertEquals(XMPPError.Condition.item_not_found.toString(),
+ ((IQ) targetResponse).getError().getCondition());
+
+ }
+
+ /**
+ * Accepting a SOCKS5 Bytestream request should fail if target is not able to connect to any of
+ * the provided SOCKS5 proxies.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void shouldFailIfRequestHasInvalidStreamHosts() throws Exception {
+
+ try {
+
+ // build SOCKS5 Bytestream initialization request
+ Bytestream bytestreamInitialization = Socks5PacketUtils.createBytestreamInitiation(
+ initiatorJID, targetJID, sessionID);
+ // add proxy that is not running
+ bytestreamInitialization.addStreamHost(proxyJID, proxyAddress, 7778);
+
+ // get SOCKS5 Bytestream manager for connection
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ // build SOCKS5 Bytestream request with the bytestream initialization
+ Socks5BytestreamRequest byteStreamRequest = new Socks5BytestreamRequest(
+ byteStreamManager, bytestreamInitialization);
+
+ // accept the stream (this is the call that is tested here)
+ byteStreamRequest.accept();
+
+ fail("exception should be thrown");
+ }
+ catch (XMPPException e) {
+ assertTrue(e.getMessage().contains("Could not establish socket with any provided host"));
+ }
+
+ // verify targets response
+ assertEquals(1, protocol.getRequests().size());
+ Packet targetResponse = protocol.getRequests().remove(0);
+ assertTrue(IQ.class.isInstance(targetResponse));
+ assertEquals(initiatorJID, targetResponse.getTo());
+ assertEquals(IQ.Type.ERROR, ((IQ) targetResponse).getType());
+ assertEquals(XMPPError.Condition.item_not_found.toString(),
+ ((IQ) targetResponse).getError().getCondition());
+
+ }
+
+ /**
+ * Target should not try to connect to SOCKS5 proxies that already failed twice.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldBlacklistInvalidProxyAfter2Failures() throws Exception {
+
+ // build SOCKS5 Bytestream initialization request
+ Bytestream bytestreamInitialization = Socks5PacketUtils.createBytestreamInitiation(
+ initiatorJID, targetJID, sessionID);
+ bytestreamInitialization.addStreamHost("invalid." + proxyJID, "127.0.0.2", 7778);
+
+ // get SOCKS5 Bytestream manager for connection
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ // try to connect several times
+ for (int i = 0; i < 2; i++) {
+ try {
+ // build SOCKS5 Bytestream request with the bytestream initialization
+ Socks5BytestreamRequest byteStreamRequest = new Socks5BytestreamRequest(
+ byteStreamManager, bytestreamInitialization);
+
+ // set timeouts
+ byteStreamRequest.setTotalConnectTimeout(600);
+ byteStreamRequest.setMinimumConnectTimeout(300);
+
+ // accept the stream (this is the call that is tested here)
+ byteStreamRequest.accept();
+
+ fail("exception should be thrown");
+ }
+ catch (XMPPException e) {
+ assertTrue(e.getMessage().contains(
+ "Could not establish socket with any provided host"));
+ }
+
+ // verify targets response
+ assertEquals(1, protocol.getRequests().size());
+ Packet targetResponse = protocol.getRequests().remove(0);
+ assertTrue(IQ.class.isInstance(targetResponse));
+ assertEquals(initiatorJID, targetResponse.getTo());
+ assertEquals(IQ.Type.ERROR, ((IQ) targetResponse).getType());
+ assertEquals(XMPPError.Condition.item_not_found.toString(),
+ ((IQ) targetResponse).getError().getCondition());
+ }
+
+ // create test data for stream
+ byte[] data = new byte[] { 1, 2, 3 };
+ Socks5TestProxy socks5Proxy = Socks5TestProxy.getProxy(7779);
+
+ assertTrue(socks5Proxy.isRunning());
+
+ // add a valid SOCKS5 proxy
+ bytestreamInitialization.addStreamHost(proxyJID, proxyAddress, 7779);
+
+ // build SOCKS5 Bytestream request with the bytestream initialization
+ Socks5BytestreamRequest byteStreamRequest = new Socks5BytestreamRequest(byteStreamManager,
+ bytestreamInitialization);
+
+ // set timeouts
+ byteStreamRequest.setTotalConnectTimeout(600);
+ byteStreamRequest.setMinimumConnectTimeout(300);
+
+ // accept the stream (this is the call that is tested here)
+ InputStream inputStream = byteStreamRequest.accept().getInputStream();
+
+ // create digest to get the socket opened by target
+ String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);
+
+ // test stream by sending some data
+ OutputStream outputStream = socks5Proxy.getSocket(digest).getOutputStream();
+ outputStream.write(data);
+
+ // verify that data is transferred correctly
+ byte[] result = new byte[3];
+ inputStream.read(result);
+ assertArrayEquals(data, result);
+
+ // verify targets response
+ assertEquals(1, protocol.getRequests().size());
+ Packet targetResponse = protocol.getRequests().remove(0);
+ assertEquals(Bytestream.class, targetResponse.getClass());
+ assertEquals(initiatorJID, targetResponse.getTo());
+ assertEquals(IQ.Type.RESULT, ((Bytestream) targetResponse).getType());
+ assertEquals(proxyJID, ((Bytestream) targetResponse).getUsedHost().getJID());
+
+ }
+
+ /**
+ * Target should not not blacklist any SOCKS5 proxies regardless of failing connections.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldNotBlacklistInvalidProxy() throws Exception {
+
+ // disable blacklisting
+ Socks5BytestreamRequest.setConnectFailureThreshold(0);
+
+ // build SOCKS5 Bytestream initialization request
+ Bytestream bytestreamInitialization = Socks5PacketUtils.createBytestreamInitiation(
+ initiatorJID, targetJID, sessionID);
+ bytestreamInitialization.addStreamHost("invalid." + proxyJID, "127.0.0.2", 7778);
+
+ // get SOCKS5 Bytestream manager for connection
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ // try to connect several times
+ for (int i = 0; i < 10; i++) {
+ try {
+ // build SOCKS5 Bytestream request with the bytestream initialization
+ Socks5BytestreamRequest byteStreamRequest = new Socks5BytestreamRequest(
+ byteStreamManager, bytestreamInitialization);
+
+ // set timeouts
+ byteStreamRequest.setTotalConnectTimeout(600);
+ byteStreamRequest.setMinimumConnectTimeout(300);
+
+ // accept the stream (this is the call that is tested here)
+ byteStreamRequest.accept();
+
+ fail("exception should be thrown");
+ }
+ catch (XMPPException e) {
+ assertTrue(e.getMessage().contains(
+ "Could not establish socket with any provided host"));
+ }
+
+ // verify targets response
+ assertEquals(1, protocol.getRequests().size());
+ Packet targetResponse = protocol.getRequests().remove(0);
+ assertTrue(IQ.class.isInstance(targetResponse));
+ assertEquals(initiatorJID, targetResponse.getTo());
+ assertEquals(IQ.Type.ERROR, ((IQ) targetResponse).getType());
+ assertEquals(XMPPError.Condition.item_not_found.toString(),
+ ((IQ) targetResponse).getError().getCondition());
+ }
+
+ // enable blacklisting
+ Socks5BytestreamRequest.setConnectFailureThreshold(2);
+
+ }
+
+ /**
+ * If the SOCKS5 Bytestream request contains multiple SOCKS5 proxies and the first one doesn't
+ * respond, the connection attempt to this proxy should not consume the whole timeout for
+ * connecting to the proxies.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldNotTimeoutIfFirstSocks5ProxyDoesNotRespond() throws Exception {
+
+ // start a local SOCKS5 proxy
+ Socks5TestProxy socks5Proxy = Socks5TestProxy.getProxy(7778);
+
+ // create a fake SOCKS5 proxy that doesn't respond to a request
+ ServerSocket serverSocket = new ServerSocket(7779);
+
+ // build SOCKS5 Bytestream initialization request
+ Bytestream bytestreamInitialization = Socks5PacketUtils.createBytestreamInitiation(
+ initiatorJID, targetJID, sessionID);
+ bytestreamInitialization.addStreamHost(proxyJID, proxyAddress, 7779);
+ bytestreamInitialization.addStreamHost(proxyJID, proxyAddress, 7778);
+
+ // create test data for stream
+ byte[] data = new byte[] { 1, 2, 3 };
+
+ // get SOCKS5 Bytestream manager for connection
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ // build SOCKS5 Bytestream request with the bytestream initialization
+ Socks5BytestreamRequest byteStreamRequest = new Socks5BytestreamRequest(byteStreamManager,
+ bytestreamInitialization);
+
+ // set timeouts
+ byteStreamRequest.setTotalConnectTimeout(2000);
+ byteStreamRequest.setMinimumConnectTimeout(1000);
+
+ // accept the stream (this is the call that is tested here)
+ InputStream inputStream = byteStreamRequest.accept().getInputStream();
+
+ // assert that client tries to connect to dumb SOCKS5 proxy
+ Socket socket = serverSocket.accept();
+ assertNotNull(socket);
+
+ // create digest to get the socket opened by target
+ String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);
+
+ // test stream by sending some data
+ OutputStream outputStream = socks5Proxy.getSocket(digest).getOutputStream();
+ outputStream.write(data);
+
+ // verify that data is transferred correctly
+ byte[] result = new byte[3];
+ inputStream.read(result);
+ assertArrayEquals(data, result);
+
+ // verify targets response
+ assertEquals(1, protocol.getRequests().size());
+ Packet targetResponse = protocol.getRequests().remove(0);
+ assertEquals(Bytestream.class, targetResponse.getClass());
+ assertEquals(initiatorJID, targetResponse.getTo());
+ assertEquals(IQ.Type.RESULT, ((Bytestream) targetResponse).getType());
+ assertEquals(proxyJID, ((Bytestream) targetResponse).getUsedHost().getJID());
+
+ serverSocket.close();
+
+ }
+
+ /**
+ * Accepting the SOCKS5 Bytestream request should be successfully.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldAcceptSocks5BytestreamRequestAndReceiveData() throws Exception {
+
+ // start a local SOCKS5 proxy
+ Socks5TestProxy socks5Proxy = Socks5TestProxy.getProxy(7778);
+
+ // build SOCKS5 Bytestream initialization request
+ Bytestream bytestreamInitialization = Socks5PacketUtils.createBytestreamInitiation(
+ initiatorJID, targetJID, sessionID);
+ bytestreamInitialization.addStreamHost(proxyJID, proxyAddress, 7778);
+
+ // create test data for stream
+ byte[] data = new byte[] { 1, 2, 3 };
+
+ // get SOCKS5 Bytestream manager for connection
+ Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ // build SOCKS5 Bytestream request with the bytestream initialization
+ Socks5BytestreamRequest byteStreamRequest = new Socks5BytestreamRequest(byteStreamManager,
+ bytestreamInitialization);
+
+ // accept the stream (this is the call that is tested here)
+ InputStream inputStream = byteStreamRequest.accept().getInputStream();
+
+ // create digest to get the socket opened by target
+ String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);
+
+ // test stream by sending some data
+ OutputStream outputStream = socks5Proxy.getSocket(digest).getOutputStream();
+ outputStream.write(data);
+
+ // verify that data is transferred correctly
+ byte[] result = new byte[3];
+ inputStream.read(result);
+ assertArrayEquals(data, result);
+
+ // verify targets response
+ assertEquals(1, protocol.getRequests().size());
+ Packet targetResponse = protocol.getRequests().remove(0);
+ assertEquals(Bytestream.class, targetResponse.getClass());
+ assertEquals(initiatorJID, targetResponse.getTo());
+ assertEquals(IQ.Type.RESULT, ((Bytestream) targetResponse).getType());
+ assertEquals(proxyJID, ((Bytestream) targetResponse).getUsedHost().getJID());
+
+ }
+
+ /**
+ * Stop eventually started local SOCKS5 test proxy.
+ */
+ @After
+ public void cleanUp() {
+ Socks5TestProxy.stopProxy();
+ SmackConfiguration.setLocalSocks5ProxyEnabled(true);
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5ClientForInitiatorTest.java b/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5ClientForInitiatorTest.java
new file mode 100644
index 000000000..a95b4813f
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5ClientForInitiatorTest.java
@@ -0,0 +1,306 @@
+/**
+ * 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.socks5bytestream;
+
+import static org.junit.Assert.*;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.packet.IQ.Type;
+import org.jivesoftware.smackx.socks5bytestream.packet.Bytestream;
+import org.jivesoftware.smackx.socks5bytestream.packet.Bytestream.StreamHost;
+import org.jivesoftware.util.ConnectionUtils;
+import org.jivesoftware.util.Protocol;
+import org.jivesoftware.util.Verification;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test for Socks5ClientForInitiator class.
+ *
+ * @author Henning Staib
+ */
+public class Socks5ClientForInitiatorTest {
+
+ // settings
+ String initiatorJID = "initiator@xmpp-server/Smack";
+ String targetJID = "target@xmpp-server/Smack";
+ String xmppServer = "xmpp-server";
+ String proxyJID = "proxy.xmpp-server";
+ String proxyAddress = "127.0.0.1";
+ int proxyPort = 7890;
+ String sessionID = "session_id";
+
+ // protocol verifier
+ Protocol protocol;
+
+ // mocked XMPP connection
+ Connection connection;
+
+ /**
+ * Initialize fields used in the tests.
+ */
+ @Before
+ public void setup() {
+
+ // build protocol verifier
+ protocol = new Protocol();
+
+ // create mocked XMPP connection
+ connection = ConnectionUtils.createMockedConnection(protocol, initiatorJID, xmppServer);
+
+ }
+
+ /**
+ * If the target is not connected to the local SOCKS5 proxy an exception should be thrown.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldFailIfTargetIsNotConnectedToLocalSocks5Proxy() throws Exception {
+
+ // start a local SOCKS5 proxy
+ SmackConfiguration.setLocalSocks5ProxyPort(proxyPort);
+ Socks5Proxy socks5Proxy = Socks5Proxy.getSocks5Proxy();
+ socks5Proxy.start();
+
+ // build stream host information for local SOCKS5 proxy
+ StreamHost streamHost = new StreamHost(connection.getUser(),
+ socks5Proxy.getLocalAddresses().get(0));
+ streamHost.setPort(socks5Proxy.getPort());
+
+ // create digest to get the socket opened by target
+ String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);
+
+ Socks5ClientForInitiator socks5Client = new Socks5ClientForInitiator(streamHost, digest,
+ connection, sessionID, targetJID);
+
+ try {
+ socks5Client.getSocket(10000);
+
+ fail("exception should be thrown");
+ }
+ catch (XMPPException e) {
+ assertTrue(e.getMessage().contains("target is not connected to SOCKS5 proxy"));
+ protocol.verifyAll(); // assert no XMPP messages were sent
+ }
+
+ socks5Proxy.stop();
+
+ }
+
+ /**
+ * Initiator and target should successfully connect to the local SOCKS5 proxy.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldSuccessfullyConnectThroughLocalSocks5Proxy() throws Exception {
+
+ // start a local SOCKS5 proxy
+ SmackConfiguration.setLocalSocks5ProxyPort(proxyPort);
+ Socks5Proxy socks5Proxy = Socks5Proxy.getSocks5Proxy();
+ socks5Proxy.start();
+
+ // test data
+ final byte[] data = new byte[] { 1, 2, 3 };
+
+ // create digest
+ final String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);
+
+ // allow connection of target with this digest
+ socks5Proxy.addTransfer(digest);
+
+ // build stream host information
+ final StreamHost streamHost = new StreamHost(connection.getUser(),
+ socks5Proxy.getLocalAddresses().get(0));
+ streamHost.setPort(socks5Proxy.getPort());
+
+ // target connects to local SOCKS5 proxy
+ Thread targetThread = new Thread() {
+
+ @Override
+ public void run() {
+ try {
+ Socks5Client targetClient = new Socks5Client(streamHost, digest);
+ Socket socket = targetClient.getSocket(10000);
+ socket.getOutputStream().write(data);
+ }
+ catch (Exception e) {
+ fail(e.getMessage());
+ }
+ }
+
+ };
+ targetThread.start();
+
+ Thread.sleep(200);
+
+ // initiator connects
+ Socks5ClientForInitiator socks5Client = new Socks5ClientForInitiator(streamHost, digest,
+ connection, sessionID, targetJID);
+
+ Socket socket = socks5Client.getSocket(10000);
+
+ // verify test data
+ InputStream in = socket.getInputStream();
+ for (int i = 0; i < data.length; i++) {
+ assertEquals(data[i], in.read());
+ }
+
+ targetThread.join();
+
+ protocol.verifyAll(); // assert no XMPP messages were sent
+
+ socks5Proxy.removeTransfer(digest);
+ socks5Proxy.stop();
+
+ }
+
+ /**
+ * If the initiator can connect to a SOCKS5 proxy but activating the stream fails an exception
+ * should be thrown.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldFailIfActivateSocks5ProxyFails() throws Exception {
+
+ // build error response as reply to the stream activation
+ XMPPError xmppError = new XMPPError(XMPPError.Condition.interna_server_error);
+ IQ error = new IQ() {
+
+ public String getChildElementXML() {
+ return null;
+ }
+
+ };
+ error.setType(Type.ERROR);
+ error.setFrom(proxyJID);
+ error.setTo(initiatorJID);
+ error.setError(xmppError);
+
+ protocol.addResponse(error, Verification.correspondingSenderReceiver,
+ Verification.requestTypeSET);
+
+ // start a local SOCKS5 proxy
+ Socks5TestProxy socks5Proxy = Socks5TestProxy.getProxy(proxyPort);
+ socks5Proxy.start();
+
+ StreamHost streamHost = new StreamHost(proxyJID, socks5Proxy.getAddress());
+ streamHost.setPort(socks5Proxy.getPort());
+
+ // create digest to get the socket opened by target
+ String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);
+
+ Socks5ClientForInitiator socks5Client = new Socks5ClientForInitiator(streamHost, digest,
+ connection, sessionID, targetJID);
+
+ try {
+
+ socks5Client.getSocket(10000);
+
+ fail("exception should be thrown");
+ }
+ catch (XMPPException e) {
+ assertTrue(e.getMessage().contains("activating SOCKS5 Bytestream failed"));
+ protocol.verifyAll();
+ }
+
+ socks5Proxy.stop();
+ }
+
+ /**
+ * Target and initiator should successfully connect to a "remote" SOCKS5 proxy and the initiator
+ * activates the bytestream.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldSuccessfullyEstablishConnectionAndActivateSocks5Proxy() throws Exception {
+
+ // build activation confirmation response
+ IQ activationResponse = new IQ() {
+
+ @Override
+ public String getChildElementXML() {
+ return null;
+ }
+
+ };
+ activationResponse.setFrom(proxyJID);
+ activationResponse.setTo(initiatorJID);
+ activationResponse.setType(IQ.Type.RESULT);
+
+ protocol.addResponse(activationResponse, Verification.correspondingSenderReceiver,
+ Verification.requestTypeSET, new Verification() {
+
+ public void verify(Bytestream request, IQ response) {
+ // verify that the correct stream should be activated
+ assertNotNull(request.getToActivate());
+ assertEquals(targetJID, request.getToActivate().getTarget());
+ }
+
+ });
+
+ // start a local SOCKS5 proxy
+ Socks5TestProxy socks5Proxy = Socks5TestProxy.getProxy(proxyPort);
+ socks5Proxy.start();
+
+ StreamHost streamHost = new StreamHost(proxyJID, socks5Proxy.getAddress());
+ streamHost.setPort(socks5Proxy.getPort());
+
+ // create digest to get the socket opened by target
+ String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);
+
+ Socks5ClientForInitiator socks5Client = new Socks5ClientForInitiator(streamHost, digest,
+ connection, sessionID, targetJID);
+
+ Socket initiatorSocket = socks5Client.getSocket(10000);
+ InputStream in = initiatorSocket.getInputStream();
+
+ Socket targetSocket = socks5Proxy.getSocket(digest);
+ OutputStream out = targetSocket.getOutputStream();
+
+ // verify test data
+ for (int i = 0; i < 10; i++) {
+ out.write(i);
+ assertEquals(i, in.read());
+ }
+
+ protocol.verifyAll();
+
+ initiatorSocket.close();
+ targetSocket.close();
+ socks5Proxy.stop();
+
+ }
+
+ /**
+ * Reset default port for local SOCKS5 proxy.
+ */
+ @After
+ public void cleanup() {
+ SmackConfiguration.setLocalSocks5ProxyPort(7777);
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5ClientTest.java b/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5ClientTest.java
new file mode 100644
index 000000000..61864a8ef
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5ClientTest.java
@@ -0,0 +1,330 @@
+/**
+ * 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.socks5bytestream;
+
+import static org.junit.Assert.*;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smackx.socks5bytestream.packet.Bytestream.StreamHost;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test for Socks5Client class.
+ *
+ * @author Henning Staib
+ */
+public class Socks5ClientTest {
+
+ // settings
+ private String serverAddress = "127.0.0.1";
+ private int serverPort = 7890;
+ private String proxyJID = "proxy.xmpp-server";
+ private String digest = "digest";
+ private ServerSocket serverSocket;
+
+ /**
+ * Initialize fields used in the tests.
+ *
+ * @throws Exception should not happen
+ */
+ @Before
+ public void setup() throws Exception {
+ // create SOCKS5 proxy server socket
+ serverSocket = new ServerSocket(serverPort);
+ }
+
+ /**
+ * A SOCKS5 client MUST close connection if server doesn't accept any of the given
+ * authentication methods. (See RFC1928 Section 3)
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldCloseSocketIfServerDoesNotAcceptAuthenticationMethod() throws Exception {
+
+ // start thread to connect to SOCKS5 proxy
+ Thread serverThread = new Thread() {
+
+ @Override
+ public void run() {
+ StreamHost streamHost = new StreamHost(proxyJID, serverAddress);
+ streamHost.setPort(serverPort);
+
+ Socks5Client socks5Client = new Socks5Client(streamHost, digest);
+
+ try {
+
+ socks5Client.getSocket(10000);
+
+ fail("exception should be thrown");
+ }
+ catch (XMPPException e) {
+ assertTrue(e.getMessage().contains(
+ "establishing connection to SOCKS5 proxy failed"));
+ }
+ catch (Exception e) {
+ fail(e.getMessage());
+ }
+
+ }
+
+ };
+ serverThread.start();
+
+ // accept connection form client
+ Socket socket = serverSocket.accept();
+ DataInputStream in = new DataInputStream(socket.getInputStream());
+ DataOutputStream out = new DataOutputStream(socket.getOutputStream());
+
+ // validate authentication request
+ assertEquals((byte) 0x05, (byte) in.read()); // version
+ assertEquals((byte) 0x01, (byte) in.read()); // number of supported auth methods
+ assertEquals((byte) 0x00, (byte) in.read()); // no-authentication method
+
+ // respond that no authentication method is accepted
+ out.write(new byte[] { (byte) 0x05, (byte) 0xFF });
+ out.flush();
+
+ // wait for client to shutdown
+ serverThread.join();
+
+ // assert socket is closed
+ assertEquals(-1, in.read());
+
+ }
+
+ /**
+ * The SOCKS5 client should close connection if server replies in an unsupported way.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldCloseSocketIfServerRepliesInUnsupportedWay() throws Exception {
+
+ // start thread to connect to SOCKS5 proxy
+ Thread serverThread = new Thread() {
+
+ @Override
+ public void run() {
+ StreamHost streamHost = new StreamHost(proxyJID, serverAddress);
+ streamHost.setPort(serverPort);
+
+ Socks5Client socks5Client = new Socks5Client(streamHost, digest);
+ try {
+ socks5Client.getSocket(10000);
+
+ fail("exception should be thrown");
+ }
+ catch (XMPPException e) {
+ assertTrue(e.getMessage().contains(
+ "establishing connection to SOCKS5 proxy failed"));
+ }
+ catch (Exception e) {
+ fail(e.getMessage());
+ }
+
+ }
+
+ };
+ serverThread.start();
+
+ // accept connection from client
+ Socket socket = serverSocket.accept();
+ DataInputStream in = new DataInputStream(socket.getInputStream());
+ DataOutputStream out = new DataOutputStream(socket.getOutputStream());
+
+ // validate authentication request
+ assertEquals((byte) 0x05, (byte) in.read()); // version
+ assertEquals((byte) 0x01, (byte) in.read()); // number of supported auth methods
+ assertEquals((byte) 0x00, (byte) in.read()); // no-authentication method
+
+ // respond that no no-authentication method is used
+ out.write(new byte[] { (byte) 0x05, (byte) 0x00 });
+ out.flush();
+
+ Socks5Utils.receiveSocks5Message(in);
+
+ // reply with unsupported address type
+ out.write(new byte[] { (byte) 0x05, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00 });
+ out.flush();
+
+ // wait for client to shutdown
+ serverThread.join();
+
+ // assert socket is closed
+ assertEquals(-1, in.read());
+
+ }
+
+ /**
+ * The SOCKS5 client should close connection if server replies with an error.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldCloseSocketIfServerRepliesWithError() throws Exception {
+
+ // start thread to connect to SOCKS5 proxy
+ Thread serverThread = new Thread() {
+
+ @Override
+ public void run() {
+ StreamHost streamHost = new StreamHost(proxyJID, serverAddress);
+ streamHost.setPort(serverPort);
+
+ Socks5Client socks5Client = new Socks5Client(streamHost, digest);
+ try {
+ socks5Client.getSocket(10000);
+
+ fail("exception should be thrown");
+ }
+ catch (XMPPException e) {
+ assertTrue(e.getMessage().contains(
+ "establishing connection to SOCKS5 proxy failed"));
+ }
+ catch (Exception e) {
+ fail(e.getMessage());
+ }
+
+ }
+
+ };
+ serverThread.start();
+
+ Socket socket = serverSocket.accept();
+ DataInputStream in = new DataInputStream(socket.getInputStream());
+ DataOutputStream out = new DataOutputStream(socket.getOutputStream());
+
+ // validate authentication request
+ assertEquals((byte) 0x05, (byte) in.read()); // version
+ assertEquals((byte) 0x01, (byte) in.read()); // number of supported auth methods
+ assertEquals((byte) 0x00, (byte) in.read()); // no-authentication method
+
+ // respond that no no-authentication method is used
+ out.write(new byte[] { (byte) 0x05, (byte) 0x00 });
+ out.flush();
+
+ Socks5Utils.receiveSocks5Message(in);
+
+ // reply with full SOCKS5 message with an error code (01 = general SOCKS server
+ // failure)
+ out.write(new byte[] { (byte) 0x05, (byte) 0x01, (byte) 0x00, (byte) 0x03 });
+ byte[] address = digest.getBytes();
+ out.write(address.length);
+ out.write(address);
+ out.write(new byte[] { (byte) 0x00, (byte) 0x00 });
+ out.flush();
+
+ // wait for client to shutdown
+ serverThread.join();
+
+ // assert socket is closed
+ assertEquals(-1, in.read());
+
+ }
+
+ /**
+ * The SOCKS5 client should successfully connect to the SOCKS5 server
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldSuccessfullyConnectToSocks5Server() throws Exception {
+
+ // start thread to connect to SOCKS5 proxy
+ Thread serverThread = new Thread() {
+
+ @Override
+ public void run() {
+ StreamHost streamHost = new StreamHost(proxyJID, serverAddress);
+ streamHost.setPort(serverPort);
+
+ Socks5Client socks5Client = new Socks5Client(streamHost, digest);
+
+ try {
+ Socket socket = socks5Client.getSocket(10000);
+ assertNotNull(socket);
+ socket.getOutputStream().write(123);
+ socket.close();
+ }
+ catch (Exception e) {
+ fail(e.getMessage());
+ }
+
+ }
+
+ };
+ serverThread.start();
+
+ Socket socket = serverSocket.accept();
+ DataInputStream in = new DataInputStream(socket.getInputStream());
+ DataOutputStream out = new DataOutputStream(socket.getOutputStream());
+
+ // validate authentication request
+ assertEquals((byte) 0x05, (byte) in.read()); // version
+ assertEquals((byte) 0x01, (byte) in.read()); // number of supported auth methods
+ assertEquals((byte) 0x00, (byte) in.read()); // no-authentication method
+
+ // respond that no no-authentication method is used
+ out.write(new byte[] { (byte) 0x05, (byte) 0x00 });
+ out.flush();
+
+ byte[] address = digest.getBytes();
+
+ assertEquals((byte) 0x05, (byte) in.read()); // version
+ assertEquals((byte) 0x01, (byte) in.read()); // connect request
+ assertEquals((byte) 0x00, (byte) in.read()); // reserved byte (always 0)
+ assertEquals((byte) 0x03, (byte) in.read()); // address type (domain)
+ assertEquals(address.length, (byte) in.read()); // address length
+ for (int i = 0; i < address.length; i++) {
+ assertEquals(address[i], (byte) in.read()); // address
+ }
+ assertEquals((byte) 0x00, (byte) in.read()); // port
+ assertEquals((byte) 0x00, (byte) in.read());
+
+ // reply with success SOCKS5 message
+ out.write(new byte[] { (byte) 0x05, (byte) 0x00, (byte) 0x00, (byte) 0x03 });
+ out.write(address.length);
+ out.write(address);
+ out.write(new byte[] { (byte) 0x00, (byte) 0x00 });
+ out.flush();
+
+ // wait for client to shutdown
+ serverThread.join();
+
+ // verify data sent from client
+ assertEquals(123, in.read());
+
+ // assert socket is closed
+ assertEquals(-1, in.read());
+
+ }
+
+ /**
+ * Close fake SOCKS5 proxy.
+ *
+ * @throws Exception should not happen
+ */
+ @After
+ public void cleanup() throws Exception {
+ serverSocket.close();
+ }
+}
diff --git a/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5PacketUtils.java b/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5PacketUtils.java
new file mode 100644
index 000000000..1c11c2ea3
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5PacketUtils.java
@@ -0,0 +1,119 @@
+/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.socks5bytestream;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DiscoverItems;
+import org.jivesoftware.smackx.socks5bytestream.packet.Bytestream;
+
+/**
+ * A collection of utility methods to create XMPP packets.
+ *
+ * @author Henning Staib
+ */
+public class Socks5PacketUtils {
+
+ /**
+ * Returns a SOCKS5 Bytestream initialization request packet. The Request doesn't contain any
+ * SOCKS5 proxies.
+ *
+ * @param from the initiator
+ * @param to the target
+ * @param sessionID the session ID
+ * @return SOCKS5 Bytestream initialization request packet
+ */
+ public static Bytestream createBytestreamInitiation(String from, String to, String sessionID) {
+ Bytestream bytestream = new Bytestream();
+ bytestream.getPacketID();
+ bytestream.setFrom(from);
+ bytestream.setTo(to);
+ bytestream.setSessionID(sessionID);
+ bytestream.setType(IQ.Type.SET);
+ return bytestream;
+ }
+
+ /**
+ * Returns a response to a SOCKS5 Bytestream initialization request. The packet doesn't contain
+ * the uses-host information.
+ *
+ * @param from the target
+ * @param to the initiator
+ * @return response to a SOCKS5 Bytestream initialization request
+ */
+ public static Bytestream createBytestreamResponse(String from, String to) {
+ Bytestream streamHostInfo = new Bytestream();
+ streamHostInfo.getPacketID();
+ streamHostInfo.setFrom(from);
+ streamHostInfo.setTo(to);
+ streamHostInfo.setType(IQ.Type.RESULT);
+ return streamHostInfo;
+ }
+
+ /**
+ * Returns a response to an item discovery request. The packet doesn't contain any items.
+ *
+ * @param from the XMPP server
+ * @param to the XMPP client
+ * @return response to an item discovery request
+ */
+ public static DiscoverItems createDiscoverItems(String from, String to) {
+ DiscoverItems discoverItems = new DiscoverItems();
+ discoverItems.getPacketID();
+ discoverItems.setFrom(from);
+ discoverItems.setTo(to);
+ discoverItems.setType(IQ.Type.RESULT);
+ return discoverItems;
+ }
+
+ /**
+ * Returns a response to an info discovery request. The packet doesn't contain any infos.
+ *
+ * @param from the target
+ * @param to the initiator
+ * @return response to an info discovery request
+ */
+ public static DiscoverInfo createDiscoverInfo(String from, String to) {
+ DiscoverInfo discoverInfo = new DiscoverInfo();
+ discoverInfo.getPacketID();
+ discoverInfo.setFrom(from);
+ discoverInfo.setTo(to);
+ discoverInfo.setType(IQ.Type.RESULT);
+ return discoverInfo;
+ }
+
+ /**
+ * Returns a response IQ for a activation request to the proxy.
+ *
+ * @param from JID of the proxy
+ * @param to JID of the client who wants to activate the SOCKS5 Bytestream
+ * @return response IQ for a activation request to the proxy
+ */
+ public static IQ createActivationConfirmation(String from, String to) {
+ IQ response = new IQ() {
+
+ @Override
+ public String getChildElementXML() {
+ return null;
+ }
+
+ };
+ response.getPacketID();
+ response.setFrom(from);
+ response.setTo(to);
+ response.setType(IQ.Type.RESULT);
+ return response;
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5ProxyTest.java b/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5ProxyTest.java
new file mode 100644
index 000000000..4234abb3d
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5ProxyTest.java
@@ -0,0 +1,359 @@
+/**
+ * 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.socks5bytestream;
+
+import static org.junit.Assert.*;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jivesoftware.smack.SmackConfiguration;
+import org.junit.After;
+import org.junit.Test;
+
+/**
+ * Test for Socks5Proxy class.
+ *
+ * @author Henning Staib
+ */
+public class Socks5ProxyTest {
+
+ /**
+ * The SOCKS5 proxy should be a singleton used by all XMPP connections
+ */
+ @Test
+ public void shouldBeASingleton() {
+ SmackConfiguration.setLocalSocks5ProxyEnabled(false);
+
+ Socks5Proxy proxy1 = Socks5Proxy.getSocks5Proxy();
+ Socks5Proxy proxy2 = Socks5Proxy.getSocks5Proxy();
+
+ assertNotNull(proxy1);
+ assertNotNull(proxy2);
+ assertSame(proxy1, proxy2);
+ }
+
+ /**
+ * The SOCKS5 proxy should not be started if disabled by configuration.
+ */
+ @Test
+ public void shouldNotBeRunningIfDisabled() {
+ SmackConfiguration.setLocalSocks5ProxyEnabled(false);
+ Socks5Proxy proxy = Socks5Proxy.getSocks5Proxy();
+ assertFalse(proxy.isRunning());
+ }
+
+ /**
+ * The SOCKS5 proxy should use a free port above the one configured.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldUseFreePortOnNegativeValues() throws Exception {
+ SmackConfiguration.setLocalSocks5ProxyEnabled(false);
+ Socks5Proxy proxy = Socks5Proxy.getSocks5Proxy();
+ assertFalse(proxy.isRunning());
+
+ ServerSocket serverSocket = new ServerSocket(0);
+ SmackConfiguration.setLocalSocks5ProxyPort(-serverSocket.getLocalPort());
+
+ proxy.start();
+
+ assertTrue(proxy.isRunning());
+
+ serverSocket.close();
+
+ assertTrue(proxy.getPort() > serverSocket.getLocalPort());
+
+ }
+
+ /**
+ * When inserting new network addresses to the proxy the order should remain in the order they
+ * were inserted.
+ */
+ @Test
+ public void shouldPreserveAddressOrderOnInsertions() {
+ Socks5Proxy proxy = Socks5Proxy.getSocks5Proxy();
+ List addresses = new ArrayList(proxy.getLocalAddresses());
+ addresses.add("1");
+ addresses.add("2");
+ addresses.add("3");
+ for (String address : addresses) {
+ proxy.addLocalAddress(address);
+ }
+
+ List localAddresses = proxy.getLocalAddresses();
+ for (int i = 0; i < addresses.size(); i++) {
+ assertEquals(addresses.get(i), localAddresses.get(i));
+ }
+ }
+
+ /**
+ * When replacing network addresses of the proxy the order should remain in the order if the
+ * given list.
+ */
+ @Test
+ public void shouldPreserveAddressOrderOnReplace() {
+ Socks5Proxy proxy = Socks5Proxy.getSocks5Proxy();
+ List addresses = new ArrayList(proxy.getLocalAddresses());
+ addresses.add("1");
+ addresses.add("2");
+ addresses.add("3");
+
+ proxy.replaceLocalAddresses(addresses);
+
+ List localAddresses = proxy.getLocalAddresses();
+ for (int i = 0; i < addresses.size(); i++) {
+ assertEquals(addresses.get(i), localAddresses.get(i));
+ }
+ }
+
+ /**
+ * Inserting the same address multiple times should not cause the proxy to return this address
+ * multiple times.
+ */
+ @Test
+ public void shouldNotReturnMultipleSameAddress() {
+ Socks5Proxy proxy = Socks5Proxy.getSocks5Proxy();
+
+ proxy.addLocalAddress("same");
+ proxy.addLocalAddress("same");
+ proxy.addLocalAddress("same");
+
+ assertEquals(2, proxy.getLocalAddresses().size());
+ }
+
+ /**
+ * There should be only one thread executing the SOCKS5 proxy process.
+ */
+ @Test
+ public void shouldOnlyStartOneServerThread() {
+ int threadCount = Thread.activeCount();
+
+ SmackConfiguration.setLocalSocks5ProxyPort(7890);
+ Socks5Proxy proxy = Socks5Proxy.getSocks5Proxy();
+ proxy.start();
+
+ assertTrue(proxy.isRunning());
+ assertEquals(threadCount + 1, Thread.activeCount());
+
+ proxy.start();
+
+ assertTrue(proxy.isRunning());
+ assertEquals(threadCount + 1, Thread.activeCount());
+
+ proxy.stop();
+
+ assertFalse(proxy.isRunning());
+ assertEquals(threadCount, Thread.activeCount());
+
+ proxy.start();
+
+ assertTrue(proxy.isRunning());
+ assertEquals(threadCount + 1, Thread.activeCount());
+
+ proxy.stop();
+
+ }
+
+ /**
+ * If the SOCKS5 proxy accepts a connection that is not a SOCKS5 connection it should close the
+ * corresponding socket.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldCloseSocketIfNoSocks5Request() throws Exception {
+ SmackConfiguration.setLocalSocks5ProxyPort(7890);
+ Socks5Proxy proxy = Socks5Proxy.getSocks5Proxy();
+ proxy.start();
+
+ Socket socket = new Socket(proxy.getLocalAddresses().get(0), proxy.getPort());
+
+ OutputStream out = socket.getOutputStream();
+ out.write(new byte[] { 1, 2, 3 });
+
+ assertEquals(-1, socket.getInputStream().read());
+
+ proxy.stop();
+
+ }
+
+ /**
+ * The SOCKS5 proxy should reply with an error message if no supported authentication methods
+ * are given in the SOCKS5 request.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldRespondWithErrorIfNoSupportedAuthenticationMethod() throws Exception {
+ SmackConfiguration.setLocalSocks5ProxyPort(7890);
+ Socks5Proxy proxy = Socks5Proxy.getSocks5Proxy();
+ proxy.start();
+
+ Socket socket = new Socket(proxy.getLocalAddresses().get(0), proxy.getPort());
+
+ OutputStream out = socket.getOutputStream();
+
+ // request username/password-authentication
+ out.write(new byte[] { (byte) 0x05, (byte) 0x01, (byte) 0x02 });
+
+ InputStream in = socket.getInputStream();
+
+ assertEquals((byte) 0x05, (byte) in.read());
+ assertEquals((byte) 0xFF, (byte) in.read());
+
+ assertEquals(-1, in.read());
+
+ proxy.stop();
+
+ }
+
+ /**
+ * The SOCKS5 proxy should respond with an error message if the client is not allowed to connect
+ * with the proxy.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldRespondWithErrorIfConnectionIsNotAllowed() throws Exception {
+ SmackConfiguration.setLocalSocks5ProxyPort(7890);
+ Socks5Proxy proxy = Socks5Proxy.getSocks5Proxy();
+ proxy.start();
+
+ Socket socket = new Socket(proxy.getLocalAddresses().get(0), proxy.getPort());
+
+ OutputStream out = socket.getOutputStream();
+ out.write(new byte[] { (byte) 0x05, (byte) 0x01, (byte) 0x00 });
+
+ InputStream in = socket.getInputStream();
+
+ assertEquals((byte) 0x05, (byte) in.read());
+ assertEquals((byte) 0x00, (byte) in.read());
+
+ // send valid SOCKS5 message
+ out.write(new byte[] { (byte) 0x05, (byte) 0x00, (byte) 0x00, (byte) 0x03, (byte) 0x01,
+ (byte) 0xAA, (byte) 0x00, (byte) 0x00 });
+
+ // verify error message
+ assertEquals((byte) 0x05, (byte) in.read());
+ assertFalse((byte) 0x00 == (byte) in.read()); // something other than 0 == success
+ assertEquals((byte) 0x00, (byte) in.read());
+ assertEquals((byte) 0x03, (byte) in.read());
+ assertEquals((byte) 0x01, (byte) in.read());
+ assertEquals((byte) 0xAA, (byte) in.read());
+ assertEquals((byte) 0x00, (byte) in.read());
+ assertEquals((byte) 0x00, (byte) in.read());
+
+ assertEquals(-1, in.read());
+
+ proxy.stop();
+
+ }
+
+ /**
+ * A Client should successfully establish a connection to the SOCKS5 proxy.
+ *
+ * @throws Exception should not happen
+ */
+ @Test
+ public void shouldSuccessfullyEstablishConnection() throws Exception {
+ SmackConfiguration.setLocalSocks5ProxyPort(7890);
+ Socks5Proxy proxy = Socks5Proxy.getSocks5Proxy();
+ proxy.start();
+
+ assertTrue(proxy.isRunning());
+
+ String digest = new String(new byte[] { (byte) 0xAA });
+
+ // add digest to allow connection
+ proxy.addTransfer(digest);
+
+ Socket socket = new Socket(proxy.getLocalAddresses().get(0), proxy.getPort());
+
+ OutputStream out = socket.getOutputStream();
+ out.write(new byte[] { (byte) 0x05, (byte) 0x01, (byte) 0x00 });
+
+ InputStream in = socket.getInputStream();
+
+ assertEquals((byte) 0x05, (byte) in.read());
+ assertEquals((byte) 0x00, (byte) in.read());
+
+ // send valid SOCKS5 message
+ out.write(new byte[] { (byte) 0x05, (byte) 0x00, (byte) 0x00, (byte) 0x03, (byte) 0x01,
+ (byte) 0xAA, (byte) 0x00, (byte) 0x00 });
+
+ // verify response
+ assertEquals((byte) 0x05, (byte) in.read());
+ assertEquals((byte) 0x00, (byte) in.read()); // success
+ assertEquals((byte) 0x00, (byte) in.read());
+ assertEquals((byte) 0x03, (byte) in.read());
+ assertEquals((byte) 0x01, (byte) in.read());
+ assertEquals((byte) 0xAA, (byte) in.read());
+ assertEquals((byte) 0x00, (byte) in.read());
+ assertEquals((byte) 0x00, (byte) in.read());
+
+ Thread.sleep(200);
+
+ Socket remoteSocket = proxy.getSocket(digest);
+
+ // remove digest
+ proxy.removeTransfer(digest);
+
+ // test stream
+ OutputStream remoteOut = remoteSocket.getOutputStream();
+ byte[] data = new byte[] { 1, 2, 3, 4, 5 };
+ remoteOut.write(data);
+ remoteOut.flush();
+
+ for (int i = 0; i < data.length; i++) {
+ assertEquals(data[i], in.read());
+ }
+
+ remoteSocket.close();
+
+ assertEquals(-1, in.read());
+
+ proxy.stop();
+
+ }
+
+ /**
+ * Reset SOCKS5 proxy settings.
+ */
+ @After
+ public void cleanup() {
+ SmackConfiguration.setLocalSocks5ProxyEnabled(true);
+ SmackConfiguration.setLocalSocks5ProxyPort(7777);
+ Socks5Proxy socks5Proxy = Socks5Proxy.getSocks5Proxy();
+ try {
+ String address = InetAddress.getLocalHost().getHostAddress();
+ List addresses = new ArrayList();
+ addresses.add(address);
+ socks5Proxy.replaceLocalAddresses(addresses);
+ }
+ catch (UnknownHostException e) {
+ // ignore
+ }
+
+ socks5Proxy.stop();
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5TestProxy.java b/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5TestProxy.java
new file mode 100644
index 000000000..25f55c6a0
--- /dev/null
+++ b/test-unit/org/jivesoftware/smackx/socks5bytestream/Socks5TestProxy.java
@@ -0,0 +1,285 @@
+/**
+ * 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.socks5bytestream;
+
+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.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.jivesoftware.smack.XMPPException;
+
+/**
+ * Simple SOCKS5 proxy for testing purposes. It is almost the same as the Socks5Proxy class but the
+ * port can be configured more easy and it all connections are allowed.
+ *
+ * @author Henning Staib
+ */
+public class Socks5TestProxy {
+
+ /* SOCKS5 proxy singleton */
+ private static Socks5TestProxy 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 connectionMap = new ConcurrentHashMap();
+
+ /* port of the test proxy */
+ private int port = 7777;
+
+ /**
+ * Private constructor.
+ */
+ private Socks5TestProxy(int port) {
+ this.serverProcess = new Socks5ServerProcess();
+ this.port = port;
+ }
+
+ /**
+ * Returns the local SOCKS5 proxy server
+ *
+ * @param port of the test proxy
+ * @return the local SOCKS5 proxy server
+ */
+ public static synchronized Socks5TestProxy getProxy(int port) {
+ if (socks5Server == null) {
+ socks5Server = new Socks5TestProxy(port);
+ socks5Server.start();
+ }
+ return socks5Server;
+ }
+
+ /**
+ * Stops the test proxy
+ */
+ public static synchronized void stopProxy() {
+ if (socks5Server != null) {
+ socks5Server.stop();
+ socks5Server = null;
+ }
+ }
+
+ /**
+ * Starts the local SOCKS5 proxy server. If it is already running, this method does nothing.
+ */
+ public synchronized void start() {
+ if (isRunning()) {
+ return;
+ }
+ try {
+ this.serverSocket = new ServerSocket(this.port);
+ this.serverThread = new Thread(this.serverProcess);
+ this.serverThread.start();
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ // do nothing
+ }
+ }
+
+ /**
+ * 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
+ e.printStackTrace();
+ }
+
+ if (this.serverThread != null && this.serverThread.isAlive()) {
+ try {
+ this.serverThread.interrupt();
+ this.serverThread.join();
+ }
+ catch (InterruptedException e) {
+ // do nothing
+ e.printStackTrace();
+ }
+ }
+ this.serverThread = null;
+ this.serverSocket = null;
+
+ }
+
+ /**
+ * Returns the host address of the local SOCKS5 proxy server.
+ *
+ * @return the host address of the local SOCKS5 proxy server
+ */
+ public String getAddress() {
+ try {
+ return InetAddress.getLocalHost().getHostAddress();
+ }
+ catch (UnknownHostException e) {
+ return null;
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @param digest identifying the connection
+ * @return socket or null if there is no socket for the given digest
+ */
+ public Socket getSocket(String digest) {
+ return this.connectionMap.get(digest);
+ }
+
+ /**
+ * Returns true if the local SOCKS5 proxy server is running, otherwise false.
+ *
+ * @return true if the local SOCKS5 proxy server is running, otherwise false
+ */
+ public boolean isRunning() {
+ return this.serverSocket != null;
+ }
+
+ /**
+ * Implementation of a simplified SOCKS5 proxy server.
+ *
+ * @author Henning Staib
+ */
+ class Socks5ServerProcess implements Runnable {
+
+ public void run() {
+ while (true) {
+ Socket socket = null;
+
+ try {
+
+ if (Socks5TestProxy.this.serverSocket.isClosed()
+ || Thread.currentThread().isInterrupted()) {
+ return;
+ }
+
+ // accept connection
+ socket = Socks5TestProxy.this.serverSocket.accept();
+
+ // initialize connection
+ establishConnection(socket);
+
+ }
+ catch (SocketException e) {
+ /* do nothing */
+ }
+ catch (Exception e) {
+ try {
+ e.printStackTrace();
+ 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]);
+
+ connectionRequest[1] = (byte) 0x00; // set return status to 0 (success)
+ out.write(connectionRequest);
+ out.flush();
+
+ // store connection
+ Socks5TestProxy.this.connectionMap.put(responseDigest, socket);
+ }
+
+ }
+
+}
diff --git a/test-unit/org/jivesoftware/util/ConnectionUtils.java b/test-unit/org/jivesoftware/util/ConnectionUtils.java
new file mode 100644
index 000000000..3834159a4
--- /dev/null
+++ b/test-unit/org/jivesoftware/util/ConnectionUtils.java
@@ -0,0 +1,94 @@
+/**
+ * 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.util;
+
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+/**
+ * A collection of utility methods to create mocked XMPP connections.
+ *
+ * @author Henning Staib
+ */
+public class ConnectionUtils {
+
+ /**
+ * Creates a mocked XMPP connection that stores every packet that is send over this
+ * connection in the given protocol instance and returns the predefined answer packets
+ * form the protocol instance.
+ *
+ * This mocked connection can used to collect packets that require a reply using a
+ * PacketCollector.
+ *
+ *