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..34c5422ce 100644 --- a/build/resources/META-INF/smack-config.xml +++ b/build/resources/META-INF/smack-config.xml @@ -8,16 +8,24 @@ org.jivesoftware.smack.PrivacyListManager org.jivesoftware.smackx.XHTMLManager org.jivesoftware.smackx.muc.MultiUserChat + org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager + org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager org.jivesoftware.smackx.filetransfer.FileTransferManager org.jivesoftware.smackx.LastActivityManager org.jivesoftware.smack.ReconnectionManager org.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..9f0f4f15a 100644 --- a/build/resources/META-INF/smack.providers +++ b/build/resources/META-INF/smack.providers @@ -158,7 +158,7 @@ org.jivesoftware.smackx.packet.OfflineMessageInfo$Provider - + query jabber:iq:last @@ -186,7 +186,7 @@ org.jivesoftware.smackx.provider.MultipleAddressesProvider - + si http://jabber.org/protocol/si @@ -196,25 +196,31 @@ query http://jabber.org/protocol/bytestreams - org.jivesoftware.smackx.provider.BytestreamsProvider + org.jivesoftware.smackx.bytestreams.socks5.provider.BytestreamsProvider open http://jabber.org/protocol/ibb - org.jivesoftware.smackx.provider.IBBProviders$Open + org.jivesoftware.smackx.bytestreams.ibb.provider.OpenIQProvider + + + + data + http://jabber.org/protocol/ibb + org.jivesoftware.smackx.bytestreams.ibb.provider.DataPacketProvider close http://jabber.org/protocol/ibb - org.jivesoftware.smackx.provider.IBBProviders$Close + org.jivesoftware.smackx.bytestreams.ibb.provider.CloseIQProvider data http://jabber.org/protocol/ibb - org.jivesoftware.smackx.provider.IBBProviders$Data + org.jivesoftware.smackx.bytestreams.ibb.provider.DataPacketProvider @@ -491,7 +497,7 @@ org.jivesoftware.smackx.provider.HeaderProvider - + pubsub http://jabber.org/protocol/pubsub @@ -546,7 +552,7 @@ org.jivesoftware.smackx.pubsub.provider.FormNodeProvider - + pubsub http://jabber.org/protocol/pubsub#owner @@ -565,7 +571,7 @@ org.jivesoftware.smackx.pubsub.provider.FormNodeProvider - + event http://jabber.org/protocol/pubsub#event @@ -621,7 +627,7 @@ org.jivesoftware.smackx.packet.Nick$Provider - + attention urn:xmpp:attention:0 diff --git a/source/org/jivesoftware/smack/AbstractConnectionListener.java b/source/org/jivesoftware/smack/AbstractConnectionListener.java new file mode 100644 index 000000000..69acf9012 --- /dev/null +++ b/source/org/jivesoftware/smack/AbstractConnectionListener.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.smack; + +/** + * The AbstractConnectionListener class provides an empty implementation for all + * methods defined by the {@link ConnectionListener} interface. This is a + * convenience class which should be used in case you do not need to implement + * all methods. + * + * @author Henning Staib + */ +public class AbstractConnectionListener implements ConnectionListener { + + public void connectionClosed() { + // do nothing + } + + public void connectionClosedOnError(Exception e) { + // do nothing + } + + public void reconnectingIn(int seconds) { + // do nothing + } + + public void reconnectionFailed(Exception e) { + // do nothing + } + + public void reconnectionSuccessful() { + // do nothing + } + +} 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..be78255d5 --- /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.bytestreams.ibb.InBandBytestreamListener; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamListener; +import org.jivesoftware.smackx.bytestreams.socks5.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..ca6bbc602 --- /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.bytestreams.ibb.InBandBytestreamManager; +import org.jivesoftware.smackx.bytestreams.socks5.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..e368bad99 --- /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.bytestreams.ibb.InBandBytestreamRequest; +import org.jivesoftware.smackx.bytestreams.socks5.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..7aafc3513 --- /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.bytestreams.ibb.InBandBytestreamSession; +import org.jivesoftware.smackx.bytestreams.socks5.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/bytestreams/ibb/CloseListener.java b/source/org/jivesoftware/smackx/bytestreams/ibb/CloseListener.java new file mode 100644 index 000000000..7690e9537 --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/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.bytestreams.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.bytestreams.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/bytestreams/ibb/DataListener.java b/source/org/jivesoftware/smackx/bytestreams/ibb/DataListener.java new file mode 100644 index 000000000..166c14647 --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/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.bytestreams.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.bytestreams.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/bytestreams/ibb/InBandBytestreamListener.java b/source/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamListener.java new file mode 100644 index 000000000..68791a6f3 --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/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.bytestreams.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/bytestreams/ibb/InBandBytestreamManager.java b/source/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamManager.java new file mode 100644 index 000000000..6c4e1b57a --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/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.bytestreams.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.bytestreams.ibb.packet.Open; +import org.jivesoftware.smackx.filetransfer.FileTransferManager; +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/bytestreams/ibb/InBandBytestreamRequest.java b/source/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamRequest.java new file mode 100644 index 000000000..5bc689a4a --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/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.bytestreams.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.bytestreams.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/bytestreams/ibb/InBandBytestreamSession.java b/source/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSession.java new file mode 100644 index 000000000..e3977a4c1 --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/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.bytestreams.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.bytestreams.ibb.packet.Close; +import org.jivesoftware.smackx.bytestreams.ibb.packet.Data; +import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension; +import org.jivesoftware.smackx.bytestreams.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/bytestreams/ibb/InitiationListener.java b/source/org/jivesoftware/smackx/bytestreams/ibb/InitiationListener.java new file mode 100644 index 000000000..0ecb08156 --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/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.bytestreams.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.bytestreams.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/bytestreams/ibb/packet/Close.java b/source/org/jivesoftware/smackx/bytestreams/ibb/packet/Close.java new file mode 100644 index 000000000..9a78d736d --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/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.bytestreams.ibb.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.bytestreams.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/bytestreams/ibb/packet/Data.java b/source/org/jivesoftware/smackx/bytestreams/ibb/packet/Data.java new file mode 100644 index 000000000..696fa75d3 --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/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.bytestreams.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/bytestreams/ibb/packet/DataPacketExtension.java b/source/org/jivesoftware/smackx/bytestreams/ibb/packet/DataPacketExtension.java new file mode 100644 index 000000000..80ed1e1fc --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/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.bytestreams.ibb.packet; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.bytestreams.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(""); + return buf.toString(); + } + +} diff --git a/source/org/jivesoftware/smackx/bytestreams/ibb/packet/Open.java b/source/org/jivesoftware/smackx/bytestreams/ibb/packet/Open.java new file mode 100644 index 000000000..94a7a9bf4 --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/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.bytestreams.ibb.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager; +import org.jivesoftware.smackx.bytestreams.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/bytestreams/ibb/provider/CloseIQProvider.java similarity index 51% rename from source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiatorManager.java rename to source/org/jivesoftware/smackx/bytestreams/ibb/provider/CloseIQProvider.java index e63c9ebf6..566724c21 100644 --- a/source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiatorManager.java +++ b/source/org/jivesoftware/smackx/bytestreams/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.bytestreams.ibb.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.bytestreams.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/bytestreams/ibb/provider/DataPacketProvider.java b/source/org/jivesoftware/smackx/bytestreams/ibb/provider/DataPacketProvider.java new file mode 100644 index 000000000..5abed085c --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/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.bytestreams.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.bytestreams.ibb.packet.Data; +import org.jivesoftware.smackx.bytestreams.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/bytestreams/ibb/provider/OpenIQProvider.java b/source/org/jivesoftware/smackx/bytestreams/ibb/provider/OpenIQProvider.java new file mode 100644 index 000000000..3cc725ae8 --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/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.bytestreams.ibb.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager.StanzaType; +import org.jivesoftware.smackx.bytestreams.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/bytestreams/socks5/InitiationListener.java b/source/org/jivesoftware/smackx/bytestreams/socks5/InitiationListener.java new file mode 100644 index 000000000..2a78250a0 --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/socks5/InitiationListener.java @@ -0,0 +1,119 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.bytestreams.socks5; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.IQTypeFilter; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smackx.bytestreams.BytestreamListener; +import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream; + +/** + * InitiationListener handles all incoming SOCKS5 Bytestream initiation requests. If there are no + * listeners for a SOCKS5 bytestream request InitiationListener will always refuse the request and + * reply with a <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/bytestreams/socks5/Socks5BytestreamListener.java b/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamListener.java new file mode 100644 index 000000000..1430b1d23 --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamListener.java @@ -0,0 +1,43 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.bytestreams.socks5; + +import org.jivesoftware.smackx.bytestreams.BytestreamListener; +import org.jivesoftware.smackx.bytestreams.BytestreamRequest; + +/** + * Socks5BytestreamListener are informed if a remote user wants to initiate a SOCKS5 Bytestream. + * Implement this interface to handle incoming SOCKS5 Bytestream requests. + *

+ * 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/bytestreams/socks5/Socks5BytestreamManager.java b/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java new file mode 100644 index 000000000..5d4b50958 --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/socks5/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.bytestreams.socks5; + +import java.io.IOException; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeoutException; + +import org.jivesoftware.smack.AbstractConnectionListener; +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.bytestreams.BytestreamListener; +import org.jivesoftware.smackx.bytestreams.BytestreamManager; +import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream; +import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost; +import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHostUsed; +import org.jivesoftware.smackx.filetransfer.FileTransferManager; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.DiscoverItems; +import org.jivesoftware.smackx.packet.SyncPacketSend; +import org.jivesoftware.smackx.packet.DiscoverInfo.Identity; +import org.jivesoftware.smackx.packet.DiscoverItems.Item; + +/** + * The Socks5BytestreamManager class handles establishing SOCKS5 Bytestreams as specified in the 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/bytestreams/socks5/Socks5BytestreamRequest.java b/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamRequest.java new file mode 100644 index 000000000..0b2fdeb6a --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamRequest.java @@ -0,0 +1,316 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.bytestreams.socks5; + +import java.io.IOException; +import java.net.Socket; +import java.util.Collection; +import java.util.concurrent.TimeoutException; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.util.Cache; +import org.jivesoftware.smackx.bytestreams.BytestreamRequest; +import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream; +import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost; + +/** + * Socks5BytestreamRequest class handles incoming SOCKS5 Bytestream requests. + * + * @author Henning Staib + */ +public class Socks5BytestreamRequest implements BytestreamRequest { + + /* lifetime of an Item in the blacklist */ + private static final long BLACKLIST_LIFETIME = 60 * 1000 * 120; + + /* size of the blacklist */ + private static final int BLACKLIST_MAX_SIZE = 100; + + /* blacklist of addresses of SOCKS5 proxies */ + private static final Cache 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/bytestreams/socks5/Socks5BytestreamSession.java b/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamSession.java new file mode 100644 index 000000000..20fbdb180 --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamSession.java @@ -0,0 +1,81 @@ +package org.jivesoftware.smackx.bytestreams.socks5; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.SocketException; + +import org.jivesoftware.smackx.bytestreams.BytestreamSession; + +/** + * Socks5BytestreamSession class represents a SOCKS5 Bytestream session. + * + * @author Henning Staib + */ +public class Socks5BytestreamSession implements BytestreamSession { + + /* the underlying socket of the SOCKS5 Bytestream */ + private final Socket socket; + + /* flag to indicate if this session is a direct or mediated connection */ + private final boolean isDirect; + + protected Socks5BytestreamSession(Socket socket, boolean isDirect) { + this.socket = socket; + this.isDirect = isDirect; + } + + /** + * Returns 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/bytestreams/socks5/Socks5Client.java b/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5Client.java new file mode 100644 index 000000000..664ea596d --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5Client.java @@ -0,0 +1,204 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.bytestreams.socks5; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost; + +/** + * The SOCKS5 client class handles establishing a connection to a SOCKS5 proxy. Connecting to a + * SOCKS5 proxy requires authentication. This implementation only supports the no-authentication + * authentication method. + * + * @author Henning Staib + */ +class Socks5Client { + + /* stream host containing network settings and name of the SOCKS5 proxy */ + protected StreamHost streamHost; + + /* SHA-1 digest identifying the SOCKS5 stream */ + protected String digest; + + /** + * Constructor for a SOCKS5 client. + * + * @param streamHost containing network settings of the SOCKS5 proxy + * @param digest identifying the SOCKS5 Bytestream + */ + public Socks5Client(StreamHost streamHost, String digest) { + this.streamHost = streamHost; + this.digest = digest; + } + + /** + * Returns the initialized socket that can be used to transfer data between peers via the SOCKS5 + * proxy. + * + * @param timeout timeout to connect to SOCKS5 proxy in milliseconds + * @return socket the initialized socket + * @throws IOException if initializing the socket failed due to a network error + * @throws XMPPException if establishing connection to SOCKS5 proxy failed + * @throws TimeoutException if connecting to SOCKS5 proxy timed out + * @throws InterruptedException if the current thread was interrupted while waiting + */ + public Socket getSocket(int timeout) throws IOException, XMPPException, InterruptedException, + TimeoutException { + + // wrap connecting in future for timeout + FutureTask 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/bytestreams/socks5/Socks5ClientForInitiator.java b/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiator.java new file mode 100644 index 000000000..04248c77b --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiator.java @@ -0,0 +1,117 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.bytestreams.socks5; + +import java.io.IOException; +import java.net.Socket; +import java.util.concurrent.TimeoutException; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream; +import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost; +import org.jivesoftware.smackx.packet.SyncPacketSend; + +/** + * Implementation of a SOCKS5 client used on the initiators side. This is needed because connecting + * to the local SOCKS5 proxy differs form the regular way to connect to a SOCKS5 proxy. Additionally + * a remote SOCKS5 proxy has to be activated by the initiator before data can be transferred between + * the peers. + * + * @author Henning Staib + */ +class Socks5ClientForInitiator extends Socks5Client { + + /* the XMPP connection used to communicate with the SOCKS5 proxy */ + private Connection connection; + + /* the session ID used to activate SOCKS5 stream */ + private String sessionID; + + /* the target JID used to activate SOCKS5 stream */ + private String target; + + /** + * Creates a new SOCKS5 client for the initiators side. + * + * @param streamHost containing network settings of the SOCKS5 proxy + * @param digest identifying the SOCKS5 Bytestream + * @param connection the XMPP connection + * @param sessionID the session ID of the SOCKS5 Bytestream + * @param target the target JID of the SOCKS5 Bytestream + */ + public Socks5ClientForInitiator(StreamHost streamHost, String digest, Connection connection, + String sessionID, String target) { + super(streamHost, digest); + this.connection = connection; + this.sessionID = sessionID; + this.target = target; + } + + public Socket getSocket(int timeout) throws IOException, XMPPException, InterruptedException, + TimeoutException { + Socket socket = null; + + // check if stream host is the local SOCKS5 proxy + if (this.streamHost.getJID().equals(this.connection.getUser())) { + Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy(); + socket = socks5Server.getSocket(this.digest); + if (socket == null) { + throw new XMPPException("target is not connected to SOCKS5 proxy"); + } + } + else { + socket = super.getSocket(timeout); + + try { + activate(); + } + catch (XMPPException e) { + socket.close(); + throw new XMPPException("activating SOCKS5 Bytestream failed", e); + } + + } + + return socket; + } + + /** + * Activates the SOCKS5 Bytestream by sending a XMPP SOCKS5 Bytestream activation packet to the + * SOCKS5 proxy. + */ + private void activate() throws XMPPException { + Bytestream activate = createStreamHostActivation(); + // if activation fails #getReply throws an exception + SyncPacketSend.getReply(this.connection, activate); + } + + /** + * Returns a SOCKS5 Bytestream activation packet. + * + * @return SOCKS5 Bytestream activation packet + */ + private Bytestream createStreamHostActivation() { + Bytestream activate = new Bytestream(this.sessionID); + activate.setMode(null); + activate.setType(IQ.Type.SET); + activate.setTo(this.streamHost.getJID()); + + activate.setToActivate(this.target); + + return activate; + } + +} diff --git a/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java b/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java new file mode 100644 index 000000000..11ef7a9c5 --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java @@ -0,0 +1,423 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.bytestreams.socks5; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPException; + +/** + * The Socks5Proxy class represents a local SOCKS5 proxy server. It can be enabled/disabled by + * setting the 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/bytestreams/socks5/Socks5Utils.java b/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5Utils.java new file mode 100644 index 000000000..9c9256341 --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5Utils.java @@ -0,0 +1,73 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.bytestreams.socks5; + +import java.io.DataInputStream; +import java.io.IOException; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.util.StringUtils; + +/** + * A collection of utility methods for SOcKS5 messages. + * + * @author Henning Staib + */ +class Socks5Utils { + + /** + * Returns a SHA-1 digest of the given parameters as specified in 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/bytestreams/socks5/packet/Bytestream.java similarity index 73% rename from source/org/jivesoftware/smackx/packet/Bytestream.java rename to source/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java index d37d9f6fb..9e07fc379 100644 --- a/source/org/jivesoftware/smackx/packet/Bytestream.java +++ b/source/org/jivesoftware/smackx/bytestreams/socks5/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.bytestreams.socks5.packet; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.PacketExtension; -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/bytestreams/socks5/provider/BytestreamsProvider.java b/source/org/jivesoftware/smackx/bytestreams/socks5/provider/BytestreamsProvider.java new file mode 100644 index 000000000..76f9b0c66 --- /dev/null +++ b/source/org/jivesoftware/smackx/bytestreams/socks5/provider/BytestreamsProvider.java @@ -0,0 +1,82 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.bytestreams.socks5.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream; +import org.xmlpull.v1.XmlPullParser; + +/** + * Parses a bytestream packet. + * + * @author Alexander Wenckus + */ +public class BytestreamsProvider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + boolean done = false; + + Bytestream toReturn = new Bytestream(); + + String id = parser.getAttributeValue("", "sid"); + String mode = parser.getAttributeValue("", "mode"); + + // streamhost + String JID = null; + String host = null; + String port = null; + + int eventType; + String elementName; + while (!done) { + eventType = parser.next(); + elementName = parser.getName(); + if (eventType == XmlPullParser.START_TAG) { + if (elementName.equals(Bytestream.StreamHost.ELEMENTNAME)) { + JID = parser.getAttributeValue("", "jid"); + host = parser.getAttributeValue("", "host"); + port = parser.getAttributeValue("", "port"); + } + else if (elementName.equals(Bytestream.StreamHostUsed.ELEMENTNAME)) { + toReturn.setUsedHost(parser.getAttributeValue("", "jid")); + } + else if (elementName.equals(Bytestream.Activate.ELEMENTNAME)) { + toReturn.setToActivate(parser.getAttributeValue("", "jid")); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (elementName.equals("streamhost")) { + if (port == null) { + toReturn.addStreamHost(JID, host); + } + else { + toReturn.addStreamHost(JID, host, Integer.parseInt(port)); + } + JID = null; + host = null; + port = null; + } + else if (elementName.equals("query")) { + done = true; + } + } + } + + toReturn.setMode((Bytestream.Mode.fromName(mode))); + toReturn.setSessionID(id); + return toReturn; + } + +} diff --git a/source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java b/source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java index 0a910dc99..cd74c1ff7 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,40 +41,26 @@ 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.bytestreams.ibb.InBandBytestreamManager; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager; import org.jivesoftware.smackx.packet.DataForm; import org.jivesoftware.smackx.packet.StreamInitiation; -import java.net.URLConnection; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - /** * Manages the negotiation of file transfers according to JEP-0096. If a file is * being sent the remote user chooses the type of stream under which the file * 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..df0f67ed1 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.bytestreams.ibb.InBandBytestreamManager; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamRequest; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamSession; +import org.jivesoftware.smackx.bytestreams.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..3c07fdca8 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.bytestreams.socks5.Socks5BytestreamManager; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamRequest; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamSession; +import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream; 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; - /** - * 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: - *

- *

-     * <iq type='set'
-     *     from='initiator@host1/foo'
-     *     to='target@host2/bar'
-     *     id='initiate'>
-     *   <query xmlns='http://jabber.org/protocol/bytestreams'
-     *          sid='mySID'
-     * 	 mode='tcp'>
-     *     <streamhost
-     *         jid='initiator@host1/foo'
-     *         host='192.168.4.1'
-     *        port='5086'/>
-     *     <streamhost
-     *         jid='proxy.host3'
-     *         host='24.24.24.1'
-     *         zeroconf='_jabber.bytestreams'/>
-     *   </query>
-     * </iq>
-     * 
- * - * @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/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(""); - 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/test-unit/org/jivesoftware/smackx/bytestreams/ibb/CloseListenerTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/CloseListenerTest.java new file mode 100644 index 000000000..cdf40870f --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/CloseListenerTest.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.bytestreams.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.ibb.CloseListener; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager; +import org.jivesoftware.smackx.bytestreams.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/bytestreams/ibb/DataListenerTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/DataListenerTest.java new file mode 100644 index 000000000..23dbd4704 --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/DataListenerTest.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.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.ibb.DataListener; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager; +import org.jivesoftware.smackx.bytestreams.ibb.packet.Data; +import org.jivesoftware.smackx.bytestreams.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/bytestreams/ibb/IBBPacketUtils.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/IBBPacketUtils.java new file mode 100644 index 000000000..29e6b1359 --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/IBBPacketUtils.java @@ -0,0 +1,57 @@ +package org.jivesoftware.smackx.bytestreams.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/bytestreams/ibb/IBBTestsSuite.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/IBBTestsSuite.java new file mode 100644 index 000000000..d00bab393 --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/IBBTestsSuite.java @@ -0,0 +1,21 @@ +package org.jivesoftware.smackx.bytestreams.ibb; + +import org.jivesoftware.smackx.bytestreams.ibb.packet.CloseTest; +import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtensionTest; +import org.jivesoftware.smackx.bytestreams.ibb.packet.DataTest; +import org.jivesoftware.smackx.bytestreams.ibb.packet.OpenTest; +import org.jivesoftware.smackx.bytestreams.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/bytestreams/ibb/InBandBytestreamManagerTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamManagerTest.java new file mode 100644 index 000000000..d810f6882 --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamManagerTest.java @@ -0,0 +1,187 @@ +/** + * 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.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.bytestreams.ibb.InBandBytestreamManager; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamSession; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager.StanzaType; +import org.jivesoftware.smackx.bytestreams.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/bytestreams/ibb/InBandBytestreamRequestTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamRequestTest.java new file mode 100644 index 000000000..a6f2b0ce3 --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamRequestTest.java @@ -0,0 +1,101 @@ +package org.jivesoftware.smackx.bytestreams.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.ibb.InBandBytestreamManager; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamRequest; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamSession; +import org.jivesoftware.smackx.bytestreams.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/bytestreams/ibb/InBandBytestreamSessionMessageTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSessionMessageTest.java new file mode 100644 index 000000000..a420fdff4 --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSessionMessageTest.java @@ -0,0 +1,356 @@ +package org.jivesoftware.smackx.bytestreams.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.bytestreams.ibb.InBandBytestreamManager; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamSession; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager.StanzaType; +import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension; +import org.jivesoftware.smackx.bytestreams.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/bytestreams/ibb/InBandBytestreamSessionTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSessionTest.java new file mode 100644 index 000000000..d2f20e5e9 --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSessionTest.java @@ -0,0 +1,700 @@ +package org.jivesoftware.smackx.bytestreams.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.bytestreams.ibb.InBandBytestreamManager; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamSession; +import org.jivesoftware.smackx.bytestreams.ibb.packet.Data; +import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension; +import org.jivesoftware.smackx.bytestreams.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/bytestreams/ibb/InitiationListenerTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InitiationListenerTest.java new file mode 100644 index 000000000..1091dc6f3 --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InitiationListenerTest.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.bytestreams.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.bytestreams.ibb.InBandBytestreamListener; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager; +import org.jivesoftware.smackx.bytestreams.ibb.InitiationListener; +import org.jivesoftware.smackx.bytestreams.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/bytestreams/ibb/packet/CloseTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/packet/CloseTest.java new file mode 100644 index 000000000..0139032af --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/packet/CloseTest.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.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.bytestreams.ibb.packet.Close; +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/bytestreams/ibb/packet/DataPacketExtensionTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/packet/DataPacketExtensionTest.java new file mode 100644 index 000000000..815c6b4f7 --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/packet/DataPacketExtensionTest.java @@ -0,0 +1,95 @@ +/** + * 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.ibb.packet; + +import static junit.framework.Assert.*; +import static org.custommonkey.xmlunit.XMLAssert.*; + +import java.util.Properties; + +import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension; +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/bytestreams/ibb/packet/DataTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/packet/DataTest.java new file mode 100644 index 000000000..d1902baae --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/packet/DataTest.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.bytestreams.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.jivesoftware.smackx.bytestreams.ibb.packet.Data; +import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension; +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/bytestreams/ibb/packet/OpenTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/packet/OpenTest.java new file mode 100644 index 000000000..055947e1e --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/packet/OpenTest.java @@ -0,0 +1,104 @@ +/** + * 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.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.bytestreams.ibb.InBandBytestreamManager.StanzaType; +import org.jivesoftware.smackx.bytestreams.ibb.packet.Open; +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/bytestreams/ibb/provider/OpenIQProviderTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/provider/OpenIQProviderTest.java new file mode 100644 index 000000000..1b28f5fc7 --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/provider/OpenIQProviderTest.java @@ -0,0 +1,87 @@ +/** + * 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.ibb.provider; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Properties; + +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager.StanzaType; +import org.jivesoftware.smackx.bytestreams.ibb.packet.Open; +import org.jivesoftware.smackx.bytestreams.ibb.provider.OpenIQProvider; +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/bytestreams/socks5/InitiationListenerTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/InitiationListenerTest.java new file mode 100644 index 000000000..c45776c8d --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/InitiationListenerTest.java @@ -0,0 +1,308 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.bytestreams.socks5; + +import 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.bytestreams.socks5.InitiationListener; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamListener; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager; +import org.jivesoftware.smackx.bytestreams.socks5.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/bytestreams/socks5/Socks5ByteStreamManagerTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamManagerTest.java new file mode 100644 index 000000000..0a769c27c --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamManagerTest.java @@ -0,0 +1,1102 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.bytestreams.socks5; + +import 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.bytestreams.socks5.Socks5BytestreamManager; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5Client; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5Proxy; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5Utils; +import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream; +import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost; +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.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/bytestreams/socks5/Socks5ByteStreamRequestTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamRequestTest.java new file mode 100644 index 000000000..596d7d36f --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamRequestTest.java @@ -0,0 +1,429 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.bytestreams.socks5; + +import 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.bytestreams.socks5.Socks5BytestreamManager; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamRequest; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5Utils; +import org.jivesoftware.smackx.bytestreams.socks5.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/bytestreams/socks5/Socks5ClientForInitiatorTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiatorTest.java new file mode 100644 index 000000000..fbf3eea87 --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiatorTest.java @@ -0,0 +1,310 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.bytestreams.socks5; + +import 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.bytestreams.socks5.Socks5Client; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5ClientForInitiator; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5Proxy; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5Utils; +import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream; +import org.jivesoftware.smackx.bytestreams.socks5.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/bytestreams/socks5/Socks5ClientTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientTest.java new file mode 100644 index 000000000..34470e6bf --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientTest.java @@ -0,0 +1,332 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.bytestreams.socks5; + +import 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.bytestreams.socks5.Socks5Client; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5Utils; +import org.jivesoftware.smackx.bytestreams.socks5.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/bytestreams/socks5/Socks5PacketUtils.java b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5PacketUtils.java new file mode 100644 index 000000000..3255fe104 --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/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.bytestreams.socks5; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.DiscoverItems; + +/** + * 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/bytestreams/socks5/Socks5ProxyTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ProxyTest.java new file mode 100644 index 000000000..117621bbc --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ProxyTest.java @@ -0,0 +1,360 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.bytestreams.socks5; + +import 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.jivesoftware.smackx.bytestreams.socks5.Socks5Proxy; +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/bytestreams/socks5/Socks5TestProxy.java b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5TestProxy.java new file mode 100644 index 000000000..4973f26b6 --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5TestProxy.java @@ -0,0 +1,286 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.bytestreams.socks5; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5Utils; + +/** + * 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. + * + *

+     * 
+     *   PacketCollector collector = connection.createPacketCollector(new PacketFilter());
+     *   connection.sendPacket(packet);
+     *   Packet reply = collector.nextResult();
+     * 
+     * 
+ * + * @param protocol protocol helper containing answer packets + * @param initiatorJID the user associated to the XMPP connection + * @param xmppServer the XMPP server associated to the XMPP connection + * @return a mocked XMPP connection + */ + public static Connection createMockedConnection(final Protocol protocol, + String initiatorJID, String xmppServer) { + + // mock XMPP connection + Connection connection = mock(Connection.class); + when(connection.getUser()).thenReturn(initiatorJID); + when(connection.getServiceName()).thenReturn(xmppServer); + + // mock packet collector + PacketCollector collector = mock(PacketCollector.class); + when(connection.createPacketCollector(isA(PacketFilter.class))).thenReturn( + collector); + Answer addIncoming = new Answer() { + + public Object answer(InvocationOnMock invocation) throws Throwable { + protocol.getRequests().add((Packet) invocation.getArguments()[0]); + return null; + } + }; + + // mock send method + doAnswer(addIncoming).when(connection).sendPacket(isA(Packet.class)); + Answer answer = new Answer() { + + public Packet answer(InvocationOnMock invocation) throws Throwable { + return protocol.getResponses().poll(); + } + }; + + // mock nextResult method + when(collector.nextResult(anyInt())).thenAnswer(answer); + when(collector.nextResult()).thenAnswer(answer); + + // initialize service discovery manager for this connection + new ServiceDiscoveryManager(connection); + + return connection; + } + +} diff --git a/test-unit/org/jivesoftware/util/Protocol.java b/test-unit/org/jivesoftware/util/Protocol.java new file mode 100644 index 000000000..4e42a925b --- /dev/null +++ b/test-unit/org/jivesoftware/util/Protocol.java @@ -0,0 +1,195 @@ +/** + * 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.junit.Assert.*; + +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +import org.jivesoftware.smack.packet.Packet; + +/** + * This class can be used in conjunction with a mocked XMPP connection ( + * {@link ConnectionUtils#createMockedConnection(Protocol, String, String)}) to + * verify a XMPP protocol. This can be accomplished in the following was: + *
    + *
  • add responses to packets sent over the mocked XMPP connection by the + * method to test in the order the tested method awaits them
  • + *
  • call the method to test
  • + *
  • call {@link #verifyAll()} to run assertions on the request/response pairs + *
  • + *
+ * Example: + * + *
+ * 
+ * public void methodToTest() {
+ *   Packet packet = new Packet(); // create an XMPP packet
+ *   PacketCollector collector = connection.createPacketCollector(new PacketIDFilter());
+ *   connection.sendPacket(packet);
+ *   Packet reply = collector.nextResult();
+ * }
+ * 
+ * public void testMethod() {
+ *   // create protocol
+ *   Protocol protocol = new Protocol();
+ *   // create mocked connection
+ *   Connection connection = ConnectionUtils.createMockedConnection(protocol, "user@xmpp-server", "xmpp-server");
+ *   
+ *   // add reply packet to protocol
+ *   Packet reply = new Packet();
+ *   protocol.add(reply);
+ *   
+ *   // call method to test
+ *   methodToTest();
+ *   
+ *   // verify protocol
+ *   protocol.verifyAll();
+ * }
+ * 
+ * 
+ * + * Additionally to adding the response to the protocol instance you can pass + * verifications that will be executed when {@link #verifyAll()} is invoked. + * (See {@link Verification} for more details.) + *

+ * If the {@link #printProtocol} flag is set to true {@link #verifyAll()} will + * also print out the XML messages in the order they are sent to the console. + * This may be useful to inspect the whole protocol "by hand". + * + * @author Henning Staib + */ +public class Protocol { + + /** + * Set to true to print XML messages to the console while + * verifying the protocol. + */ + public boolean printProtocol = false; + + // responses to requests are taken form this queue + Queue responses = new LinkedList(); + + // list of verifications + List[]> verificationList = new ArrayList[]>(); + + // list of requests + List requests = new ArrayList(); + + // list of all responses + List responsesList = new ArrayList(); + + /** + * Adds a responses and all verifications for the request/response pair to + * the protocol. + * + * @param response the response for a request + * @param verifications verifications for request/response pair + */ + public void addResponse(Packet response, Verification... verifications) { + responses.offer(response); + verificationList.add(verifications); + responsesList.add(response); + } + + /** + * Verifies the request/response pairs by checking if their numbers match + * and executes the verification for each pair. + */ + @SuppressWarnings("unchecked") + public void verifyAll() { + assertEquals(requests.size(), responsesList.size()); + + if (printProtocol) + System.out.println("=================== Start ===============\n"); + + for (int i = 0; i < requests.size(); i++) { + Packet request = requests.get(i); + Packet response = responsesList.get(i); + + if (printProtocol) { + System.out.println("------------------- Request -------------\n"); + System.out.println(prettyFormat(request.toXML())); + System.out.println("------------------- Response ------------\n"); + if (response != null) { + System.out.println(prettyFormat(response.toXML())); + } + else { + System.out.println("No response"); + } + } + + Verification[] verifications = verificationList.get(i); + if (verifications != null) { + for (Verification verification : verifications) { + verification.verify(request, response); + } + } + } + if (printProtocol) + System.out.println("=================== End =================\n"); + } + + /** + * Returns the responses queue. + * + * @return the responses queue + */ + protected Queue getResponses() { + return responses; + } + + /** + * Returns a list of all collected requests. + * + * @return list of requests + */ + public List getRequests() { + return requests; + } + + private String prettyFormat(String input, int indent) { + try { + Source xmlInput = new StreamSource(new StringReader(input)); + StringWriter stringWriter = new StringWriter(); + StreamResult xmlOutput = new StreamResult(stringWriter); + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", + String.valueOf(indent)); + transformer.transform(xmlInput, xmlOutput); + return xmlOutput.getWriter().toString(); + } + catch (Exception e) { + return "error while formatting the XML: " + e.getMessage(); + } + } + + private String prettyFormat(String input) { + return prettyFormat(input, 2); + } + +} diff --git a/test-unit/org/jivesoftware/util/Verification.java b/test-unit/org/jivesoftware/util/Verification.java new file mode 100644 index 000000000..03d7a8dc6 --- /dev/null +++ b/test-unit/org/jivesoftware/util/Verification.java @@ -0,0 +1,97 @@ +/** + * 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.junit.Assert.*; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; + +/** + * Implement this interface to verify a request/response pair. + *

+ * For convenience there are some useful predefined implementations. + * + * @param class of the request + * @param class of the response + * + * @author Henning Staib + */ +public interface Verification { + + /** + * Verifies that the "To" field of the request corresponds with the "From" field of + * the response. + */ + public static Verification correspondingSenderReceiver = new Verification() { + + public void verify(Packet request, Packet response) { + assertEquals(response.getFrom(), request.getTo()); + } + + }; + + /** + * Verifies that the type of the request is a GET. + */ + public static Verification requestTypeGET = new Verification() { + + public void verify(IQ request, Packet response) { + assertEquals(IQ.Type.GET, request.getType()); + } + + }; + + /** + * Verifies that the type of the request is a SET. + */ + public static Verification requestTypeSET = new Verification() { + + public void verify(IQ request, Packet response) { + assertEquals(IQ.Type.SET, request.getType()); + } + + }; + + /** + * Verifies that the type of the request is a RESULT. + */ + public static Verification requestTypeRESULT = new Verification() { + + public void verify(IQ request, Packet response) { + assertEquals(IQ.Type.RESULT, request.getType()); + } + + }; + + /** + * Verifies that the type of the request is an ERROR. + */ + public static Verification requestTypeERROR = new Verification() { + + public void verify(IQ request, Packet response) { + assertEquals(IQ.Type.ERROR, request.getType()); + } + + }; + + /** + * Implement this method to make assertions of the request/response pairs. + * + * @param request the request collected by the mocked XMPP connection + * @param response the response added to the protocol instance + */ + public void verify(T request, S response); + +} diff --git a/test/org/jivesoftware/smack/RosterTest.java b/test/org/jivesoftware/smack/RosterSmackTest.java similarity index 99% rename from test/org/jivesoftware/smack/RosterTest.java rename to test/org/jivesoftware/smack/RosterSmackTest.java index f434d52c0..3ee5efcaa 100644 --- a/test/org/jivesoftware/smack/RosterTest.java +++ b/test/org/jivesoftware/smack/RosterSmackTest.java @@ -65,13 +65,13 @@ import java.util.List; * * @author Gaston Dombiak */ -public class RosterTest extends SmackTestCase { +public class RosterSmackTest extends SmackTestCase { /** - * Constructor for RosterTest. + * Constructor for RosterSmackTest. * @param name */ - public RosterTest(String name) { + public RosterSmackTest(String name) { super(name); } diff --git a/test/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamTest.java b/test/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamTest.java new file mode 100644 index 000000000..5f2a2cb9a --- /dev/null +++ b/test/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamTest.java @@ -0,0 +1,259 @@ +/** + * 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.ibb; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Random; +import java.util.concurrent.SynchronousQueue; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.test.SmackTestCase; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamListener; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamRequest; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamSession; +import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager.StanzaType; +import org.jivesoftware.smackx.bytestreams.ibb.packet.Open; + +/** + * Test for In-Band Bytestreams with real XMPP servers. + * + * @author Henning Staib + */ +public class InBandBytestreamTest extends SmackTestCase { + + /* the amount of data transmitted in each test */ + int dataSize = 1024000; + + public InBandBytestreamTest(String arg0) { + super(arg0); + } + + /** + * Target should respond with not-acceptable error if no listeners for incoming In-Band + * Bytestream requests are registered. + * + * @throws XMPPException should not happen + */ + public void testRespondWithErrorOnInBandBytestreamRequest() throws XMPPException { + Connection targetConnection = getConnection(0); + + Connection initiatorConnection = getConnection(1); + + Open open = new Open("sessionID", 1024); + open.setFrom(initiatorConnection.getUser()); + open.setTo(targetConnection.getUser()); + + PacketCollector collector = initiatorConnection.createPacketCollector(new PacketIDFilter( + open.getPacketID())); + initiatorConnection.sendPacket(open); + Packet result = collector.nextResult(); + + assertNotNull(result.getError()); + assertEquals(XMPPError.Condition.no_acceptable.toString(), result.getError().getCondition()); + + } + + /** + * An In-Band Bytestream should be successfully established using IQ stanzas. + * + * @throws Exception should not happen + */ + public void testInBandBytestreamWithIQStanzas() throws Exception { + + Connection initiatorConnection = getConnection(0); + Connection targetConnection = getConnection(1); + + // test data + Random rand = new Random(); + final byte[] data = new byte[dataSize]; + rand.nextBytes(data); + final SynchronousQueue queue = new SynchronousQueue(); + + InBandBytestreamManager targetByteStreamManager = InBandBytestreamManager.getByteStreamManager(targetConnection); + + InBandBytestreamListener incomingByteStreamListener = new InBandBytestreamListener() { + + public void incomingBytestreamRequest(InBandBytestreamRequest request) { + InputStream inputStream; + try { + inputStream = request.accept().getInputStream(); + byte[] receivedData = new byte[dataSize]; + int totalRead = 0; + while (totalRead < dataSize) { + int read = inputStream.read(receivedData, totalRead, dataSize - totalRead); + totalRead += read; + } + queue.put(receivedData); + } + catch (Exception e) { + fail(e.getMessage()); + } + } + + }; + targetByteStreamManager.addIncomingBytestreamListener(incomingByteStreamListener); + + InBandBytestreamManager initiatorByteStreamManager = InBandBytestreamManager.getByteStreamManager(initiatorConnection); + + OutputStream outputStream = initiatorByteStreamManager.establishSession( + targetConnection.getUser()).getOutputStream(); + + // verify stream + outputStream.write(data); + outputStream.flush(); + outputStream.close(); + + assertEquals("received data not equal to sent data", data, queue.take()); + + } + + /** + * An In-Band Bytestream should be successfully established using message stanzas. + * + * @throws Exception should not happen + */ + public void testInBandBytestreamWithMessageStanzas() throws Exception { + + Connection initiatorConnection = getConnection(0); + Connection targetConnection = getConnection(1); + + // test data + Random rand = new Random(); + final byte[] data = new byte[dataSize]; + rand.nextBytes(data); + final SynchronousQueue queue = new SynchronousQueue(); + + InBandBytestreamManager targetByteStreamManager = InBandBytestreamManager.getByteStreamManager(targetConnection); + + InBandBytestreamListener incomingByteStreamListener = new InBandBytestreamListener() { + + public void incomingBytestreamRequest(InBandBytestreamRequest request) { + InputStream inputStream; + try { + inputStream = request.accept().getInputStream(); + byte[] receivedData = new byte[dataSize]; + int totalRead = 0; + while (totalRead < dataSize) { + int read = inputStream.read(receivedData, totalRead, dataSize - totalRead); + totalRead += read; + } + queue.put(receivedData); + } + catch (Exception e) { + fail(e.getMessage()); + } + } + + }; + targetByteStreamManager.addIncomingBytestreamListener(incomingByteStreamListener); + + InBandBytestreamManager initiatorByteStreamManager = InBandBytestreamManager.getByteStreamManager(initiatorConnection); + initiatorByteStreamManager.setStanza(StanzaType.MESSAGE); + + OutputStream outputStream = initiatorByteStreamManager.establishSession( + targetConnection.getUser()).getOutputStream(); + + // verify stream + outputStream.write(data); + outputStream.flush(); + outputStream.close(); + + assertEquals("received data not equal to sent data", data, queue.take()); + + } + + /** + * An In-Band Bytestream should be successfully established using IQ stanzas. The established + * session should transfer data bidirectional. + * + * @throws Exception should not happen + */ + public void testBiDirectionalInBandBytestream() throws Exception { + + Connection initiatorConnection = getConnection(0); + + Connection targetConnection = getConnection(1); + + // test data + Random rand = new Random(); + final byte[] data = new byte[dataSize]; + rand.nextBytes(data); + + final SynchronousQueue queue = new SynchronousQueue(); + + InBandBytestreamManager targetByteStreamManager = InBandBytestreamManager.getByteStreamManager(targetConnection); + + InBandBytestreamListener incomingByteStreamListener = new InBandBytestreamListener() { + + public void incomingBytestreamRequest(InBandBytestreamRequest request) { + try { + InBandBytestreamSession session = request.accept(); + OutputStream outputStream = session.getOutputStream(); + outputStream.write(data); + outputStream.flush(); + InputStream inputStream = session.getInputStream(); + byte[] receivedData = new byte[dataSize]; + int totalRead = 0; + while (totalRead < dataSize) { + int read = inputStream.read(receivedData, totalRead, dataSize - totalRead); + totalRead += read; + } + queue.put(receivedData); + } + catch (Exception e) { + fail(e.getMessage()); + } + } + + }; + targetByteStreamManager.addIncomingBytestreamListener(incomingByteStreamListener); + + InBandBytestreamManager initiatorByteStreamManager = InBandBytestreamManager.getByteStreamManager(initiatorConnection); + + InBandBytestreamSession session = initiatorByteStreamManager.establishSession(targetConnection.getUser()); + + // verify stream + byte[] receivedData = new byte[dataSize]; + InputStream inputStream = session.getInputStream(); + int totalRead = 0; + while (totalRead < dataSize) { + int read = inputStream.read(receivedData, totalRead, dataSize - totalRead); + totalRead += read; + } + + assertEquals("sent data not equal to received data", data, receivedData); + + OutputStream outputStream = session.getOutputStream(); + + outputStream.write(data); + outputStream.flush(); + outputStream.close(); + + assertEquals("received data not equal to sent data", data, queue.take()); + + } + + @Override + protected int getMaxConnections() { + return 2; + } + +} diff --git a/test/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamTest.java b/test/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamTest.java new file mode 100644 index 000000000..07381ea19 --- /dev/null +++ b/test/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamTest.java @@ -0,0 +1,337 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.bytestreams.socks5; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.test.SmackTestCase; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamListener; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamRequest; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamSession; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5PacketUtils; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5Proxy; +import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream; + +/** + * Test for Socks5 bytestreams with real XMPP servers. + * + * @author Henning Staib + */ +public class Socks5ByteStreamTest extends SmackTestCase { + + /** + * Constructor + * + * @param arg0 + */ + public Socks5ByteStreamTest(String arg0) { + super(arg0); + } + + /** + * Socks5 feature should be added to the service discovery on Smack startup. + * + * @throws XMPPException should not happen + */ + public void testInitializationSocks5FeaturesAndListenerOnStartup() throws XMPPException { + Connection connection = getConnection(0); + + assertTrue(ServiceDiscoveryManager.getInstanceFor(connection).includesFeature( + Socks5BytestreamManager.NAMESPACE)); + + } + + /** + * Target should respond with not-acceptable error if no listeners for incoming Socks5 + * bytestream requests are registered. + * + * @throws XMPPException should not happen + */ + public void testRespondWithErrorOnSocks5BytestreamRequest() throws XMPPException { + Connection targetConnection = getConnection(0); + + Connection initiatorConnection = getConnection(1); + + Bytestream bytestreamInitiation = Socks5PacketUtils.createBytestreamInitiation( + initiatorConnection.getUser(), targetConnection.getUser(), "session_id"); + bytestreamInitiation.addStreamHost("proxy.localhost", "127.0.0.1", 7777); + + PacketCollector collector = initiatorConnection.createPacketCollector(new PacketIDFilter( + bytestreamInitiation.getPacketID())); + initiatorConnection.sendPacket(bytestreamInitiation); + Packet result = collector.nextResult(); + + assertNotNull(result.getError()); + assertEquals(XMPPError.Condition.no_acceptable.toString(), result.getError().getCondition()); + + } + + /** + * Socks5 bytestream should be successfully established using the local Socks5 proxy. + * + * @throws Exception should not happen + */ + public void testSocks5BytestreamWithLocalSocks5Proxy() throws Exception { + + // setup port for local socks5 proxy + SmackConfiguration.setLocalSocks5ProxyEnabled(true); + SmackConfiguration.setLocalSocks5ProxyPort(7778); + Socks5Proxy socks5Proxy = Socks5Proxy.getSocks5Proxy(); + socks5Proxy.start(); + + assertTrue(socks5Proxy.isRunning()); + + Connection initiatorConnection = getConnection(0); + Connection targetConnection = getConnection(1); + + // test data + final byte[] data = new byte[] { 1, 2, 3 }; + final SynchronousQueue queue = new SynchronousQueue(); + + Socks5BytestreamManager targetByteStreamManager = Socks5BytestreamManager.getBytestreamManager(targetConnection); + + Socks5BytestreamListener incomingByteStreamListener = new Socks5BytestreamListener() { + + public void incomingBytestreamRequest(Socks5BytestreamRequest request) { + InputStream inputStream; + try { + Socks5BytestreamSession session = request.accept(); + inputStream = session.getInputStream(); + byte[] receivedData = new byte[3]; + inputStream.read(receivedData); + queue.put(receivedData); + } + catch (Exception e) { + fail(e.getMessage()); + } + } + + }; + targetByteStreamManager.addIncomingBytestreamListener(incomingByteStreamListener); + + Socks5BytestreamManager initiatorByteStreamManager = Socks5BytestreamManager.getBytestreamManager(initiatorConnection); + + Socks5BytestreamSession session = initiatorByteStreamManager.establishSession( + targetConnection.getUser()); + OutputStream outputStream = session.getOutputStream(); + + assertTrue(session.isDirect()); + + // verify stream + outputStream.write(data); + outputStream.flush(); + outputStream.close(); + + assertEquals("received data not equal to sent data", data, queue.take()); + + // reset default configuration + SmackConfiguration.setLocalSocks5ProxyPort(7777); + + } + + /** + * Socks5 bytestream should be successfully established using a Socks5 proxy provided by the + * XMPP server. + *

+ * This test will fail if the XMPP server doesn't provide any Socks5 proxies or the Socks5 proxy + * only allows Socks5 bytestreams in the context of a file transfer (like Openfire in default + * configuration, see xmpp.proxy.transfer.required flag). + * + * @throws Exception if no Socks5 proxies found or proxy is unwilling to activate Socks5 + * bytestream + */ + public void testSocks5BytestreamWithRemoteSocks5Proxy() throws Exception { + + // disable local socks5 proxy + SmackConfiguration.setLocalSocks5ProxyEnabled(false); + Socks5Proxy.getSocks5Proxy().stop(); + + assertFalse(Socks5Proxy.getSocks5Proxy().isRunning()); + + Connection initiatorConnection = getConnection(0); + Connection targetConnection = getConnection(1); + + // test data + final byte[] data = new byte[] { 1, 2, 3 }; + final SynchronousQueue queue = new SynchronousQueue(); + + Socks5BytestreamManager targetByteStreamManager = Socks5BytestreamManager.getBytestreamManager(targetConnection); + + Socks5BytestreamListener incomingByteStreamListener = new Socks5BytestreamListener() { + + public void incomingBytestreamRequest(Socks5BytestreamRequest request) { + InputStream inputStream; + try { + Socks5BytestreamSession session = request.accept(); + inputStream = session.getInputStream(); + byte[] receivedData = new byte[3]; + inputStream.read(receivedData); + queue.put(receivedData); + } + catch (Exception e) { + fail(e.getMessage()); + } + } + + }; + targetByteStreamManager.addIncomingBytestreamListener(incomingByteStreamListener); + + Socks5BytestreamManager initiatorByteStreamManager = Socks5BytestreamManager.getBytestreamManager(initiatorConnection); + + Socks5BytestreamSession session = initiatorByteStreamManager.establishSession( + targetConnection.getUser()); + OutputStream outputStream = session.getOutputStream(); + + assertTrue(session.isMediated()); + + // verify stream + outputStream.write(data); + outputStream.flush(); + outputStream.close(); + + assertEquals("received data not equal to sent data", data, queue.take()); + + // reset default configuration + SmackConfiguration.setLocalSocks5ProxyEnabled(true); + Socks5Proxy.getSocks5Proxy().start(); + + } + + /** + * Socks5 bytestream should be successfully established using a Socks5 proxy provided by the + * XMPP server. The established connection should transfer data bidirectional if the Socks5 + * proxy supports it. + *

+ * Support for bidirectional Socks5 bytestream: + *

    + *
  • Openfire (3.6.4 and below) - no
  • + *
  • ejabberd (2.0.5 and higher) - yes
  • + *
+ *

+ * This test will fail if the XMPP server doesn't provide any Socks5 proxies or the Socks5 proxy + * only allows Socks5 bytestreams in the context of a file transfer (like Openfire in default + * configuration, see xmpp.proxy.transfer.required flag). + * + * @throws Exception if no Socks5 proxies found or proxy is unwilling to activate Socks5 + * bytestream + */ + public void testBiDirectionalSocks5BytestreamWithRemoteSocks5Proxy() throws Exception { + + Connection initiatorConnection = getConnection(0); + + // disable local socks5 proxy + SmackConfiguration.setLocalSocks5ProxyEnabled(false); + Socks5Proxy.getSocks5Proxy().stop(); + + assertFalse(Socks5Proxy.getSocks5Proxy().isRunning()); + + Connection targetConnection = getConnection(1); + + // test data + final byte[] data = new byte[] { 1, 2, 3 }; + final SynchronousQueue queue = new SynchronousQueue(); + + Socks5BytestreamManager targetByteStreamManager = Socks5BytestreamManager.getBytestreamManager(targetConnection); + + Socks5BytestreamListener incomingByteStreamListener = new Socks5BytestreamListener() { + + public void incomingBytestreamRequest(Socks5BytestreamRequest request) { + try { + Socks5BytestreamSession session = request.accept(); + OutputStream outputStream = session.getOutputStream(); + outputStream.write(data); + outputStream.flush(); + InputStream inputStream = session.getInputStream(); + byte[] receivedData = new byte[3]; + inputStream.read(receivedData); + queue.put(receivedData); + session.close(); + } + catch (Exception e) { + fail(e.getMessage()); + } + } + + }; + targetByteStreamManager.addIncomingBytestreamListener(incomingByteStreamListener); + + Socks5BytestreamManager initiatorByteStreamManager = Socks5BytestreamManager.getBytestreamManager(initiatorConnection); + + Socks5BytestreamSession session = initiatorByteStreamManager.establishSession(targetConnection.getUser()); + + assertTrue(session.isMediated()); + + // verify stream + final byte[] receivedData = new byte[3]; + final InputStream inputStream = session.getInputStream(); + + FutureTask futureTask = new FutureTask(new Callable() { + + public Integer call() throws Exception { + return inputStream.read(receivedData); + } + }); + Thread executor = new Thread(futureTask); + executor.start(); + + try { + futureTask.get(2000, TimeUnit.MILLISECONDS); + } + catch (TimeoutException e) { + // reset default configuration + SmackConfiguration.setLocalSocks5ProxyEnabled(true); + Socks5Proxy.getSocks5Proxy().start(); + + fail("Couldn't send data from target to inititator"); + } + + assertEquals("sent data not equal to received data", data, receivedData); + + OutputStream outputStream = session.getOutputStream(); + + outputStream.write(data); + outputStream.flush(); + outputStream.close(); + + assertEquals("received data not equal to sent data", data, queue.take()); + + session.close(); + + // reset default configuration + SmackConfiguration.setLocalSocks5ProxyEnabled(true); + Socks5Proxy.getSocks5Proxy().start(); + + } + + @Override + protected int getMaxConnections() { + return 2; + } + +}