merged branch improve_bytestreams in trunk
git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/trunk@11821 b35dd754-fafc-0310-a699-88a17e54d16e
This commit is contained in:
parent
ef74695a1b
commit
8b54f34153
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -8,16 +8,24 @@
|
||||||
<className>org.jivesoftware.smack.PrivacyListManager</className>
|
<className>org.jivesoftware.smack.PrivacyListManager</className>
|
||||||
<className>org.jivesoftware.smackx.XHTMLManager</className>
|
<className>org.jivesoftware.smackx.XHTMLManager</className>
|
||||||
<className>org.jivesoftware.smackx.muc.MultiUserChat</className>
|
<className>org.jivesoftware.smackx.muc.MultiUserChat</className>
|
||||||
|
<className>org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager</className>
|
||||||
|
<className>org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager</className>
|
||||||
<className>org.jivesoftware.smackx.filetransfer.FileTransferManager</className>
|
<className>org.jivesoftware.smackx.filetransfer.FileTransferManager</className>
|
||||||
<className>org.jivesoftware.smackx.LastActivityManager</className>
|
<className>org.jivesoftware.smackx.LastActivityManager</className>
|
||||||
<className>org.jivesoftware.smack.ReconnectionManager</className>
|
<className>org.jivesoftware.smack.ReconnectionManager</className>
|
||||||
<className>org.jivesoftware.smackx.commands.AdHocCommandManager</className>
|
<className>org.jivesoftware.smackx.commands.AdHocCommandManager</className>
|
||||||
</startupClasses>
|
</startupClasses>
|
||||||
|
|
||||||
<!-- Paket reply timeout in milliseconds -->
|
<!-- Packet reply timeout in milliseconds -->
|
||||||
<packetReplyTimeout>5000</packetReplyTimeout>
|
<packetReplyTimeout>5000</packetReplyTimeout>
|
||||||
|
|
||||||
<!-- Keep-alive interval in milleseconds -->
|
<!-- Keep-alive interval in milliseconds -->
|
||||||
<keepAliveInterval>30000</keepAliveInterval>
|
<keepAliveInterval>30000</keepAliveInterval>
|
||||||
|
|
||||||
</smack>
|
<!-- Enable/Disable local Socks5 proxy -->
|
||||||
|
<localSocks5ProxyEnabled>true</localSocks5ProxyEnabled>
|
||||||
|
|
||||||
|
<!-- Port of the local Socks5 proxy -->
|
||||||
|
<localSocks5ProxyPort>7777</localSocks5ProxyPort>
|
||||||
|
|
||||||
|
</smack>
|
||||||
|
|
|
@ -158,7 +158,7 @@
|
||||||
<className>org.jivesoftware.smackx.packet.OfflineMessageInfo$Provider</className>
|
<className>org.jivesoftware.smackx.packet.OfflineMessageInfo$Provider</className>
|
||||||
</extensionProvider>
|
</extensionProvider>
|
||||||
|
|
||||||
<!-- Last Activity -->
|
<!-- Last Activity -->
|
||||||
<iqProvider>
|
<iqProvider>
|
||||||
<elementName>query</elementName>
|
<elementName>query</elementName>
|
||||||
<namespace>jabber:iq:last</namespace>
|
<namespace>jabber:iq:last</namespace>
|
||||||
|
@ -186,7 +186,7 @@
|
||||||
<className>org.jivesoftware.smackx.provider.MultipleAddressesProvider</className>
|
<className>org.jivesoftware.smackx.provider.MultipleAddressesProvider</className>
|
||||||
</extensionProvider>
|
</extensionProvider>
|
||||||
|
|
||||||
<!-- FileTransfer -->
|
<!-- FileTransfer -->
|
||||||
<iqProvider>
|
<iqProvider>
|
||||||
<elementName>si</elementName>
|
<elementName>si</elementName>
|
||||||
<namespace>http://jabber.org/protocol/si</namespace>
|
<namespace>http://jabber.org/protocol/si</namespace>
|
||||||
|
@ -196,25 +196,31 @@
|
||||||
<iqProvider>
|
<iqProvider>
|
||||||
<elementName>query</elementName>
|
<elementName>query</elementName>
|
||||||
<namespace>http://jabber.org/protocol/bytestreams</namespace>
|
<namespace>http://jabber.org/protocol/bytestreams</namespace>
|
||||||
<className>org.jivesoftware.smackx.provider.BytestreamsProvider</className>
|
<className>org.jivesoftware.smackx.bytestreams.socks5.provider.BytestreamsProvider</className>
|
||||||
</iqProvider>
|
</iqProvider>
|
||||||
|
|
||||||
<iqProvider>
|
<iqProvider>
|
||||||
<elementName>open</elementName>
|
<elementName>open</elementName>
|
||||||
<namespace>http://jabber.org/protocol/ibb</namespace>
|
<namespace>http://jabber.org/protocol/ibb</namespace>
|
||||||
<className>org.jivesoftware.smackx.provider.IBBProviders$Open</className>
|
<className>org.jivesoftware.smackx.bytestreams.ibb.provider.OpenIQProvider</className>
|
||||||
|
</iqProvider>
|
||||||
|
|
||||||
|
<iqProvider>
|
||||||
|
<elementName>data</elementName>
|
||||||
|
<namespace>http://jabber.org/protocol/ibb</namespace>
|
||||||
|
<className>org.jivesoftware.smackx.bytestreams.ibb.provider.DataPacketProvider</className>
|
||||||
</iqProvider>
|
</iqProvider>
|
||||||
|
|
||||||
<iqProvider>
|
<iqProvider>
|
||||||
<elementName>close</elementName>
|
<elementName>close</elementName>
|
||||||
<namespace>http://jabber.org/protocol/ibb</namespace>
|
<namespace>http://jabber.org/protocol/ibb</namespace>
|
||||||
<className>org.jivesoftware.smackx.provider.IBBProviders$Close</className>
|
<className>org.jivesoftware.smackx.bytestreams.ibb.provider.CloseIQProvider</className>
|
||||||
</iqProvider>
|
</iqProvider>
|
||||||
|
|
||||||
<extensionProvider>
|
<extensionProvider>
|
||||||
<elementName>data</elementName>
|
<elementName>data</elementName>
|
||||||
<namespace>http://jabber.org/protocol/ibb</namespace>
|
<namespace>http://jabber.org/protocol/ibb</namespace>
|
||||||
<className>org.jivesoftware.smackx.provider.IBBProviders$Data</className>
|
<className>org.jivesoftware.smackx.bytestreams.ibb.provider.DataPacketProvider</className>
|
||||||
</extensionProvider>
|
</extensionProvider>
|
||||||
|
|
||||||
<!-- Privacy -->
|
<!-- Privacy -->
|
||||||
|
@ -491,7 +497,7 @@
|
||||||
<className>org.jivesoftware.smackx.provider.HeaderProvider</className>
|
<className>org.jivesoftware.smackx.provider.HeaderProvider</className>
|
||||||
</extensionProvider>
|
</extensionProvider>
|
||||||
|
|
||||||
<!-- XEP-0060 pubsub -->
|
<!-- XEP-0060 pubsub -->
|
||||||
<iqProvider>
|
<iqProvider>
|
||||||
<elementName>pubsub</elementName>
|
<elementName>pubsub</elementName>
|
||||||
<namespace>http://jabber.org/protocol/pubsub</namespace>
|
<namespace>http://jabber.org/protocol/pubsub</namespace>
|
||||||
|
@ -546,7 +552,7 @@
|
||||||
<className>org.jivesoftware.smackx.pubsub.provider.FormNodeProvider</className>
|
<className>org.jivesoftware.smackx.pubsub.provider.FormNodeProvider</className>
|
||||||
</extensionProvider>
|
</extensionProvider>
|
||||||
|
|
||||||
<!-- XEP-0060 pubsub#owner -->
|
<!-- XEP-0060 pubsub#owner -->
|
||||||
<iqProvider>
|
<iqProvider>
|
||||||
<elementName>pubsub</elementName>
|
<elementName>pubsub</elementName>
|
||||||
<namespace>http://jabber.org/protocol/pubsub#owner</namespace>
|
<namespace>http://jabber.org/protocol/pubsub#owner</namespace>
|
||||||
|
@ -565,7 +571,7 @@
|
||||||
<className>org.jivesoftware.smackx.pubsub.provider.FormNodeProvider</className>
|
<className>org.jivesoftware.smackx.pubsub.provider.FormNodeProvider</className>
|
||||||
</extensionProvider>
|
</extensionProvider>
|
||||||
|
|
||||||
<!-- XEP-0060 pubsub#event -->
|
<!-- XEP-0060 pubsub#event -->
|
||||||
<extensionProvider>
|
<extensionProvider>
|
||||||
<elementName>event</elementName>
|
<elementName>event</elementName>
|
||||||
<namespace>http://jabber.org/protocol/pubsub#event</namespace>
|
<namespace>http://jabber.org/protocol/pubsub#event</namespace>
|
||||||
|
@ -621,7 +627,7 @@
|
||||||
<className>org.jivesoftware.smackx.packet.Nick$Provider</className>
|
<className>org.jivesoftware.smackx.packet.Nick$Provider</className>
|
||||||
</extensionProvider>
|
</extensionProvider>
|
||||||
|
|
||||||
<!-- Attention -->
|
<!-- Attention -->
|
||||||
<extensionProvider>
|
<extensionProvider>
|
||||||
<elementName>attention</elementName>
|
<elementName>attention</elementName>
|
||||||
<namespace>urn:xmpp:attention:0</namespace>
|
<namespace>urn:xmpp:attention:0</namespace>
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -50,6 +50,9 @@ public final class SmackConfiguration {
|
||||||
private static int keepAliveInterval = 30000;
|
private static int keepAliveInterval = 30000;
|
||||||
private static Vector<String> defaultMechs = new Vector<String>();
|
private static Vector<String> defaultMechs = new Vector<String>();
|
||||||
|
|
||||||
|
private static boolean localSocks5ProxyEnabled = true;
|
||||||
|
private static int localSocks5ProxyPort = 7777;
|
||||||
|
|
||||||
private SmackConfiguration() {
|
private SmackConfiguration() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,6 +93,12 @@ public final class SmackConfiguration {
|
||||||
}
|
}
|
||||||
else if (parser.getName().equals("mechName")) {
|
else if (parser.getName().equals("mechName")) {
|
||||||
defaultMechs.add(parser.nextText());
|
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();
|
eventType = parser.next();
|
||||||
|
@ -230,6 +239,43 @@ public final class SmackConfiguration {
|
||||||
return defaultMechs;
|
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 {
|
private static void parseClassToLoad(XmlPullParser parser) throws Exception {
|
||||||
String className = parser.nextText();
|
String className = parser.nextText();
|
||||||
// Attempt to load the class so that the class can get initialized
|
// Attempt to load the class so that the class can get initialized
|
||||||
|
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* BytestreamListener can be registered at the {@link Socks5BytestreamManager} or the
|
||||||
|
* {@link InBandBytestreamManager}.
|
||||||
|
* <p>
|
||||||
|
* There are two ways to add this listener. See
|
||||||
|
* {@link BytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and
|
||||||
|
* {@link BytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)} for further
|
||||||
|
* details.
|
||||||
|
* <p>
|
||||||
|
* {@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);
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* Use this method if you are awaiting an incoming bytestream request from a specific user.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* To establish a bytestream after negotiation the kind of data to be sent (e.g. file transfer)
|
||||||
|
* use {@link #establishSession(String, String)}.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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();
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
}
|
|
@ -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 <a
|
||||||
|
* href="http://xmpp.org/extensions/xep-0047.html">XEP-0047</a>.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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 <a href="http://xmpp.org/extensions/xep-0079.html">Advanced Message
|
||||||
|
* Processing</a>. To set the stanza that should be used invoke {@link #setStanza(StanzaType)}.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* If a session ID for the In-Band Bytestream was already negotiated (e.g. while negotiating a file
|
||||||
|
* transfer) invoke {@link #establishSession(String, String)}.
|
||||||
|
* <p>
|
||||||
|
* 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)}.
|
||||||
|
* <p>
|
||||||
|
* Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
|
||||||
|
* In-Band bytestream requests sent in the context of <a
|
||||||
|
* href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
||||||
|
* {@link FileTransferManager})
|
||||||
|
* <p>
|
||||||
|
* If no {@link 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<Connection, InBandBytestreamManager> managers = new HashMap<Connection, InBandBytestreamManager>();
|
||||||
|
|
||||||
|
/* 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<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* list of listeners that respond to all In-Band Bytestream requests if there are no user
|
||||||
|
* specific listeners for that request
|
||||||
|
*/
|
||||||
|
private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());
|
||||||
|
|
||||||
|
/* 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<String, InBandBytestreamSession> sessions = new ConcurrentHashMap<String, InBandBytestreamSession>();
|
||||||
|
|
||||||
|
/* 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<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* If no listeners are registered all In-Band Bytestream request are rejected with a
|
||||||
|
* <not-acceptable/> error.
|
||||||
|
* <p>
|
||||||
|
* Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
|
||||||
|
* Socks5 bytestream requests sent in the context of <a
|
||||||
|
* href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
||||||
|
* {@link FileTransferManager})
|
||||||
|
*
|
||||||
|
* @param listener the listener to register
|
||||||
|
*/
|
||||||
|
public void addIncomingBytestreamListener(BytestreamListener listener) {
|
||||||
|
this.allRequestListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the given listener from the list of listeners for all incoming 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.
|
||||||
|
* <p>
|
||||||
|
* Use this method if you are awaiting an incoming Socks5 bytestream request from a specific
|
||||||
|
* user.
|
||||||
|
* <p>
|
||||||
|
* If no listeners are registered all In-Band Bytestream request are rejected with a
|
||||||
|
* <not-acceptable/> error.
|
||||||
|
* <p>
|
||||||
|
* Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
|
||||||
|
* Socks5 bytestream requests sent in the context of <a
|
||||||
|
* href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
||||||
|
* {@link FileTransferManager})
|
||||||
|
*
|
||||||
|
* @param listener the listener to register
|
||||||
|
* @param initiatorJID the JID of the user that wants to establish 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* The recommended default block size is 4096 bytes. See <a
|
||||||
|
* href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> 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.
|
||||||
|
* <p>
|
||||||
|
* The default block size must be between 1 and 65535 bytes. The recommended default block size
|
||||||
|
* is 4096 bytes. See <a href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* The maximum block size must be between 1 and 65535 bytes.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* Default is {@link StanzaType#IQ}. See <a
|
||||||
|
* href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
|
||||||
|
*
|
||||||
|
* @return the stanza used to send data packets
|
||||||
|
*/
|
||||||
|
public StanzaType getStanza() {
|
||||||
|
return stanza;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the stanza used to send data packets.
|
||||||
|
* <p>
|
||||||
|
* The use of {@link StanzaType#IQ} is recommended. See <a
|
||||||
|
* href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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<BytestreamListener> getAllRequestListeners() {
|
||||||
|
return this.allRequestListeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the sessions map.
|
||||||
|
*
|
||||||
|
* @return the sessions map
|
||||||
|
*/
|
||||||
|
protected Map<String, InBandBytestreamSession> getSessions() {
|
||||||
|
return sessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of session IDs that should be ignored by the InitialtionListener
|
||||||
|
*
|
||||||
|
* @return list of session IDs
|
||||||
|
*/
|
||||||
|
protected List<String> 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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* In-band bytestreams are bidirectional and this session encapsulates the streams for both
|
||||||
|
* directions.
|
||||||
|
* <p>
|
||||||
|
* 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 <code>false</code>.
|
||||||
|
*
|
||||||
|
* @return <code>true</code> if both streams will be closed if one of the streams is closed,
|
||||||
|
* <code>false</code> 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 <code>false</code>.
|
||||||
|
*
|
||||||
|
* @param closeBothStreamsEnabled <code>true</code> if both streams should be closed if one of
|
||||||
|
* the streams is closed, <code>false</code> 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<DataPacketExtension> dataQueue = new LinkedBlockingQueue<DataPacketExtension>();
|
||||||
|
|
||||||
|
/* 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 <code>true</code> if data was received, otherwise <code>false</code>
|
||||||
|
* @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 <code>true</code> 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 (<a
|
||||||
|
* href="http://xmpp.org/extensions/xep-0047.html#example-5" >XEP-0047</a> Section 2.1).
|
||||||
|
* <p>
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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("<close ");
|
||||||
|
buf.append("xmlns=\"");
|
||||||
|
buf.append(InBandBytestreamManager.NAMESPACE);
|
||||||
|
buf.append("\" ");
|
||||||
|
buf.append("sid=\"");
|
||||||
|
buf.append(sessionID);
|
||||||
|
buf.append("\"");
|
||||||
|
buf.append("/>");
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* Convenience method for <code>packet.getExtension("data",
|
||||||
|
* "http://jabber.org/protocol/ibb")</code>.
|
||||||
|
*
|
||||||
|
* @return the data packet extension
|
||||||
|
*/
|
||||||
|
public DataPacketExtension getDataPacketExtension() {
|
||||||
|
return this.dataPacketExtension;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getChildElementXML() {
|
||||||
|
return this.dataPacketExtension.toXML();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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 <a
|
||||||
|
* href="http://xmpp.org/extensions/xep-0047.html#sec">XEP-0047</a> Section
|
||||||
|
* 6.
|
||||||
|
*
|
||||||
|
* @return the decoded data
|
||||||
|
*/
|
||||||
|
public byte[] getDecodedData() {
|
||||||
|
// return cached decoded data
|
||||||
|
if (this.decodedData != null) {
|
||||||
|
return this.decodedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// data must not contain the pad (=) other than end of data
|
||||||
|
if (data.matches(".*={1,2}+.+")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeBase64 will return null if bad characters are included
|
||||||
|
this.decodedData = StringUtils.decodeBase64(data);
|
||||||
|
return this.decodedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getElementName() {
|
||||||
|
return ELEMENT_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNamespace() {
|
||||||
|
return InBandBytestreamManager.NAMESPACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toXML() {
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
buf.append("<");
|
||||||
|
buf.append(getElementName());
|
||||||
|
buf.append(" ");
|
||||||
|
buf.append("xmlns=\"");
|
||||||
|
buf.append(InBandBytestreamManager.NAMESPACE);
|
||||||
|
buf.append("\" ");
|
||||||
|
buf.append("seq=\"");
|
||||||
|
buf.append(seq);
|
||||||
|
buf.append("\" ");
|
||||||
|
buf.append("sid=\"");
|
||||||
|
buf.append(sessionID);
|
||||||
|
buf.append("\">");
|
||||||
|
buf.append(data);
|
||||||
|
buf.append("</");
|
||||||
|
buf.append(getElementName());
|
||||||
|
buf.append(">");
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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("<open ");
|
||||||
|
buf.append("xmlns=\"");
|
||||||
|
buf.append(InBandBytestreamManager.NAMESPACE);
|
||||||
|
buf.append("\" ");
|
||||||
|
buf.append("block-size=\"");
|
||||||
|
buf.append(blockSize);
|
||||||
|
buf.append("\" ");
|
||||||
|
buf.append("sid=\"");
|
||||||
|
buf.append(sessionID);
|
||||||
|
buf.append("\" ");
|
||||||
|
buf.append("stanza=\"");
|
||||||
|
buf.append(stanza.toString().toLowerCase());
|
||||||
|
buf.append("\"");
|
||||||
|
buf.append("/>");
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,26 +1,33 @@
|
||||||
/**
|
/**
|
||||||
* $Revision:$
|
* All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* $Date:$
|
* you may not use this file except in compliance with the License.
|
||||||
*
|
* You may obtain a copy of the License at
|
||||||
* Copyright 2003-2007 Jive Software.
|
*
|
||||||
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
* 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.
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* You may obtain a copy of the License at
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
*
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* See the License for the specific language governing permissions and
|
||||||
*
|
* limitations under the License.
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
*/
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
package org.jivesoftware.smackx.bytestreams.ibb.provider;
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
import org.jivesoftware.smack.packet.IQ;
|
||||||
* limitations under the License.
|
import org.jivesoftware.smack.provider.IQProvider;
|
||||||
*/
|
import org.jivesoftware.smackx.bytestreams.ibb.packet.Close;
|
||||||
package org.jivesoftware.smackx.filetransfer;
|
import org.xmlpull.v1.XmlPullParser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Parses a close In-Band Bytestream packet.
|
||||||
*/
|
*
|
||||||
public interface FileTransferNegotiatorManager {
|
* @author Henning Staib
|
||||||
StreamNegotiator createNegotiator();
|
*/
|
||||||
}
|
public class CloseIQProvider implements IQProvider {
|
||||||
|
|
||||||
|
public IQ parseIQ(XmlPullParser parser) throws Exception {
|
||||||
|
String sid = parser.getAttributeValue("", "sid");
|
||||||
|
return new Close(sid);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 (<a
|
||||||
|
* href="http://xmpp.org/extensions/xep-0065.html#usecase-alternate">XEP-0065</a> Section 5.2.A2).
|
||||||
|
*
|
||||||
|
* @author Henning Staib
|
||||||
|
*/
|
||||||
|
final class InitiationListener implements PacketListener {
|
||||||
|
|
||||||
|
/* manager containing the listeners and the XMPP connection */
|
||||||
|
private final Socks5BytestreamManager manager;
|
||||||
|
|
||||||
|
/* packet filter for all SOCKS5 Bytestream requests */
|
||||||
|
private final PacketFilter initFilter = new AndFilter(new PacketTypeFilter(Bytestream.class),
|
||||||
|
new IQTypeFilter(IQ.Type.SET));
|
||||||
|
|
||||||
|
/* executor service to process incoming requests concurrently */
|
||||||
|
private final ExecutorService initiationListenerExecutor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param manager the SOCKS5 Bytestream manager
|
||||||
|
*/
|
||||||
|
protected InitiationListener(Socks5BytestreamManager manager) {
|
||||||
|
this.manager = manager;
|
||||||
|
initiationListenerExecutor = Executors.newCachedThreadPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void processPacket(final Packet packet) {
|
||||||
|
initiationListenerExecutor.execute(new Runnable() {
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
processRequest(packet);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processRequest(Packet packet) {
|
||||||
|
Bytestream byteStreamRequest = (Bytestream) packet;
|
||||||
|
|
||||||
|
// ignore request if in ignore list
|
||||||
|
if (this.manager.getIgnoredBytestreamRequests().remove(byteStreamRequest.getSessionID())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// build bytestream request from packet
|
||||||
|
Socks5BytestreamRequest request = new Socks5BytestreamRequest(this.manager,
|
||||||
|
byteStreamRequest);
|
||||||
|
|
||||||
|
// notify listeners for bytestream initiation from a specific user
|
||||||
|
BytestreamListener userListener = this.manager.getUserListener(byteStreamRequest.getFrom());
|
||||||
|
if (userListener != null) {
|
||||||
|
userListener.incomingBytestreamRequest(request);
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (!this.manager.getAllRequestListeners().isEmpty()) {
|
||||||
|
/*
|
||||||
|
* if there is no user specific listener inform listeners for all initiation requests
|
||||||
|
*/
|
||||||
|
for (BytestreamListener listener : this.manager.getAllRequestListeners()) {
|
||||||
|
listener.incomingBytestreamRequest(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
/*
|
||||||
|
* if there is no listener for this initiation request, reply with reject message
|
||||||
|
*/
|
||||||
|
this.manager.replyRejectPacket(byteStreamRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the packet filter for SOCKS5 Bytestream initialization requests.
|
||||||
|
*
|
||||||
|
* @return the packet filter for SOCKS5 Bytestream initialization requests
|
||||||
|
*/
|
||||||
|
protected PacketFilter getFilter() {
|
||||||
|
return this.initFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuts down the listeners executor service.
|
||||||
|
*/
|
||||||
|
protected void shutdown() {
|
||||||
|
this.initiationListenerExecutor.shutdownNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.jivesoftware.smackx.bytestreams.socks5;
|
||||||
|
|
||||||
|
import org.jivesoftware.smackx.bytestreams.BytestreamListener;
|
||||||
|
import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Socks5BytestreamListener are informed if a remote user wants to initiate a SOCKS5 Bytestream.
|
||||||
|
* Implement this interface to handle incoming SOCKS5 Bytestream requests.
|
||||||
|
* <p>
|
||||||
|
* There are two ways to add this listener. See
|
||||||
|
* {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and
|
||||||
|
* {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)} for
|
||||||
|
* further details.
|
||||||
|
*
|
||||||
|
* @author Henning Staib
|
||||||
|
*/
|
||||||
|
public abstract class Socks5BytestreamListener implements BytestreamListener {
|
||||||
|
|
||||||
|
public void incomingBytestreamRequest(BytestreamRequest request) {
|
||||||
|
incomingBytestreamRequest((Socks5BytestreamRequest) request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This listener is notified if a SOCKS5 Bytestream request from another user has been received.
|
||||||
|
*
|
||||||
|
* @param request the incoming SOCKS5 Bytestream request
|
||||||
|
*/
|
||||||
|
public abstract void incomingBytestreamRequest(Socks5BytestreamRequest request);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,760 @@
|
||||||
|
/**
|
||||||
|
* All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.jivesoftware.smackx.bytestreams.socks5;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
import org.jivesoftware.smack.AbstractConnectionListener;
|
||||||
|
import org.jivesoftware.smack.Connection;
|
||||||
|
import org.jivesoftware.smack.ConnectionCreationListener;
|
||||||
|
import org.jivesoftware.smack.XMPPException;
|
||||||
|
import org.jivesoftware.smack.packet.IQ;
|
||||||
|
import org.jivesoftware.smack.packet.Packet;
|
||||||
|
import org.jivesoftware.smack.packet.XMPPError;
|
||||||
|
import org.jivesoftware.smackx.ServiceDiscoveryManager;
|
||||||
|
import org.jivesoftware.smackx.bytestreams.BytestreamListener;
|
||||||
|
import org.jivesoftware.smackx.bytestreams.BytestreamManager;
|
||||||
|
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
|
||||||
|
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
|
||||||
|
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHostUsed;
|
||||||
|
import org.jivesoftware.smackx.filetransfer.FileTransferManager;
|
||||||
|
import org.jivesoftware.smackx.packet.DiscoverInfo;
|
||||||
|
import org.jivesoftware.smackx.packet.DiscoverItems;
|
||||||
|
import org.jivesoftware.smackx.packet.SyncPacketSend;
|
||||||
|
import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
|
||||||
|
import org.jivesoftware.smackx.packet.DiscoverItems.Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Socks5BytestreamManager class handles establishing SOCKS5 Bytestreams as specified in the <a
|
||||||
|
* href="http://xmpp.org/extensions/xep-0065.html">XEP-0065</a>.
|
||||||
|
* <p>
|
||||||
|
* A SOCKS5 Bytestream is negotiated partly over the XMPP XML stream and partly over a separate
|
||||||
|
* socket. The actual transfer though takes place over a separately created socket.
|
||||||
|
* <p>
|
||||||
|
* A SOCKS5 Bytestream generally has three parties, the initiator, the target, and the stream host.
|
||||||
|
* The stream host is a specialized SOCKS5 proxy setup on a server, or, the initiator can act as the
|
||||||
|
* stream host.
|
||||||
|
* <p>
|
||||||
|
* To establish a SOCKS5 Bytestream invoke the {@link #establishSession(String)} method. This will
|
||||||
|
* negotiate a SOCKS5 Bytestream with the given target JID and return a socket.
|
||||||
|
* <p>
|
||||||
|
* If a session ID for the SOCKS5 Bytestream was already negotiated (e.g. while negotiating a file
|
||||||
|
* transfer) invoke {@link #establishSession(String, String)}.
|
||||||
|
* <p>
|
||||||
|
* To handle incoming SOCKS5 Bytestream requests add an {@link Socks5BytestreamListener} to the
|
||||||
|
* manager. There are two ways to add this listener. If you want to be informed about incoming
|
||||||
|
* SOCKS5 Bytestreams from a specific user add the listener by invoking
|
||||||
|
* {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the listener should
|
||||||
|
* respond to all SOCKS5 Bytestream requests invoke
|
||||||
|
* {@link #addIncomingBytestreamListener(BytestreamListener)}.
|
||||||
|
* <p>
|
||||||
|
* Note that the registered {@link Socks5BytestreamListener} will NOT be notified on incoming Socks5
|
||||||
|
* bytestream requests sent in the context of <a
|
||||||
|
* href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
||||||
|
* {@link FileTransferManager})
|
||||||
|
* <p>
|
||||||
|
* If no {@link Socks5BytestreamListener}s are registered, all incoming SOCKS5 Bytestream requests
|
||||||
|
* will be rejected by returning a <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<Connection, Socks5BytestreamManager> managers = new HashMap<Connection, Socks5BytestreamManager>();
|
||||||
|
|
||||||
|
/* XMPP connection */
|
||||||
|
private final Connection connection;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* assigns a user to a listener that is informed if a bytestream request for this user is
|
||||||
|
* received
|
||||||
|
*/
|
||||||
|
private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* list of listeners that respond to all bytestream requests if there are not user specific
|
||||||
|
* listeners for that request
|
||||||
|
*/
|
||||||
|
private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());
|
||||||
|
|
||||||
|
/* listener that handles all incoming bytestream requests */
|
||||||
|
private final InitiationListener initiationListener;
|
||||||
|
|
||||||
|
/* timeout to wait for the response to the SOCKS5 Bytestream initialization request */
|
||||||
|
private int targetResponseTimeout = 10000;
|
||||||
|
|
||||||
|
/* timeout for connecting to the SOCKS5 proxy selected by the target */
|
||||||
|
private int proxyConnectionTimeout = 10000;
|
||||||
|
|
||||||
|
/* blacklist of errornous SOCKS5 proxies */
|
||||||
|
private final List<String> proxyBlacklist = Collections.synchronizedList(new LinkedList<String>());
|
||||||
|
|
||||||
|
/* remember the last proxy that worked to prioritize it */
|
||||||
|
private String lastWorkingProxy = null;
|
||||||
|
|
||||||
|
/* flag to enable/disable prioritization of last working proxy */
|
||||||
|
private boolean proxyPrioritizationEnabled = true;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* list containing session IDs of SOCKS5 Bytestream initialization packets that should be
|
||||||
|
* ignored by the InitiationListener
|
||||||
|
*/
|
||||||
|
private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Socks5BytestreamManager to handle SOCKS5 Bytestreams for a given
|
||||||
|
* {@link Connection}.
|
||||||
|
* <p>
|
||||||
|
* If no manager exists a new is created and initialized.
|
||||||
|
*
|
||||||
|
* @param connection the XMPP connection or <code>null</code> if given connection is
|
||||||
|
* <code>null</code>
|
||||||
|
* @return the Socks5BytestreamManager for the given XMPP connection
|
||||||
|
*/
|
||||||
|
public static synchronized Socks5BytestreamManager getBytestreamManager(Connection connection) {
|
||||||
|
if (connection == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Socks5BytestreamManager manager = managers.get(connection);
|
||||||
|
if (manager == null) {
|
||||||
|
manager = new Socks5BytestreamManager(connection);
|
||||||
|
managers.put(connection, manager);
|
||||||
|
manager.activate();
|
||||||
|
}
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor.
|
||||||
|
*
|
||||||
|
* @param connection the XMPP connection
|
||||||
|
*/
|
||||||
|
private Socks5BytestreamManager(Connection connection) {
|
||||||
|
this.connection = connection;
|
||||||
|
this.initiationListener = new InitiationListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request unless
|
||||||
|
* there is a user specific BytestreamListener registered.
|
||||||
|
* <p>
|
||||||
|
* If no listeners are registered all SOCKS5 Bytestream request are rejected with a
|
||||||
|
* <not-acceptable/> error.
|
||||||
|
* <p>
|
||||||
|
* Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
|
||||||
|
* bytestream requests sent in the context of <a
|
||||||
|
* href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
||||||
|
* {@link FileTransferManager})
|
||||||
|
*
|
||||||
|
* @param listener the listener to register
|
||||||
|
*/
|
||||||
|
public void addIncomingBytestreamListener(BytestreamListener listener) {
|
||||||
|
this.allRequestListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the given listener from the list of listeners for all incoming SOCKS5 Bytestream
|
||||||
|
* requests.
|
||||||
|
*
|
||||||
|
* @param listener the listener to remove
|
||||||
|
*/
|
||||||
|
public void removeIncomingBytestreamListener(BytestreamListener listener) {
|
||||||
|
this.allRequestListeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request from the
|
||||||
|
* given user.
|
||||||
|
* <p>
|
||||||
|
* Use this method if you are awaiting an incoming SOCKS5 Bytestream request from a specific
|
||||||
|
* user.
|
||||||
|
* <p>
|
||||||
|
* If no listeners are registered all SOCKS5 Bytestream request are rejected with a
|
||||||
|
* <not-acceptable/> error.
|
||||||
|
* <p>
|
||||||
|
* Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
|
||||||
|
* bytestream requests sent in the context of <a
|
||||||
|
* href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
||||||
|
* {@link FileTransferManager})
|
||||||
|
*
|
||||||
|
* @param listener the listener to register
|
||||||
|
* @param initiatorJID the JID of the user that wants to establish a SOCKS5 Bytestream
|
||||||
|
*/
|
||||||
|
public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) {
|
||||||
|
this.userListeners.put(initiatorJID, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the listener for the given user.
|
||||||
|
*
|
||||||
|
* @param initiatorJID the JID of the user the listener should be removed
|
||||||
|
*/
|
||||||
|
public void removeIncomingBytestreamListener(String initiatorJID) {
|
||||||
|
this.userListeners.remove(initiatorJID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this method to ignore the next incoming SOCKS5 Bytestream request containing the given
|
||||||
|
* session ID. No listeners will be notified for this request and and no error will be returned
|
||||||
|
* to the initiator.
|
||||||
|
* <p>
|
||||||
|
* This method should be used if you are awaiting a SOCKS5 Bytestream request as a reply to
|
||||||
|
* another packet (e.g. file transfer).
|
||||||
|
*
|
||||||
|
* @param sessionID to be ignored
|
||||||
|
*/
|
||||||
|
public void ignoreBytestreamRequestOnce(String sessionID) {
|
||||||
|
this.ignoredBytestreamRequests.add(sessionID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables the SOCKS5 Bytestream manager by removing the SOCKS5 Bytestream feature from the
|
||||||
|
* service discovery, disabling the listener for SOCKS5 Bytestream initiation requests and
|
||||||
|
* resetting its internal state.
|
||||||
|
* <p>
|
||||||
|
* To re-enable the SOCKS5 Bytestream feature invoke {@link #getBytestreamManager(Connection)}.
|
||||||
|
* Using the file transfer API will automatically re-enable the SOCKS5 Bytestream feature.
|
||||||
|
*/
|
||||||
|
public synchronized void disableService() {
|
||||||
|
|
||||||
|
// remove initiation packet listener
|
||||||
|
this.connection.removePacketListener(this.initiationListener);
|
||||||
|
|
||||||
|
// shutdown threads
|
||||||
|
this.initiationListener.shutdown();
|
||||||
|
|
||||||
|
// clear listeners
|
||||||
|
this.allRequestListeners.clear();
|
||||||
|
this.userListeners.clear();
|
||||||
|
|
||||||
|
// reset internal state
|
||||||
|
this.lastWorkingProxy = null;
|
||||||
|
this.proxyBlacklist.clear();
|
||||||
|
this.ignoredBytestreamRequests.clear();
|
||||||
|
|
||||||
|
// remove manager from static managers map
|
||||||
|
managers.remove(this.connection);
|
||||||
|
|
||||||
|
// shutdown local SOCKS5 proxy if there are no more managers for other connections
|
||||||
|
if (managers.size() == 0) {
|
||||||
|
Socks5Proxy.getSocks5Proxy().stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove feature from service discovery
|
||||||
|
ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
|
||||||
|
|
||||||
|
// check if service discovery is not already disposed by connection shutdown
|
||||||
|
if (serviceDiscoveryManager != null) {
|
||||||
|
serviceDiscoveryManager.removeFeature(NAMESPACE);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the timeout to wait for the response to the SOCKS5 Bytestream initialization request.
|
||||||
|
* Default is 10000ms.
|
||||||
|
*
|
||||||
|
* @return the timeout to wait for the response to the SOCKS5 Bytestream initialization request
|
||||||
|
*/
|
||||||
|
public int getTargetResponseTimeout() {
|
||||||
|
if (this.targetResponseTimeout <= 0) {
|
||||||
|
this.targetResponseTimeout = 10000;
|
||||||
|
}
|
||||||
|
return targetResponseTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the timeout to wait for the response to the SOCKS5 Bytestream initialization request.
|
||||||
|
* Default is 10000ms.
|
||||||
|
*
|
||||||
|
* @param targetResponseTimeout the timeout to set
|
||||||
|
*/
|
||||||
|
public void setTargetResponseTimeout(int targetResponseTimeout) {
|
||||||
|
this.targetResponseTimeout = targetResponseTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the timeout for connecting to the SOCKS5 proxy selected by the target. Default is
|
||||||
|
* 10000ms.
|
||||||
|
*
|
||||||
|
* @return the timeout for connecting to the SOCKS5 proxy selected by the target
|
||||||
|
*/
|
||||||
|
public int getProxyConnectionTimeout() {
|
||||||
|
if (this.proxyConnectionTimeout <= 0) {
|
||||||
|
this.proxyConnectionTimeout = 10000;
|
||||||
|
}
|
||||||
|
return proxyConnectionTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the timeout for connecting to the SOCKS5 proxy selected by the target. Default is
|
||||||
|
* 10000ms.
|
||||||
|
*
|
||||||
|
* @param proxyConnectionTimeout the timeout to set
|
||||||
|
*/
|
||||||
|
public void setProxyConnectionTimeout(int proxyConnectionTimeout) {
|
||||||
|
this.proxyConnectionTimeout = proxyConnectionTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns if the prioritization of the last working SOCKS5 proxy on successive SOCKS5
|
||||||
|
* Bytestream connections is enabled. Default is <code>true</code>.
|
||||||
|
*
|
||||||
|
* @return <code>true</code> if prioritization is enabled, <code>false</code> otherwise
|
||||||
|
*/
|
||||||
|
public boolean isProxyPrioritizationEnabled() {
|
||||||
|
return proxyPrioritizationEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable/disable the prioritization of the last working SOCKS5 proxy on successive SOCKS5
|
||||||
|
* Bytestream connections.
|
||||||
|
*
|
||||||
|
* @param proxyPrioritizationEnabled enable/disable the prioritization of the last working
|
||||||
|
* SOCKS5 proxy
|
||||||
|
*/
|
||||||
|
public void setProxyPrioritizationEnabled(boolean proxyPrioritizationEnabled) {
|
||||||
|
this.proxyPrioritizationEnabled = proxyPrioritizationEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establishes a SOCKS5 Bytestream with the given user and returns the Socket to send/receive
|
||||||
|
* data to/from the user.
|
||||||
|
* <p>
|
||||||
|
* Use this method to establish SOCKS5 Bytestreams to users accepting all incoming Socks5
|
||||||
|
* bytestream requests since this method doesn't provide a way to tell the user something about
|
||||||
|
* the data to be sent.
|
||||||
|
* <p>
|
||||||
|
* To establish a SOCKS5 Bytestream after negotiation the kind of data to be sent (e.g. file
|
||||||
|
* transfer) use {@link #establishSession(String, String)}.
|
||||||
|
*
|
||||||
|
* @param targetJID the JID of the user a SOCKS5 Bytestream should be established
|
||||||
|
* @return the Socket to send/receive data to/from the user
|
||||||
|
* @throws XMPPException if the user doesn't support or accept SOCKS5 Bytestreams, if no Socks5
|
||||||
|
* Proxy could be found, if the user couldn't connect to any of the SOCKS5 Proxies
|
||||||
|
* @throws IOException if the bytestream could not be established
|
||||||
|
* @throws InterruptedException if the current thread was interrupted while waiting
|
||||||
|
*/
|
||||||
|
public Socks5BytestreamSession establishSession(String targetJID) throws XMPPException,
|
||||||
|
IOException, InterruptedException {
|
||||||
|
String sessionID = getNextSessionID();
|
||||||
|
return establishSession(targetJID, sessionID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establishes a SOCKS5 Bytestream with the given user using the given session ID and returns
|
||||||
|
* the Socket to send/receive data to/from the user.
|
||||||
|
*
|
||||||
|
* @param targetJID the JID of the user a SOCKS5 Bytestream should be established
|
||||||
|
* @param sessionID the session ID for the SOCKS5 Bytestream request
|
||||||
|
* @return the Socket to send/receive data to/from the user
|
||||||
|
* @throws XMPPException if the user doesn't support or accept SOCKS5 Bytestreams, if no Socks5
|
||||||
|
* Proxy could be found, if the user couldn't connect to any of the SOCKS5 Proxies
|
||||||
|
* @throws IOException if the bytestream could not be established
|
||||||
|
* @throws InterruptedException if the current thread was interrupted while waiting
|
||||||
|
*/
|
||||||
|
public Socks5BytestreamSession establishSession(String targetJID, String sessionID)
|
||||||
|
throws XMPPException, IOException, InterruptedException {
|
||||||
|
|
||||||
|
// check if target supports SOCKS5 Bytestream
|
||||||
|
if (!supportsSocks5(targetJID)) {
|
||||||
|
throw new XMPPException(targetJID + " doesn't support SOCKS5 Bytestream");
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine SOCKS5 proxies from XMPP-server
|
||||||
|
List<String> proxies = determineProxies();
|
||||||
|
|
||||||
|
// determine address and port of each proxy
|
||||||
|
List<StreamHost> streamHosts = determineStreamHostInfos(proxies);
|
||||||
|
|
||||||
|
// compute digest
|
||||||
|
String digest = Socks5Utils.createDigest(sessionID, this.connection.getUser(), targetJID);
|
||||||
|
|
||||||
|
if (streamHosts.isEmpty()) {
|
||||||
|
throw new XMPPException("no SOCKS5 proxies available");
|
||||||
|
}
|
||||||
|
|
||||||
|
// prioritize last working SOCKS5 proxy if exists
|
||||||
|
if (this.proxyPrioritizationEnabled && this.lastWorkingProxy != null) {
|
||||||
|
StreamHost selectedStreamHost = null;
|
||||||
|
for (StreamHost streamHost : streamHosts) {
|
||||||
|
if (streamHost.getJID().equals(this.lastWorkingProxy)) {
|
||||||
|
selectedStreamHost = streamHost;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selectedStreamHost != null) {
|
||||||
|
streamHosts.remove(selectedStreamHost);
|
||||||
|
streamHosts.add(0, selectedStreamHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Socks5Proxy socks5Proxy = Socks5Proxy.getSocks5Proxy();
|
||||||
|
try {
|
||||||
|
|
||||||
|
// add transfer digest to local proxy to make transfer valid
|
||||||
|
socks5Proxy.addTransfer(digest);
|
||||||
|
|
||||||
|
// create initiation packet
|
||||||
|
Bytestream initiation = createBytestreamInitiation(sessionID, targetJID, streamHosts);
|
||||||
|
|
||||||
|
// send initiation packet
|
||||||
|
Packet response = SyncPacketSend.getReply(this.connection, initiation,
|
||||||
|
getTargetResponseTimeout());
|
||||||
|
|
||||||
|
// extract used stream host from response
|
||||||
|
StreamHostUsed streamHostUsed = ((Bytestream) response).getUsedHost();
|
||||||
|
StreamHost usedStreamHost = initiation.getStreamHost(streamHostUsed.getJID());
|
||||||
|
|
||||||
|
if (usedStreamHost == null) {
|
||||||
|
throw new XMPPException("Remote user responded with unknown host");
|
||||||
|
}
|
||||||
|
|
||||||
|
// build SOCKS5 client
|
||||||
|
Socks5Client socks5Client = new Socks5ClientForInitiator(usedStreamHost, digest,
|
||||||
|
this.connection, sessionID, targetJID);
|
||||||
|
|
||||||
|
// establish connection to proxy
|
||||||
|
Socket socket = socks5Client.getSocket(getProxyConnectionTimeout());
|
||||||
|
|
||||||
|
// remember last working SOCKS5 proxy to prioritize it for next request
|
||||||
|
this.lastWorkingProxy = usedStreamHost.getJID();
|
||||||
|
|
||||||
|
// negotiation successful, return the output stream
|
||||||
|
return new Socks5BytestreamSession(socket, usedStreamHost.getJID().equals(
|
||||||
|
this.connection.getUser()));
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (TimeoutException e) {
|
||||||
|
throw new IOException("Timeout while connecting to SOCKS5 proxy");
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
|
||||||
|
// remove transfer digest if output stream is returned or an exception
|
||||||
|
// occurred
|
||||||
|
socks5Proxy.removeTransfer(digest);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns <code>true</code> if the given target JID supports feature SOCKS5 Bytestream.
|
||||||
|
*
|
||||||
|
* @param targetJID the target JID
|
||||||
|
* @return <code>true</code> if the given target JID supports feature SOCKS5 Bytestream
|
||||||
|
* otherwise <code>false</code>
|
||||||
|
* @throws XMPPException if there was an error querying target for supported features
|
||||||
|
*/
|
||||||
|
private boolean supportsSocks5(String targetJID) throws XMPPException {
|
||||||
|
ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
|
||||||
|
DiscoverInfo discoverInfo = serviceDiscoveryManager.discoverInfo(targetJID);
|
||||||
|
return discoverInfo.containsFeature(NAMESPACE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of JIDs of SOCKS5 proxies by querying the XMPP server. The SOCKS5 proxies are
|
||||||
|
* in the same order as returned by the XMPP server.
|
||||||
|
*
|
||||||
|
* @return list of JIDs of SOCKS5 proxies
|
||||||
|
* @throws XMPPException if there was an error querying the XMPP server for SOCKS5 proxies
|
||||||
|
*/
|
||||||
|
private List<String> determineProxies() throws XMPPException {
|
||||||
|
ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
|
||||||
|
|
||||||
|
List<String> proxies = new ArrayList<String>();
|
||||||
|
|
||||||
|
// get all items form XMPP server
|
||||||
|
DiscoverItems discoverItems = serviceDiscoveryManager.discoverItems(this.connection.getServiceName());
|
||||||
|
Iterator<Item> itemIterator = discoverItems.getItems();
|
||||||
|
|
||||||
|
// query all items if they are SOCKS5 proxies
|
||||||
|
while (itemIterator.hasNext()) {
|
||||||
|
Item item = itemIterator.next();
|
||||||
|
|
||||||
|
// skip blacklisted servers
|
||||||
|
if (this.proxyBlacklist.contains(item.getEntityID())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DiscoverInfo proxyInfo;
|
||||||
|
proxyInfo = serviceDiscoveryManager.discoverInfo(item.getEntityID());
|
||||||
|
Iterator<Identity> identities = proxyInfo.getIdentities();
|
||||||
|
|
||||||
|
// item must have category "proxy" and type "bytestream"
|
||||||
|
while (identities.hasNext()) {
|
||||||
|
Identity identity = identities.next();
|
||||||
|
|
||||||
|
if ("proxy".equalsIgnoreCase(identity.getCategory())
|
||||||
|
&& "bytestreams".equalsIgnoreCase(identity.getType())) {
|
||||||
|
proxies.add(item.getEntityID());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* server is not a SOCKS5 proxy, blacklist server to skip next time a Socks5
|
||||||
|
* bytestream should be established
|
||||||
|
*/
|
||||||
|
this.proxyBlacklist.add(item.getEntityID());
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (XMPPException e) {
|
||||||
|
// blacklist errornous server
|
||||||
|
this.proxyBlacklist.add(item.getEntityID());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of stream hosts containing the IP address an the port for the given list of
|
||||||
|
* SOCKS5 proxy JIDs. The order of the returned list is the same as the given list of JIDs
|
||||||
|
* excluding all SOCKS5 proxies who's network settings could not be determined. If a local
|
||||||
|
* SOCKS5 proxy is running it will be the first item in the list returned.
|
||||||
|
*
|
||||||
|
* @param proxies a list of SOCKS5 proxy JIDs
|
||||||
|
* @return a list of stream hosts containing the IP address an the port
|
||||||
|
*/
|
||||||
|
private List<StreamHost> determineStreamHostInfos(List<String> proxies) {
|
||||||
|
List<StreamHost> streamHosts = new ArrayList<StreamHost>();
|
||||||
|
|
||||||
|
// add local proxy on first position if exists
|
||||||
|
List<StreamHost> localProxies = getLocalStreamHost();
|
||||||
|
if (localProxies != null) {
|
||||||
|
streamHosts.addAll(localProxies);
|
||||||
|
}
|
||||||
|
|
||||||
|
// query SOCKS5 proxies for network settings
|
||||||
|
for (String proxy : proxies) {
|
||||||
|
Bytestream streamHostRequest = createStreamHostRequest(proxy);
|
||||||
|
try {
|
||||||
|
Bytestream response = (Bytestream) SyncPacketSend.getReply(this.connection,
|
||||||
|
streamHostRequest);
|
||||||
|
streamHosts.addAll(response.getStreamHosts());
|
||||||
|
}
|
||||||
|
catch (XMPPException e) {
|
||||||
|
// blacklist errornous proxies
|
||||||
|
this.proxyBlacklist.add(proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return streamHosts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a IQ packet to query a SOCKS5 proxy its network settings.
|
||||||
|
*
|
||||||
|
* @param proxy the proxy to query
|
||||||
|
* @return IQ packet to query a SOCKS5 proxy its network settings
|
||||||
|
*/
|
||||||
|
private Bytestream createStreamHostRequest(String proxy) {
|
||||||
|
Bytestream request = new Bytestream();
|
||||||
|
request.setType(IQ.Type.GET);
|
||||||
|
request.setTo(proxy);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the stream host information of the local SOCKS5 proxy containing the IP address and
|
||||||
|
* the port or null if local SOCKS5 proxy is not running.
|
||||||
|
*
|
||||||
|
* @return the stream host information of the local SOCKS5 proxy or null if local SOCKS5 proxy
|
||||||
|
* is not running
|
||||||
|
*/
|
||||||
|
private List<StreamHost> getLocalStreamHost() {
|
||||||
|
|
||||||
|
// get local proxy singleton
|
||||||
|
Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy();
|
||||||
|
|
||||||
|
if (socks5Server.isRunning()) {
|
||||||
|
List<String> addresses = socks5Server.getLocalAddresses();
|
||||||
|
int port = socks5Server.getPort();
|
||||||
|
|
||||||
|
if (addresses.size() >= 1) {
|
||||||
|
List<StreamHost> streamHosts = new ArrayList<StreamHost>();
|
||||||
|
for (String address : addresses) {
|
||||||
|
StreamHost streamHost = new StreamHost(this.connection.getUser(), address);
|
||||||
|
streamHost.setPort(port);
|
||||||
|
streamHosts.add(streamHost);
|
||||||
|
}
|
||||||
|
return streamHosts;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// server is not running or local address could not be determined
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a SOCKS5 Bytestream initialization request packet with the given session ID
|
||||||
|
* containing the given stream hosts for the given target JID.
|
||||||
|
*
|
||||||
|
* @param sessionID the session ID for the SOCKS5 Bytestream
|
||||||
|
* @param targetJID the target JID of SOCKS5 Bytestream request
|
||||||
|
* @param streamHosts a list of SOCKS5 proxies the target should connect to
|
||||||
|
* @return a SOCKS5 Bytestream initialization request packet
|
||||||
|
*/
|
||||||
|
private Bytestream createBytestreamInitiation(String sessionID, String targetJID,
|
||||||
|
List<StreamHost> streamHosts) {
|
||||||
|
Bytestream initiation = new Bytestream(sessionID);
|
||||||
|
|
||||||
|
// add all stream hosts
|
||||||
|
for (StreamHost streamHost : streamHosts) {
|
||||||
|
initiation.addStreamHost(streamHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
initiation.setType(IQ.Type.SET);
|
||||||
|
initiation.setTo(targetJID);
|
||||||
|
|
||||||
|
return initiation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responses to the given packet's sender with a XMPP error that a SOCKS5 Bytestream is not
|
||||||
|
* accepted.
|
||||||
|
*
|
||||||
|
* @param packet Packet that should be answered with a not-acceptable error
|
||||||
|
*/
|
||||||
|
protected void replyRejectPacket(IQ packet) {
|
||||||
|
XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable);
|
||||||
|
IQ errorIQ = IQ.createErrorResponse(packet, xmppError);
|
||||||
|
this.connection.sendPacket(errorIQ);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates the Socks5BytestreamManager by registering the SOCKS5 Bytestream initialization
|
||||||
|
* listener and enabling the SOCKS5 Bytestream feature.
|
||||||
|
*/
|
||||||
|
private void activate() {
|
||||||
|
// register bytestream initiation packet listener
|
||||||
|
this.connection.addPacketListener(this.initiationListener,
|
||||||
|
this.initiationListener.getFilter());
|
||||||
|
|
||||||
|
// enable SOCKS5 feature
|
||||||
|
enableService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the SOCKS5 Bytestream feature to the service discovery.
|
||||||
|
*/
|
||||||
|
private void enableService() {
|
||||||
|
ServiceDiscoveryManager manager = ServiceDiscoveryManager.getInstanceFor(this.connection);
|
||||||
|
if (!manager.includesFeature(NAMESPACE)) {
|
||||||
|
manager.addFeature(NAMESPACE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new unique session ID.
|
||||||
|
*
|
||||||
|
* @return a new unique session ID
|
||||||
|
*/
|
||||||
|
private String getNextSessionID() {
|
||||||
|
StringBuilder buffer = new StringBuilder();
|
||||||
|
buffer.append(SESSION_ID_PREFIX);
|
||||||
|
buffer.append(Math.abs(randomGenerator.nextLong()));
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the XMPP connection.
|
||||||
|
*
|
||||||
|
* @return the XMPP connection
|
||||||
|
*/
|
||||||
|
protected Connection getConnection() {
|
||||||
|
return this.connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link BytestreamListener} that should be informed if a SOCKS5 Bytestream request
|
||||||
|
* from the given initiator JID is received.
|
||||||
|
*
|
||||||
|
* @param initiator the initiator's JID
|
||||||
|
* @return the listener
|
||||||
|
*/
|
||||||
|
protected BytestreamListener getUserListener(String initiator) {
|
||||||
|
return this.userListeners.get(initiator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of {@link BytestreamListener} that are informed if there are no listeners for
|
||||||
|
* a specific initiator.
|
||||||
|
*
|
||||||
|
* @return list of listeners
|
||||||
|
*/
|
||||||
|
protected List<BytestreamListener> getAllRequestListeners() {
|
||||||
|
return this.allRequestListeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of session IDs that should be ignored by the InitialtionListener
|
||||||
|
*
|
||||||
|
* @return list of session IDs
|
||||||
|
*/
|
||||||
|
protected List<String> getIgnoredBytestreamRequests() {
|
||||||
|
return ignoredBytestreamRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,316 @@
|
||||||
|
/**
|
||||||
|
* All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.jivesoftware.smackx.bytestreams.socks5;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
import org.jivesoftware.smack.XMPPException;
|
||||||
|
import org.jivesoftware.smack.packet.IQ;
|
||||||
|
import org.jivesoftware.smack.packet.XMPPError;
|
||||||
|
import org.jivesoftware.smack.util.Cache;
|
||||||
|
import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
|
||||||
|
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
|
||||||
|
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Socks5BytestreamRequest class handles incoming SOCKS5 Bytestream requests.
|
||||||
|
*
|
||||||
|
* @author Henning Staib
|
||||||
|
*/
|
||||||
|
public class Socks5BytestreamRequest implements BytestreamRequest {
|
||||||
|
|
||||||
|
/* lifetime of an Item in the blacklist */
|
||||||
|
private static final long BLACKLIST_LIFETIME = 60 * 1000 * 120;
|
||||||
|
|
||||||
|
/* size of the blacklist */
|
||||||
|
private static final int BLACKLIST_MAX_SIZE = 100;
|
||||||
|
|
||||||
|
/* blacklist of addresses of SOCKS5 proxies */
|
||||||
|
private static final Cache<String, Integer> ADDRESS_BLACKLIST = new Cache<String, Integer>(
|
||||||
|
BLACKLIST_MAX_SIZE, BLACKLIST_LIFETIME);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The number of connection failures it takes for a particular SOCKS5 proxy to be blacklisted.
|
||||||
|
* When a proxy is blacklisted no more connection attempts will be made to it for a period of 2
|
||||||
|
* hours.
|
||||||
|
*/
|
||||||
|
private static int CONNECTION_FAILURE_THRESHOLD = 2;
|
||||||
|
|
||||||
|
/* the bytestream initialization request */
|
||||||
|
private Bytestream bytestreamRequest;
|
||||||
|
|
||||||
|
/* SOCKS5 Bytestream manager containing the XMPP connection and helper methods */
|
||||||
|
private Socks5BytestreamManager manager;
|
||||||
|
|
||||||
|
/* timeout to connect to all SOCKS5 proxies */
|
||||||
|
private int totalConnectTimeout = 10000;
|
||||||
|
|
||||||
|
/* minimum timeout to connect to one SOCKS5 proxy */
|
||||||
|
private int minimumConnectTimeout = 2000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of connection failures it takes for a particular SOCKS5 proxy to be
|
||||||
|
* blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
|
||||||
|
* period of 2 hours. Default is 2.
|
||||||
|
*
|
||||||
|
* @return the number of connection failures it takes for a particular SOCKS5 proxy to be
|
||||||
|
* blacklisted
|
||||||
|
*/
|
||||||
|
public static int getConnectFailureThreshold() {
|
||||||
|
return CONNECTION_FAILURE_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the number of connection failures it takes for a particular SOCKS5 proxy to be
|
||||||
|
* blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
|
||||||
|
* period of 2 hours. Default is 2.
|
||||||
|
* <p>
|
||||||
|
* Setting the connection failure threshold to zero disables the blacklisting.
|
||||||
|
*
|
||||||
|
* @param connectFailureThreshold the number of connection failures it takes for a particular
|
||||||
|
* SOCKS5 proxy to be blacklisted
|
||||||
|
*/
|
||||||
|
public static void setConnectFailureThreshold(int connectFailureThreshold) {
|
||||||
|
CONNECTION_FAILURE_THRESHOLD = connectFailureThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Socks5BytestreamRequest.
|
||||||
|
*
|
||||||
|
* @param manager the SOCKS5 Bytestream manager
|
||||||
|
* @param bytestreamRequest the SOCKS5 Bytestream initialization packet
|
||||||
|
*/
|
||||||
|
protected Socks5BytestreamRequest(Socks5BytestreamManager manager, Bytestream bytestreamRequest) {
|
||||||
|
this.manager = manager;
|
||||||
|
this.bytestreamRequest = bytestreamRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
|
||||||
|
* <p>
|
||||||
|
* When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
|
||||||
|
* by the initiator until a connection is established. This timeout divided by the number of
|
||||||
|
* SOCKS5 proxies determines the timeout for every connection attempt.
|
||||||
|
* <p>
|
||||||
|
* You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
|
||||||
|
* {@link #setMinimumConnectTimeout(int)}.
|
||||||
|
*
|
||||||
|
* @return the maximum timeout to connect to SOCKS5 proxies
|
||||||
|
*/
|
||||||
|
public int getTotalConnectTimeout() {
|
||||||
|
if (this.totalConnectTimeout <= 0) {
|
||||||
|
return 10000;
|
||||||
|
}
|
||||||
|
return this.totalConnectTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
|
||||||
|
* <p>
|
||||||
|
* When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
|
||||||
|
* by the initiator until a connection is established. This timeout divided by the number of
|
||||||
|
* SOCKS5 proxies determines the timeout for every connection attempt.
|
||||||
|
* <p>
|
||||||
|
* You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
|
||||||
|
* {@link #setMinimumConnectTimeout(int)}.
|
||||||
|
*
|
||||||
|
* @param totalConnectTimeout the maximum timeout to connect to SOCKS5 proxies
|
||||||
|
*/
|
||||||
|
public void setTotalConnectTimeout(int totalConnectTimeout) {
|
||||||
|
this.totalConnectTimeout = totalConnectTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
|
||||||
|
* request. Default is 2000ms.
|
||||||
|
*
|
||||||
|
* @return the timeout to connect to one SOCKS5 proxy
|
||||||
|
*/
|
||||||
|
public int getMinimumConnectTimeout() {
|
||||||
|
if (this.minimumConnectTimeout <= 0) {
|
||||||
|
return 2000;
|
||||||
|
}
|
||||||
|
return this.minimumConnectTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
|
||||||
|
* request. Default is 2000ms.
|
||||||
|
*
|
||||||
|
* @param minimumConnectTimeout the timeout to connect to one SOCKS5 proxy
|
||||||
|
*/
|
||||||
|
public void setMinimumConnectTimeout(int minimumConnectTimeout) {
|
||||||
|
this.minimumConnectTimeout = minimumConnectTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the sender of the SOCKS5 Bytestream initialization request.
|
||||||
|
*
|
||||||
|
* @return the sender of the SOCKS5 Bytestream initialization request.
|
||||||
|
*/
|
||||||
|
public String getFrom() {
|
||||||
|
return this.bytestreamRequest.getFrom();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the session ID of the SOCKS5 Bytestream initialization request.
|
||||||
|
*
|
||||||
|
* @return the session ID of the SOCKS5 Bytestream initialization request.
|
||||||
|
*/
|
||||||
|
public String getSessionID() {
|
||||||
|
return this.bytestreamRequest.getSessionID();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts the SOCKS5 Bytestream initialization request and returns the socket to send/receive
|
||||||
|
* data.
|
||||||
|
* <p>
|
||||||
|
* Before accepting the SOCKS5 Bytestream request you can set timeouts by invoking
|
||||||
|
* {@link #setTotalConnectTimeout(int)} and {@link #setMinimumConnectTimeout(int)}.
|
||||||
|
*
|
||||||
|
* @return the socket to send/receive data
|
||||||
|
* @throws XMPPException if connection to all SOCKS5 proxies failed or if stream is invalid.
|
||||||
|
* @throws InterruptedException if the current thread was interrupted while waiting
|
||||||
|
*/
|
||||||
|
public Socks5BytestreamSession accept() throws XMPPException, InterruptedException {
|
||||||
|
Collection<StreamHost> streamHosts = this.bytestreamRequest.getStreamHosts();
|
||||||
|
|
||||||
|
// throw exceptions if request contains no stream hosts
|
||||||
|
if (streamHosts.size() == 0) {
|
||||||
|
cancelRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamHost selectedHost = null;
|
||||||
|
Socket socket = null;
|
||||||
|
|
||||||
|
String digest = Socks5Utils.createDigest(this.bytestreamRequest.getSessionID(),
|
||||||
|
this.bytestreamRequest.getFrom(), this.manager.getConnection().getUser());
|
||||||
|
|
||||||
|
/*
|
||||||
|
* determine timeout for each connection attempt; each SOCKS5 proxy has the same amount of
|
||||||
|
* time so that the first does not consume the whole timeout
|
||||||
|
*/
|
||||||
|
int timeout = Math.max(getTotalConnectTimeout() / streamHosts.size(),
|
||||||
|
getMinimumConnectTimeout());
|
||||||
|
|
||||||
|
for (StreamHost streamHost : streamHosts) {
|
||||||
|
String address = streamHost.getAddress() + ":" + streamHost.getPort();
|
||||||
|
|
||||||
|
// check to see if this address has been blacklisted
|
||||||
|
int failures = getConnectionFailures(address);
|
||||||
|
if (CONNECTION_FAILURE_THRESHOLD > 0 && failures >= CONNECTION_FAILURE_THRESHOLD) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// establish socket
|
||||||
|
try {
|
||||||
|
|
||||||
|
// build SOCKS5 client
|
||||||
|
final Socks5Client socks5Client = new Socks5Client(streamHost, digest);
|
||||||
|
|
||||||
|
// connect to SOCKS5 proxy with a timeout
|
||||||
|
socket = socks5Client.getSocket(timeout);
|
||||||
|
|
||||||
|
// set selected host
|
||||||
|
selectedHost = streamHost;
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (TimeoutException e) {
|
||||||
|
incrementConnectionFailures(address);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
incrementConnectionFailures(address);
|
||||||
|
}
|
||||||
|
catch (XMPPException e) {
|
||||||
|
incrementConnectionFailures(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// throw exception if connecting to all SOCKS5 proxies failed
|
||||||
|
if (selectedHost == null || socket == null) {
|
||||||
|
cancelRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
// send used-host confirmation
|
||||||
|
Bytestream response = createUsedHostResponse(selectedHost);
|
||||||
|
this.manager.getConnection().sendPacket(response);
|
||||||
|
|
||||||
|
return new Socks5BytestreamSession(socket, selectedHost.getJID().equals(
|
||||||
|
this.bytestreamRequest.getFrom()));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rejects the SOCKS5 Bytestream request by sending a reject error to the initiator.
|
||||||
|
*/
|
||||||
|
public void reject() {
|
||||||
|
this.manager.replyRejectPacket(this.bytestreamRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the SOCKS5 Bytestream request by sending an error to the initiator and building a
|
||||||
|
* XMPP exception.
|
||||||
|
*
|
||||||
|
* @throws XMPPException XMPP exception containing the XMPP error
|
||||||
|
*/
|
||||||
|
private void cancelRequest() throws XMPPException {
|
||||||
|
String errorMessage = "Could not establish socket with any provided host";
|
||||||
|
XMPPError error = new XMPPError(XMPPError.Condition.item_not_found, errorMessage);
|
||||||
|
IQ errorIQ = IQ.createErrorResponse(this.bytestreamRequest, error);
|
||||||
|
this.manager.getConnection().sendPacket(errorIQ);
|
||||||
|
throw new XMPPException(errorMessage, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the response to the SOCKS5 Bytestream request containing the SOCKS5 proxy used.
|
||||||
|
*
|
||||||
|
* @param selectedHost the used SOCKS5 proxy
|
||||||
|
* @return the response to the SOCKS5 Bytestream request
|
||||||
|
*/
|
||||||
|
private Bytestream createUsedHostResponse(StreamHost selectedHost) {
|
||||||
|
Bytestream response = new Bytestream(this.bytestreamRequest.getSessionID());
|
||||||
|
response.setTo(this.bytestreamRequest.getFrom());
|
||||||
|
response.setType(IQ.Type.RESULT);
|
||||||
|
response.setPacketID(this.bytestreamRequest.getPacketID());
|
||||||
|
response.setUsedHost(selectedHost.getJID());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments the connection failure counter by one for the given address.
|
||||||
|
*
|
||||||
|
* @param address the address the connection failure counter should be increased
|
||||||
|
*/
|
||||||
|
private void incrementConnectionFailures(String address) {
|
||||||
|
Integer count = ADDRESS_BLACKLIST.get(address);
|
||||||
|
ADDRESS_BLACKLIST.put(address, count == null ? 1 : count + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns how often the connection to the given address failed.
|
||||||
|
*
|
||||||
|
* @param address the address
|
||||||
|
* @return number of connection failures
|
||||||
|
*/
|
||||||
|
private int getConnectionFailures(String address) {
|
||||||
|
Integer count = ADDRESS_BLACKLIST.get(address);
|
||||||
|
return count != null ? count : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package org.jivesoftware.smackx.bytestreams.socks5;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.SocketException;
|
||||||
|
|
||||||
|
import org.jivesoftware.smackx.bytestreams.BytestreamSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Socks5BytestreamSession class represents a SOCKS5 Bytestream session.
|
||||||
|
*
|
||||||
|
* @author Henning Staib
|
||||||
|
*/
|
||||||
|
public class Socks5BytestreamSession implements BytestreamSession {
|
||||||
|
|
||||||
|
/* the underlying socket of the SOCKS5 Bytestream */
|
||||||
|
private final Socket socket;
|
||||||
|
|
||||||
|
/* flag to indicate if this session is a direct or mediated connection */
|
||||||
|
private final boolean isDirect;
|
||||||
|
|
||||||
|
protected Socks5BytestreamSession(Socket socket, boolean isDirect) {
|
||||||
|
this.socket = socket;
|
||||||
|
this.isDirect = isDirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns <code>true</code> if the session is established through a direct connection between
|
||||||
|
* the initiator and target, <code>false</code> if the session is mediated over a SOCKS proxy.
|
||||||
|
*
|
||||||
|
* @return <code>true</code> if session is a direct connection, <code>false</code> if session is
|
||||||
|
* mediated over a SOCKS5 proxy
|
||||||
|
*/
|
||||||
|
public boolean isDirect() {
|
||||||
|
return this.isDirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns <code>true</code> if the session is mediated over a SOCKS proxy, <code>false</code>
|
||||||
|
* if this session is established through a direct connection between the initiator and target.
|
||||||
|
*
|
||||||
|
* @return <code>true</code> if session is mediated over a SOCKS5 proxy, <code>false</code> if
|
||||||
|
* session is a direct connection
|
||||||
|
*/
|
||||||
|
public boolean isMediated() {
|
||||||
|
return !this.isDirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getInputStream() throws IOException {
|
||||||
|
return this.socket.getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
public OutputStream getOutputStream() throws IOException {
|
||||||
|
return this.socket.getOutputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getReadTimeout() throws IOException {
|
||||||
|
try {
|
||||||
|
return this.socket.getSoTimeout();
|
||||||
|
}
|
||||||
|
catch (SocketException e) {
|
||||||
|
throw new IOException("Error on underlying Socket");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReadTimeout(int timeout) throws IOException {
|
||||||
|
try {
|
||||||
|
this.socket.setSoTimeout(timeout);
|
||||||
|
}
|
||||||
|
catch (SocketException e) {
|
||||||
|
throw new IOException("Error on underlying Socket");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() throws IOException {
|
||||||
|
this.socket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,204 @@
|
||||||
|
/**
|
||||||
|
* All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.jivesoftware.smackx.bytestreams.socks5;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.SocketAddress;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.FutureTask;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
import org.jivesoftware.smack.XMPPException;
|
||||||
|
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SOCKS5 client class handles establishing a connection to a SOCKS5 proxy. Connecting to a
|
||||||
|
* SOCKS5 proxy requires authentication. This implementation only supports the no-authentication
|
||||||
|
* authentication method.
|
||||||
|
*
|
||||||
|
* @author Henning Staib
|
||||||
|
*/
|
||||||
|
class Socks5Client {
|
||||||
|
|
||||||
|
/* stream host containing network settings and name of the SOCKS5 proxy */
|
||||||
|
protected StreamHost streamHost;
|
||||||
|
|
||||||
|
/* SHA-1 digest identifying the SOCKS5 stream */
|
||||||
|
protected String digest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for a SOCKS5 client.
|
||||||
|
*
|
||||||
|
* @param streamHost containing network settings of the SOCKS5 proxy
|
||||||
|
* @param digest identifying the SOCKS5 Bytestream
|
||||||
|
*/
|
||||||
|
public Socks5Client(StreamHost streamHost, String digest) {
|
||||||
|
this.streamHost = streamHost;
|
||||||
|
this.digest = digest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the initialized socket that can be used to transfer data between peers via the SOCKS5
|
||||||
|
* proxy.
|
||||||
|
*
|
||||||
|
* @param timeout timeout to connect to SOCKS5 proxy in milliseconds
|
||||||
|
* @return socket the initialized socket
|
||||||
|
* @throws IOException if initializing the socket failed due to a network error
|
||||||
|
* @throws XMPPException if establishing connection to SOCKS5 proxy failed
|
||||||
|
* @throws TimeoutException if connecting to SOCKS5 proxy timed out
|
||||||
|
* @throws InterruptedException if the current thread was interrupted while waiting
|
||||||
|
*/
|
||||||
|
public Socket getSocket(int timeout) throws IOException, XMPPException, InterruptedException,
|
||||||
|
TimeoutException {
|
||||||
|
|
||||||
|
// wrap connecting in future for timeout
|
||||||
|
FutureTask<Socket> futureTask = new FutureTask<Socket>(new Callable<Socket>() {
|
||||||
|
|
||||||
|
public Socket call() throws Exception {
|
||||||
|
|
||||||
|
// initialize socket
|
||||||
|
Socket socket = new Socket();
|
||||||
|
SocketAddress socketAddress = new InetSocketAddress(streamHost.getAddress(),
|
||||||
|
streamHost.getPort());
|
||||||
|
socket.connect(socketAddress);
|
||||||
|
|
||||||
|
// initialize connection to SOCKS5 proxy
|
||||||
|
if (!establish(socket)) {
|
||||||
|
|
||||||
|
// initialization failed, close socket
|
||||||
|
socket.close();
|
||||||
|
throw new XMPPException("establishing connection to SOCKS5 proxy failed");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
Thread executor = new Thread(futureTask);
|
||||||
|
executor.start();
|
||||||
|
|
||||||
|
// get connection to initiator with timeout
|
||||||
|
try {
|
||||||
|
return futureTask.get(timeout, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
catch (ExecutionException e) {
|
||||||
|
Throwable cause = e.getCause();
|
||||||
|
if (cause != null) {
|
||||||
|
// case exceptions to comply with method signature
|
||||||
|
if (cause instanceof IOException) {
|
||||||
|
throw (IOException) cause;
|
||||||
|
}
|
||||||
|
if (cause instanceof XMPPException) {
|
||||||
|
throw (XMPPException) cause;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// throw generic IO exception if unexpected exception was thrown
|
||||||
|
throw new IOException("Error while connection to SOCKS5 proxy");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the connection to the SOCKS5 proxy by negotiating authentication method and
|
||||||
|
* requesting a stream for the given digest. Currently only the no-authentication method is
|
||||||
|
* supported by the Socks5Client.
|
||||||
|
* <p>
|
||||||
|
* Returns <code>true</code> if a stream could be established, otherwise <code>false</code>. If
|
||||||
|
* <code>false</code> is returned the given Socket should be closed.
|
||||||
|
*
|
||||||
|
* @param socket connected to a SOCKS5 proxy
|
||||||
|
* @return <code>true</code> if if a stream could be established, otherwise <code>false</code>.
|
||||||
|
* If <code>false</code> is returned the given Socket should be closed.
|
||||||
|
* @throws IOException if a network error occurred
|
||||||
|
*/
|
||||||
|
protected boolean establish(Socket socket) throws IOException {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* use DataInputStream/DataOutpuStream to assure read and write is completed in a single
|
||||||
|
* statement
|
||||||
|
*/
|
||||||
|
DataInputStream in = new DataInputStream(socket.getInputStream());
|
||||||
|
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
|
||||||
|
|
||||||
|
// authentication negotiation
|
||||||
|
byte[] cmd = new byte[3];
|
||||||
|
|
||||||
|
cmd[0] = (byte) 0x05; // protocol version 5
|
||||||
|
cmd[1] = (byte) 0x01; // number of authentication methods supported
|
||||||
|
cmd[2] = (byte) 0x00; // authentication method: no-authentication required
|
||||||
|
|
||||||
|
out.write(cmd);
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
byte[] response = new byte[2];
|
||||||
|
in.readFully(response);
|
||||||
|
|
||||||
|
// check if server responded with correct version and no-authentication method
|
||||||
|
if (response[0] != (byte) 0x05 || response[1] != (byte) 0x00) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// request SOCKS5 connection with given address/digest
|
||||||
|
byte[] connectionRequest = createSocks5ConnectRequest();
|
||||||
|
out.write(connectionRequest);
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
// receive response
|
||||||
|
byte[] connectionResponse;
|
||||||
|
try {
|
||||||
|
connectionResponse = Socks5Utils.receiveSocks5Message(in);
|
||||||
|
}
|
||||||
|
catch (XMPPException e) {
|
||||||
|
return false; // server answered in an unsupported way
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify response
|
||||||
|
connectionRequest[1] = (byte) 0x00; // set expected return status to 0
|
||||||
|
return Arrays.equals(connectionRequest, connectionResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a SOCKS5 connection request message. It contains the command "connect", the address
|
||||||
|
* type "domain" and the digest as address.
|
||||||
|
* <p>
|
||||||
|
* (see <a href="http://tools.ietf.org/html/rfc1928">RFC1928</a>)
|
||||||
|
*
|
||||||
|
* @return SOCKS5 connection request message
|
||||||
|
*/
|
||||||
|
private byte[] createSocks5ConnectRequest() {
|
||||||
|
byte addr[] = this.digest.getBytes();
|
||||||
|
|
||||||
|
byte[] data = new byte[7 + addr.length];
|
||||||
|
data[0] = (byte) 0x05; // version (SOCKS5)
|
||||||
|
data[1] = (byte) 0x01; // command (1 - connect)
|
||||||
|
data[2] = (byte) 0x00; // reserved byte (always 0)
|
||||||
|
data[3] = (byte) 0x03; // address type (3 - domain name)
|
||||||
|
data[4] = (byte) addr.length; // address length
|
||||||
|
System.arraycopy(addr, 0, data, 5, addr.length); // address
|
||||||
|
data[data.length - 2] = (byte) 0; // address port (2 bytes always 0)
|
||||||
|
data[data.length - 1] = (byte) 0;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,423 @@
|
||||||
|
/**
|
||||||
|
* All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.jivesoftware.smackx.bytestreams.socks5;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import org.jivesoftware.smack.SmackConfiguration;
|
||||||
|
import org.jivesoftware.smack.XMPPException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Socks5Proxy class represents a local SOCKS5 proxy server. It can be enabled/disabled by
|
||||||
|
* setting the <code>localSocks5ProxyEnabled</code> flag in the <code>smack-config.xml</code> or by
|
||||||
|
* invoking {@link SmackConfiguration#setLocalSocks5ProxyEnabled(boolean)}. The proxy is enabled by
|
||||||
|
* default.
|
||||||
|
* <p>
|
||||||
|
* The port of the local SOCKS5 proxy can be configured by setting <code>localSocks5ProxyPort</code>
|
||||||
|
* in the <code>smack-config.xml</code> or by invoking
|
||||||
|
* {@link SmackConfiguration#setLocalSocks5ProxyPort(int)}. Default port is 7777. If you set the
|
||||||
|
* port to a negative value Smack tries to the absolute value and all following until it finds an
|
||||||
|
* open port.
|
||||||
|
* <p>
|
||||||
|
* If your application is running on a machine with multiple network interfaces or if you want to
|
||||||
|
* provide your public address in case you are behind a NAT router, invoke
|
||||||
|
* {@link #addLocalAddress(String)} or {@link #replaceLocalAddresses(List)} to modify the list of
|
||||||
|
* local network addresses used for outgoing SOCKS5 Bytestream requests.
|
||||||
|
* <p>
|
||||||
|
* The local SOCKS5 proxy server refuses all connections except the ones that are explicitly allowed
|
||||||
|
* in the process of establishing a SOCKS5 Bytestream (
|
||||||
|
* {@link Socks5BytestreamManager#establishSession(String)}).
|
||||||
|
* <p>
|
||||||
|
* This Implementation has the following limitations:
|
||||||
|
* <ul>
|
||||||
|
* <li>only supports the no-authentication authentication method</li>
|
||||||
|
* <li>only supports the <code>connect</code> command and will not answer correctly to other
|
||||||
|
* commands</li>
|
||||||
|
* <li>only supports requests with the domain address type and will not correctly answer to requests
|
||||||
|
* with other address types</li>
|
||||||
|
* </ul>
|
||||||
|
* (see <a href="http://tools.ietf.org/html/rfc1928">RFC 1928</a>)
|
||||||
|
*
|
||||||
|
* @author Henning Staib
|
||||||
|
*/
|
||||||
|
public class Socks5Proxy {
|
||||||
|
|
||||||
|
/* SOCKS5 proxy singleton */
|
||||||
|
private static Socks5Proxy socks5Server;
|
||||||
|
|
||||||
|
/* reusable implementation of a SOCKS5 proxy server process */
|
||||||
|
private Socks5ServerProcess serverProcess;
|
||||||
|
|
||||||
|
/* thread running the SOCKS5 server process */
|
||||||
|
private Thread serverThread;
|
||||||
|
|
||||||
|
/* server socket to accept SOCKS5 connections */
|
||||||
|
private ServerSocket serverSocket;
|
||||||
|
|
||||||
|
/* assigns a connection to a digest */
|
||||||
|
private final Map<String, Socket> connectionMap = new ConcurrentHashMap<String, Socket>();
|
||||||
|
|
||||||
|
/* list of digests connections should be stored */
|
||||||
|
private final List<String> allowedConnections = Collections.synchronizedList(new LinkedList<String>());
|
||||||
|
|
||||||
|
private final Set<String> localAddresses = Collections.synchronizedSet(new LinkedHashSet<String>());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor.
|
||||||
|
*/
|
||||||
|
private Socks5Proxy() {
|
||||||
|
this.serverProcess = new Socks5ServerProcess();
|
||||||
|
|
||||||
|
// add default local address
|
||||||
|
try {
|
||||||
|
this.localAddresses.add(InetAddress.getLocalHost().getHostAddress());
|
||||||
|
}
|
||||||
|
catch (UnknownHostException e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the local SOCKS5 proxy server.
|
||||||
|
*
|
||||||
|
* @return the local SOCKS5 proxy server
|
||||||
|
*/
|
||||||
|
public static synchronized Socks5Proxy getSocks5Proxy() {
|
||||||
|
if (socks5Server == null) {
|
||||||
|
socks5Server = new Socks5Proxy();
|
||||||
|
}
|
||||||
|
if (SmackConfiguration.isLocalSocks5ProxyEnabled()) {
|
||||||
|
socks5Server.start();
|
||||||
|
}
|
||||||
|
return socks5Server;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the local SOCKS5 proxy server. If it is already running, this method does nothing.
|
||||||
|
*/
|
||||||
|
public synchronized void start() {
|
||||||
|
if (isRunning()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (SmackConfiguration.getLocalSocks5ProxyPort() < 0) {
|
||||||
|
int port = Math.abs(SmackConfiguration.getLocalSocks5ProxyPort());
|
||||||
|
for (int i = 0; i < 65535 - port; i++) {
|
||||||
|
try {
|
||||||
|
this.serverSocket = new ServerSocket(port + i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
// port is used, try next one
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.serverSocket = new ServerSocket(SmackConfiguration.getLocalSocks5ProxyPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.serverSocket != null) {
|
||||||
|
this.serverThread = new Thread(this.serverProcess);
|
||||||
|
this.serverThread.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
// couldn't setup server
|
||||||
|
System.err.println("couldn't setup local SOCKS5 proxy on port "
|
||||||
|
+ SmackConfiguration.getLocalSocks5ProxyPort() + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the local SOCKS5 proxy server. If it is not running this method does nothing.
|
||||||
|
*/
|
||||||
|
public synchronized void stop() {
|
||||||
|
if (!isRunning()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.serverSocket.close();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.serverThread != null && this.serverThread.isAlive()) {
|
||||||
|
try {
|
||||||
|
this.serverThread.interrupt();
|
||||||
|
this.serverThread.join();
|
||||||
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.serverThread = null;
|
||||||
|
this.serverSocket = null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the given address to the list of local network addresses.
|
||||||
|
* <p>
|
||||||
|
* Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request.
|
||||||
|
* This may be necessary if your application is running on a machine with multiple network
|
||||||
|
* interfaces or if you want to provide your public address in case you are behind a NAT router.
|
||||||
|
* <p>
|
||||||
|
* The order of the addresses used is determined by the order you add addresses.
|
||||||
|
* <p>
|
||||||
|
* Note that the list of addresses initially contains the address returned by
|
||||||
|
* <code>InetAddress.getLocalHost().getHostAddress()</code>. You can replace the list of
|
||||||
|
* addresses by invoking {@link #replaceLocalAddresses(List)}.
|
||||||
|
*
|
||||||
|
* @param address the local network address to add
|
||||||
|
*/
|
||||||
|
public void addLocalAddress(String address) {
|
||||||
|
if (address == null) {
|
||||||
|
throw new IllegalArgumentException("address may not be null");
|
||||||
|
}
|
||||||
|
this.localAddresses.add(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the given address from the list of local network addresses. This address will then no
|
||||||
|
* longer be used of outgoing SOCKS5 Bytestream requests.
|
||||||
|
*
|
||||||
|
* @param address the local network address to remove
|
||||||
|
*/
|
||||||
|
public void removeLocalAddress(String address) {
|
||||||
|
this.localAddresses.remove(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an unmodifiable list of the local network addresses that will be used for streamhost
|
||||||
|
* candidates of outgoing SOCKS5 Bytestream requests.
|
||||||
|
*
|
||||||
|
* @return unmodifiable list of the local network addresses
|
||||||
|
*/
|
||||||
|
public List<String> getLocalAddresses() {
|
||||||
|
return Collections.unmodifiableList(new ArrayList<String>(this.localAddresses));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the list of local network addresses.
|
||||||
|
* <p>
|
||||||
|
* Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request and
|
||||||
|
* want to define their order. This may be necessary if your application is running on a machine
|
||||||
|
* with multiple network interfaces or if you want to provide your public address in case you
|
||||||
|
* are behind a NAT router.
|
||||||
|
*
|
||||||
|
* @param addresses the new list of local network addresses
|
||||||
|
*/
|
||||||
|
public void replaceLocalAddresses(List<String> addresses) {
|
||||||
|
if (addresses == null) {
|
||||||
|
throw new IllegalArgumentException("list must not be null");
|
||||||
|
}
|
||||||
|
this.localAddresses.clear();
|
||||||
|
this.localAddresses.addAll(addresses);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the port of the local SOCKS5 proxy server. If it is not running -1 will be returned.
|
||||||
|
*
|
||||||
|
* @return the port of the local SOCKS5 proxy server or -1 if proxy is not running
|
||||||
|
*/
|
||||||
|
public int getPort() {
|
||||||
|
if (!isRunning()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return this.serverSocket.getLocalPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the socket for the given digest. A socket will be returned if the given digest has
|
||||||
|
* been in the list of allowed transfers (see {@link #addTransfer(String)}) while the peer
|
||||||
|
* connected to the SOCKS5 proxy.
|
||||||
|
*
|
||||||
|
* @param digest identifying the connection
|
||||||
|
* @return socket or null if there is no socket for the given digest
|
||||||
|
*/
|
||||||
|
protected Socket getSocket(String digest) {
|
||||||
|
return this.connectionMap.get(digest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the given digest to the list of allowed transfers. Only connections for allowed transfers
|
||||||
|
* are stored and can be retrieved by invoking {@link #getSocket(String)}. All connections to
|
||||||
|
* the local SOCKS5 proxy that don't contain an allowed digest are discarded.
|
||||||
|
*
|
||||||
|
* @param digest to be added to the list of allowed transfers
|
||||||
|
*/
|
||||||
|
protected void addTransfer(String digest) {
|
||||||
|
this.allowedConnections.add(digest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the given digest from the list of allowed transfers. After invoking this method
|
||||||
|
* already stored connections with the given digest will be removed.
|
||||||
|
* <p>
|
||||||
|
* The digest should be removed after establishing the SOCKS5 Bytestream is finished, an error
|
||||||
|
* occurred while establishing the connection or if the connection is not allowed anymore.
|
||||||
|
*
|
||||||
|
* @param digest to be removed from the list of allowed transfers
|
||||||
|
*/
|
||||||
|
protected void removeTransfer(String digest) {
|
||||||
|
this.allowedConnections.remove(digest);
|
||||||
|
this.connectionMap.remove(digest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns <code>true</code> if the local SOCKS5 proxy server is running, otherwise
|
||||||
|
* <code>false</code>.
|
||||||
|
*
|
||||||
|
* @return <code>true</code> if the local SOCKS5 proxy server is running, otherwise
|
||||||
|
* <code>false</code>
|
||||||
|
*/
|
||||||
|
public boolean isRunning() {
|
||||||
|
return this.serverSocket != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of a simplified SOCKS5 proxy server.
|
||||||
|
*/
|
||||||
|
private class Socks5ServerProcess implements Runnable {
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
while (true) {
|
||||||
|
Socket socket = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (Socks5Proxy.this.serverSocket.isClosed()
|
||||||
|
|| Thread.currentThread().isInterrupted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// accept connection
|
||||||
|
socket = Socks5Proxy.this.serverSocket.accept();
|
||||||
|
|
||||||
|
// initialize connection
|
||||||
|
establishConnection(socket);
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (SocketException e) {
|
||||||
|
/*
|
||||||
|
* do nothing, if caused by closing the server socket, thread will terminate in
|
||||||
|
* next loop
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
try {
|
||||||
|
if (socket != null) {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e1) {
|
||||||
|
/* do nothing */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Negotiates a SOCKS5 connection and stores it on success.
|
||||||
|
*
|
||||||
|
* @param socket connection to the client
|
||||||
|
* @throws XMPPException if client requests a connection in an unsupported way
|
||||||
|
* @throws IOException if a network error occurred
|
||||||
|
*/
|
||||||
|
private void establishConnection(Socket socket) throws XMPPException, IOException {
|
||||||
|
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
|
||||||
|
DataInputStream in = new DataInputStream(socket.getInputStream());
|
||||||
|
|
||||||
|
// first byte is version should be 5
|
||||||
|
int b = in.read();
|
||||||
|
if (b != 5) {
|
||||||
|
throw new XMPPException("Only SOCKS5 supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
// second byte number of authentication methods supported
|
||||||
|
b = in.read();
|
||||||
|
|
||||||
|
// read list of supported authentication methods
|
||||||
|
byte[] auth = new byte[b];
|
||||||
|
in.readFully(auth);
|
||||||
|
|
||||||
|
byte[] authMethodSelectionResponse = new byte[2];
|
||||||
|
authMethodSelectionResponse[0] = (byte) 0x05; // protocol version
|
||||||
|
|
||||||
|
// only authentication method 0, no authentication, supported
|
||||||
|
boolean noAuthMethodFound = false;
|
||||||
|
for (int i = 0; i < auth.length; i++) {
|
||||||
|
if (auth[i] == (byte) 0x00) {
|
||||||
|
noAuthMethodFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noAuthMethodFound) {
|
||||||
|
authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods
|
||||||
|
out.write(authMethodSelectionResponse);
|
||||||
|
out.flush();
|
||||||
|
throw new XMPPException("Authentication method not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method
|
||||||
|
out.write(authMethodSelectionResponse);
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
// receive connection request
|
||||||
|
byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in);
|
||||||
|
|
||||||
|
// extract digest
|
||||||
|
String responseDigest = new String(connectionRequest, 5, connectionRequest[4]);
|
||||||
|
|
||||||
|
// return error if digest is not allowed
|
||||||
|
if (!Socks5Proxy.this.allowedConnections.contains(responseDigest)) {
|
||||||
|
connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused)
|
||||||
|
out.write(connectionRequest);
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
throw new XMPPException("Connection is not allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionRequest[1] = (byte) 0x00; // set return status to 0 (success)
|
||||||
|
out.write(connectionRequest);
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
// store connection
|
||||||
|
Socks5Proxy.this.connectionMap.put(responseDigest, socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/**
|
||||||
|
* All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.jivesoftware.smackx.bytestreams.socks5;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.jivesoftware.smack.XMPPException;
|
||||||
|
import org.jivesoftware.smack.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of utility methods for SOcKS5 messages.
|
||||||
|
*
|
||||||
|
* @author Henning Staib
|
||||||
|
*/
|
||||||
|
class Socks5Utils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a SHA-1 digest of the given parameters as specified in <a
|
||||||
|
* href="http://xmpp.org/extensions/xep-0065.html#impl-socks5">XEP-0065</a>.
|
||||||
|
*
|
||||||
|
* @param sessionID for the SOCKS5 Bytestream
|
||||||
|
* @param initiatorJID JID of the initiator of a SOCKS5 Bytestream
|
||||||
|
* @param targetJID JID of the target of a SOCKS5 Bytestream
|
||||||
|
* @return SHA-1 digest of the given parameters
|
||||||
|
*/
|
||||||
|
public static String createDigest(String sessionID, String initiatorJID, String targetJID) {
|
||||||
|
StringBuilder b = new StringBuilder();
|
||||||
|
b.append(sessionID).append(initiatorJID).append(targetJID);
|
||||||
|
return StringUtils.hash(b.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a SOCKS5 message from the given InputStream. The message can either be a SOCKS5 request
|
||||||
|
* message or a SOCKS5 response message.
|
||||||
|
* <p>
|
||||||
|
* (see <a href="http://tools.ietf.org/html/rfc1928">RFC1928</a>)
|
||||||
|
*
|
||||||
|
* @param in the DataInputStream to read the message from
|
||||||
|
* @return the SOCKS5 message
|
||||||
|
* @throws IOException if a network error occurred
|
||||||
|
* @throws XMPPException if the SOCKS5 message contains an unsupported address type
|
||||||
|
*/
|
||||||
|
public static byte[] receiveSocks5Message(DataInputStream in) throws IOException, XMPPException {
|
||||||
|
byte[] header = new byte[5];
|
||||||
|
in.readFully(header, 0, 5);
|
||||||
|
|
||||||
|
if (header[3] != (byte) 0x03) {
|
||||||
|
throw new XMPPException("Unsupported SOCKS5 address type");
|
||||||
|
}
|
||||||
|
|
||||||
|
int addressLength = header[4];
|
||||||
|
|
||||||
|
byte[] response = new byte[7 + addressLength];
|
||||||
|
System.arraycopy(header, 0, response, 0, header.length);
|
||||||
|
|
||||||
|
in.readFully(response, header.length, addressLength + 2);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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");
|
* 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 not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
|
@ -17,16 +11,19 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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.IQ;
|
||||||
import org.jivesoftware.smack.packet.PacketExtension;
|
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
|
* @author Alexander Wenckus
|
||||||
*/
|
*/
|
||||||
public class Bytestream extends IQ {
|
public class Bytestream extends IQ {
|
||||||
|
@ -50,7 +47,7 @@ public class Bytestream extends IQ {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A constructor where the session ID can be specified.
|
* A constructor where the session ID can be specified.
|
||||||
*
|
*
|
||||||
* @param SID The session ID related to the negotiation.
|
* @param SID The session ID related to the negotiation.
|
||||||
* @see #setSessionID(String)
|
* @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
|
* Set the session ID related to the bytestream. The session ID is a unique identifier used to
|
||||||
* identifier used to differentiate between stream negotations.
|
* differentiate between stream negotiations.
|
||||||
*
|
*
|
||||||
* @param sessionID the unique session ID that identifies the transfer.
|
* @param sessionID the unique session ID that identifies the transfer.
|
||||||
*/
|
*/
|
||||||
public void setSessionID(final String sessionID) {
|
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.
|
* Returns the session ID related to the bytestream negotiation.
|
||||||
*
|
*
|
||||||
* @return Returns the session ID related to the Byte Stream negotiation.
|
* @return Returns the session ID related to the bytestream negotiation.
|
||||||
* @see #setSessionID(String)
|
* @see #setSessionID(String)
|
||||||
*/
|
*/
|
||||||
public String getSessionID() {
|
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
|
* Set the transport mode. This should be put in the initiation of the interaction.
|
||||||
* interaction.
|
*
|
||||||
*
|
|
||||||
* @param mode the transport mode, either UDP or TCP
|
* @param mode the transport mode, either UDP or TCP
|
||||||
* @see Mode
|
* @see Mode
|
||||||
*/
|
*/
|
||||||
|
@ -92,7 +88,7 @@ public class Bytestream extends IQ {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the transport mode.
|
* Returns the transport mode.
|
||||||
*
|
*
|
||||||
* @return Returns the transport mode.
|
* @return Returns the transport mode.
|
||||||
* @see #setMode(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
|
* Adds a potential stream host that the remote user can connect to to receive the file.
|
||||||
* receive the file.
|
*
|
||||||
*
|
* @param JID The JID of the stream host.
|
||||||
* @param JID The jabber ID of the stream host.
|
|
||||||
* @param address The internet address of the stream host.
|
* @param address The internet address of the stream host.
|
||||||
* @return The added 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
|
* Adds a potential stream host that the remote user can connect to to receive the file.
|
||||||
* receive the file.
|
*
|
||||||
*
|
* @param JID The JID of the stream host.
|
||||||
* @param JID The jabber ID of the stream host.
|
|
||||||
* @param address The internet address 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.
|
* @return The added stream host.
|
||||||
*/
|
*/
|
||||||
public StreamHost addStreamHost(final String JID, final String address,
|
public StreamHost addStreamHost(final String JID, final String address, final int port) {
|
||||||
final int port) {
|
|
||||||
StreamHost host = new StreamHost(JID, address);
|
StreamHost host = new StreamHost(JID, address);
|
||||||
host.setPort(port);
|
host.setPort(port);
|
||||||
addStreamHost(host);
|
addStreamHost(host);
|
||||||
|
@ -131,9 +124,8 @@ public class Bytestream extends IQ {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a potential stream host that the remote user can transfer the file
|
* Adds a potential stream host that the remote user can transfer the file through.
|
||||||
* through.
|
*
|
||||||
*
|
|
||||||
* @param host The potential stream host.
|
* @param host The potential stream host.
|
||||||
*/
|
*/
|
||||||
public void addStreamHost(final StreamHost 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.
|
* Returns the list of stream hosts contained in the packet.
|
||||||
*
|
*
|
||||||
* @return Returns the list of stream hosts contained in the packet.
|
* @return Returns the list of stream hosts contained in the packet.
|
||||||
*/
|
*/
|
||||||
public Collection<StreamHost> getStreamHosts() {
|
public Collection<StreamHost> getStreamHosts() {
|
||||||
|
@ -150,15 +142,13 @@ public class Bytestream extends IQ {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the stream host related to the given jabber ID, or null if there
|
* Returns the stream host related to the given JID, or null if there is none.
|
||||||
* is none.
|
*
|
||||||
*
|
* @param JID The JID of the desired stream host.
|
||||||
* @param JID The jabber ID of the desired stream host.
|
* @return Returns the stream host related to the given JID, or null if there is none.
|
||||||
* @return Returns the stream host related to the given jabber ID, or null
|
|
||||||
* if there is none.
|
|
||||||
*/
|
*/
|
||||||
public StreamHost getStreamHost(final String JID) {
|
public StreamHost getStreamHost(final String JID) {
|
||||||
if(JID == null) {
|
if (JID == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
for (StreamHost host : streamHosts) {
|
for (StreamHost host : streamHosts) {
|
||||||
|
@ -172,7 +162,7 @@ public class Bytestream extends IQ {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the count of stream hosts contained in this packet.
|
* Returns the count of stream hosts contained in this packet.
|
||||||
*
|
*
|
||||||
* @return Returns the count of stream hosts contained in this packet.
|
* @return Returns the count of stream hosts contained in this packet.
|
||||||
*/
|
*/
|
||||||
public int countStreamHosts() {
|
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
|
* Upon connecting to the stream host the target of the stream replies to the initiator with the
|
||||||
* initiator with the jabber id of the Socks5 host that they used.
|
* JID of the SOCKS5 host that they used.
|
||||||
*
|
*
|
||||||
* @param JID The jabber ID of the used host.
|
* @param JID The JID of the used host.
|
||||||
*/
|
*/
|
||||||
public void setUsedHost(final String JID) {
|
public void setUsedHost(final String JID) {
|
||||||
this.usedHost = new StreamHostUsed(JID);
|
this.usedHost = new StreamHostUsed(JID);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
* @return Returns the SOCKS5 host connected to by the remote user.
|
||||||
*/
|
*/
|
||||||
public StreamHostUsed getUsedHost() {
|
public StreamHostUsed getUsedHost() {
|
||||||
return usedHost;
|
return usedHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the activate element of the packet sent to the proxy host to
|
* Returns the activate element of the packet sent to the proxy host to verify the identity of
|
||||||
* verify the identity of the initiator and match them to the appropriate
|
* the initiator and match them to the appropriate stream.
|
||||||
* stream.
|
*
|
||||||
*
|
* @return Returns the activate element of the packet sent to the proxy host to verify the
|
||||||
* @return Returns the activate element of the packet sent to the proxy host
|
* identity of the initiator and match them to the appropriate stream.
|
||||||
* to verify the identity of the initiator and match them to the
|
|
||||||
* appropriate stream.
|
|
||||||
*/
|
*/
|
||||||
public Activate getToActivate() {
|
public Activate getToActivate() {
|
||||||
return toActivate;
|
return toActivate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upon the response from the target of the used host the activate packet is
|
* Upon the response from the target of the used host the activate packet is sent to the SOCKS5
|
||||||
* sent to the Socks5 proxy. The proxy will activate the stream or return an
|
* proxy. The proxy will activate the stream or return an error after verifying the identity of
|
||||||
* error after verifying the identity of the initiator, using the activate
|
* the initiator, using the activate packet.
|
||||||
* packet.
|
*
|
||||||
*
|
* @param targetID The JID of the target of the file transfer.
|
||||||
* @param targetID The jabber ID of the target of the file transfer.
|
|
||||||
*/
|
*/
|
||||||
public void setToActivate(final String targetID) {
|
public void setToActivate(final String targetID) {
|
||||||
this.toActivate = new Activate(targetID);
|
this.toActivate = new Activate(targetID);
|
||||||
|
@ -228,10 +215,12 @@ public class Bytestream extends IQ {
|
||||||
|
|
||||||
buf.append("<query xmlns=\"http://jabber.org/protocol/bytestreams\"");
|
buf.append("<query xmlns=\"http://jabber.org/protocol/bytestreams\"");
|
||||||
if (this.getType().equals(IQ.Type.SET)) {
|
if (this.getType().equals(IQ.Type.SET)) {
|
||||||
if (getSessionID() != null)
|
if (getSessionID() != null) {
|
||||||
buf.append(" sid=\"").append(getSessionID()).append("\"");
|
buf.append(" sid=\"").append(getSessionID()).append("\"");
|
||||||
if (getMode() != null)
|
}
|
||||||
|
if (getMode() != null) {
|
||||||
buf.append(" mode = \"").append(getMode()).append("\"");
|
buf.append(" mode = \"").append(getMode()).append("\"");
|
||||||
|
}
|
||||||
buf.append(">");
|
buf.append(">");
|
||||||
if (getToActivate() == null) {
|
if (getToActivate() == null) {
|
||||||
for (StreamHost streamHost : getStreamHosts()) {
|
for (StreamHost streamHost : getStreamHosts()) {
|
||||||
|
@ -244,8 +233,9 @@ public class Bytestream extends IQ {
|
||||||
}
|
}
|
||||||
else if (this.getType().equals(IQ.Type.RESULT)) {
|
else if (this.getType().equals(IQ.Type.RESULT)) {
|
||||||
buf.append(">");
|
buf.append(">");
|
||||||
if (getUsedHost() != null)
|
if (getUsedHost() != null) {
|
||||||
buf.append(getUsedHost().toXML());
|
buf.append(getUsedHost().toXML());
|
||||||
|
}
|
||||||
// A result from the server can also contain stream hosts
|
// A result from the server can also contain stream hosts
|
||||||
else if (countStreamHosts() > 0) {
|
else if (countStreamHosts() > 0) {
|
||||||
for (StreamHost host : streamHosts) {
|
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 {
|
else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -262,10 +255,9 @@ public class Bytestream extends IQ {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Packet extension that represents a potential Socks5 proxy for the file
|
* Packet extension that represents a potential SOCKS5 proxy for the file transfer. Stream hosts
|
||||||
* transfer. Stream hosts are forwared to the target of the file transfer
|
* are forwarded to the target of the file transfer who then chooses and connects to one.
|
||||||
* who then chooses and connects to one.
|
*
|
||||||
*
|
|
||||||
* @author Alexander Wenckus
|
* @author Alexander Wenckus
|
||||||
*/
|
*/
|
||||||
public static class StreamHost implements PacketExtension {
|
public static class StreamHost implements PacketExtension {
|
||||||
|
@ -282,8 +274,8 @@ public class Bytestream extends IQ {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default constructor.
|
* 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.
|
* @param address The internet address of the stream host.
|
||||||
*/
|
*/
|
||||||
public StreamHost(final String JID, final String address) {
|
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.
|
* Returns the JID of the stream host.
|
||||||
*
|
*
|
||||||
* @return Returns the jabber ID of the stream host.
|
* @return Returns the JID of the stream host.
|
||||||
*/
|
*/
|
||||||
public String getJID() {
|
public String getJID() {
|
||||||
return JID;
|
return JID;
|
||||||
|
@ -302,7 +294,7 @@ public class Bytestream extends IQ {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the internet address of the stream host.
|
* Returns the internet address of the stream host.
|
||||||
*
|
*
|
||||||
* @return Returns the internet address of the stream host.
|
* @return Returns the internet address of the stream host.
|
||||||
*/
|
*/
|
||||||
public String getAddress() {
|
public String getAddress() {
|
||||||
|
@ -311,20 +303,17 @@ public class Bytestream extends IQ {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the port of the stream host.
|
* Sets the port of the stream host.
|
||||||
*
|
*
|
||||||
* @param port The port on which the potential stream host would accept
|
* @param port The port on which the potential stream host would accept the connection.
|
||||||
* the connection.
|
|
||||||
*/
|
*/
|
||||||
public void setPort(final int port) {
|
public void setPort(final int port) {
|
||||||
this.port = port;
|
this.port = port;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the port on which the potential stream host would accept the
|
* Returns the port on which the potential stream host would accept the connection.
|
||||||
* connection.
|
*
|
||||||
*
|
* @return 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() {
|
public int getPort() {
|
||||||
return port;
|
return port;
|
||||||
|
@ -344,10 +333,12 @@ public class Bytestream extends IQ {
|
||||||
buf.append("<").append(getElementName()).append(" ");
|
buf.append("<").append(getElementName()).append(" ");
|
||||||
buf.append("jid=\"").append(getJID()).append("\" ");
|
buf.append("jid=\"").append(getJID()).append("\" ");
|
||||||
buf.append("host=\"").append(getAddress()).append("\" ");
|
buf.append("host=\"").append(getAddress()).append("\" ");
|
||||||
if (getPort() != 0)
|
if (getPort() != 0) {
|
||||||
buf.append("port=\"").append(getPort()).append("\"");
|
buf.append("port=\"").append(getPort()).append("\"");
|
||||||
else
|
}
|
||||||
|
else {
|
||||||
buf.append("zeroconf=\"_jabber.bytestreams\"");
|
buf.append("zeroconf=\"_jabber.bytestreams\"");
|
||||||
|
}
|
||||||
buf.append("/>");
|
buf.append("/>");
|
||||||
|
|
||||||
return buf.toString();
|
return buf.toString();
|
||||||
|
@ -355,10 +346,9 @@ public class Bytestream extends IQ {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After selected a Socks5 stream host and successfully connecting, the
|
* After selected a SOCKS5 stream host and successfully connecting, the target of the file
|
||||||
* target of the file transfer returns a byte stream packet with the stream
|
* transfer returns a byte stream packet with the stream host used extension.
|
||||||
* host used extension.
|
*
|
||||||
*
|
|
||||||
* @author Alexander Wenckus
|
* @author Alexander Wenckus
|
||||||
*/
|
*/
|
||||||
public static class StreamHostUsed implements PacketExtension {
|
public static class StreamHostUsed implements PacketExtension {
|
||||||
|
@ -371,17 +361,17 @@ public class Bytestream extends IQ {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default constructor.
|
* 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) {
|
public StreamHostUsed(final String JID) {
|
||||||
this.JID = JID;
|
this.JID = JID;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the jabber ID of the selected stream host.
|
* Returns the JID of the selected stream host.
|
||||||
*
|
*
|
||||||
* @return Returns the jabber ID of the selected stream host.
|
* @return Returns the JID of the selected stream host.
|
||||||
*/
|
*/
|
||||||
public String getJID() {
|
public String getJID() {
|
||||||
return JID;
|
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 packet sent by the stream initiator to the stream proxy to activate the connection.
|
||||||
* the connection.
|
*
|
||||||
*
|
|
||||||
* @author Alexander Wenckus
|
* @author Alexander Wenckus
|
||||||
*/
|
*/
|
||||||
public static class Activate implements PacketExtension {
|
public static class Activate implements PacketExtension {
|
||||||
|
@ -420,7 +409,7 @@ public class Bytestream extends IQ {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default constructor specifying the target of the stream.
|
* Default constructor specifying the target of the stream.
|
||||||
*
|
*
|
||||||
* @param target The target of the stream.
|
* @param target The target of the stream.
|
||||||
*/
|
*/
|
||||||
public Activate(final String target) {
|
public Activate(final String target) {
|
||||||
|
@ -429,7 +418,7 @@ public class Bytestream extends IQ {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the target of the activation.
|
* Returns the target of the activation.
|
||||||
*
|
*
|
||||||
* @return Returns the target of the activation.
|
* @return Returns the target of the activation.
|
||||||
*/
|
*/
|
||||||
public String getTarget() {
|
public String getTarget() {
|
||||||
|
@ -455,7 +444,7 @@ public class Bytestream extends IQ {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The stream can be either a TCP stream or a UDP stream.
|
* The stream can be either a TCP stream or a UDP stream.
|
||||||
*
|
*
|
||||||
* @author Alexander Wenckus
|
* @author Alexander Wenckus
|
||||||
*/
|
*/
|
||||||
public enum Mode {
|
public enum Mode {
|
||||||
|
@ -475,7 +464,7 @@ public class Bytestream extends IQ {
|
||||||
try {
|
try {
|
||||||
mode = Mode.valueOf(name);
|
mode = Mode.valueOf(name);
|
||||||
}
|
}
|
||||||
catch(Exception ex) {
|
catch (Exception ex) {
|
||||||
mode = tcp;
|
mode = tcp;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -19,9 +19,20 @@
|
||||||
*/
|
*/
|
||||||
package org.jivesoftware.smackx.filetransfer;
|
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.ConnectionListener;
|
||||||
import org.jivesoftware.smack.PacketCollector;
|
import org.jivesoftware.smack.PacketCollector;
|
||||||
import org.jivesoftware.smack.Connection;
|
|
||||||
import org.jivesoftware.smack.XMPPException;
|
import org.jivesoftware.smack.XMPPException;
|
||||||
import org.jivesoftware.smack.filter.PacketIDFilter;
|
import org.jivesoftware.smack.filter.PacketIDFilter;
|
||||||
import org.jivesoftware.smack.packet.IQ;
|
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.Form;
|
||||||
import org.jivesoftware.smackx.FormField;
|
import org.jivesoftware.smackx.FormField;
|
||||||
import org.jivesoftware.smackx.ServiceDiscoveryManager;
|
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.DataForm;
|
||||||
import org.jivesoftware.smackx.packet.StreamInitiation;
|
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
|
* 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
|
* being sent the remote user chooses the type of stream under which the file
|
||||||
* will be sent.
|
* will be sent.
|
||||||
*
|
*
|
||||||
* @author Alexander Wenckus
|
* @author Alexander Wenckus
|
||||||
* @see <a href=http://www.jabber.org/jeps/jep-0096.html>JEP-0096: File Transfer</a>
|
* @see <a href="http://www.jabber.org/jeps/jep-0096.html">JEP-0096: File Transfer</a>
|
||||||
*/
|
*/
|
||||||
public class FileTransferNegotiator {
|
public class FileTransferNegotiator {
|
||||||
|
|
||||||
// Static
|
// 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 = {
|
private static final String[] NAMESPACE = {
|
||||||
"http://jabber.org/protocol/si/profile/file-transfer",
|
"http://jabber.org/protocol/si/profile/file-transfer",
|
||||||
"http://jabber.org/protocol/si", BYTE_STREAM, INBAND_BYTE_STREAM};
|
"http://jabber.org/protocol/si"};
|
||||||
|
|
||||||
private static final String[] PROTOCOLS = {BYTE_STREAM, INBAND_BYTE_STREAM};
|
|
||||||
|
|
||||||
private static final Map<Connection, FileTransferNegotiator> transferObject =
|
private static final Map<Connection, FileTransferNegotiator> transferObject =
|
||||||
new ConcurrentHashMap<Connection, FileTransferNegotiator>();
|
new ConcurrentHashMap<Connection, FileTransferNegotiator>();
|
||||||
|
@ -121,14 +118,24 @@ public class FileTransferNegotiator {
|
||||||
final boolean isEnabled) {
|
final boolean isEnabled) {
|
||||||
ServiceDiscoveryManager manager = ServiceDiscoveryManager
|
ServiceDiscoveryManager manager = ServiceDiscoveryManager
|
||||||
.getInstanceFor(connection);
|
.getInstanceFor(connection);
|
||||||
for (String ns : NAMESPACE) {
|
|
||||||
|
List<String> namespaces = new ArrayList<String>();
|
||||||
|
namespaces.addAll(Arrays.asList(NAMESPACE));
|
||||||
|
namespaces.add(InBandBytestreamManager.NAMESPACE);
|
||||||
|
if (!IBB_ONLY) {
|
||||||
|
namespaces.add(Socks5BytestreamManager.NAMESPACE);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String namespace : namespaces) {
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
manager.addFeature(ns);
|
if (!manager.includesFeature(namespace)) {
|
||||||
}
|
manager.addFeature(namespace);
|
||||||
else {
|
}
|
||||||
manager.removeFeature(ns);
|
} else {
|
||||||
|
manager.removeFeature(namespace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -139,20 +146,31 @@ public class FileTransferNegotiator {
|
||||||
* @return True if all related services are enabled, false if they are not.
|
* @return True if all related services are enabled, false if they are not.
|
||||||
*/
|
*/
|
||||||
public static boolean isServiceEnabled(final Connection connection) {
|
public static boolean isServiceEnabled(final Connection connection) {
|
||||||
for (String ns : NAMESPACE) {
|
ServiceDiscoveryManager manager = ServiceDiscoveryManager
|
||||||
if (!ServiceDiscoveryManager.getInstanceFor(connection).includesFeature(ns))
|
.getInstanceFor(connection);
|
||||||
|
|
||||||
|
List<String> namespaces = new ArrayList<String>();
|
||||||
|
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 false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
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 ID The packet ID of the
|
||||||
* @param to To whom the packet is addressed.
|
* @param to To whom the packet is addressed.
|
||||||
* @param from From whom the packet is sent.
|
* @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.
|
* @return The created IQ packet.
|
||||||
*/
|
*/
|
||||||
public static IQ createIQ(final String ID, final String to,
|
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.
|
* @return Returns a collection of the supported transfer protocols.
|
||||||
*/
|
*/
|
||||||
public static Collection<String> getSupportedProtocols() {
|
public static Collection<String> getSupportedProtocols() {
|
||||||
return Collections.unmodifiableList(Arrays.asList(PROTOCOLS));
|
List<String> protocols = new ArrayList<String>();
|
||||||
|
protocols.add(InBandBytestreamManager.NAMESPACE);
|
||||||
|
if (!IBB_ONLY) {
|
||||||
|
protocols.add(Socks5BytestreamManager.NAMESPACE);
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(protocols);
|
||||||
}
|
}
|
||||||
|
|
||||||
// non-static
|
// non-static
|
||||||
|
|
||||||
private final Connection connection;
|
private final Connection connection;
|
||||||
|
|
||||||
private final Socks5TransferNegotiatorManager byteStreamTransferManager;
|
private final StreamNegotiator byteStreamTransferManager;
|
||||||
|
|
||||||
private final StreamNegotiator inbandTransferManager;
|
private final StreamNegotiator inbandTransferManager;
|
||||||
|
|
||||||
|
@ -191,7 +214,7 @@ public class FileTransferNegotiator {
|
||||||
configureConnection(connection);
|
configureConnection(connection);
|
||||||
|
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
byteStreamTransferManager = new Socks5TransferNegotiatorManager(connection);
|
byteStreamTransferManager = new Socks5TransferNegotiator(connection);
|
||||||
inbandTransferManager = new IBBTransferNegotiator(connection);
|
inbandTransferManager = new IBBTransferNegotiator(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,7 +244,6 @@ public class FileTransferNegotiator {
|
||||||
|
|
||||||
private void cleanup(final Connection connection) {
|
private void cleanup(final Connection connection) {
|
||||||
if (transferObject.remove(connection) != null) {
|
if (transferObject.remove(connection) != null) {
|
||||||
byteStreamTransferManager.cleanup();
|
|
||||||
inbandTransferManager.cleanup();
|
inbandTransferManager.cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -288,10 +310,10 @@ public class FileTransferNegotiator {
|
||||||
boolean isIBB = false;
|
boolean isIBB = false;
|
||||||
for (Iterator<FormField.Option> it = field.getOptions(); it.hasNext();) {
|
for (Iterator<FormField.Option> it = field.getOptions(); it.hasNext();) {
|
||||||
variable = it.next().getValue();
|
variable = it.next().getValue();
|
||||||
if (variable.equals(BYTE_STREAM) && !IBB_ONLY) {
|
if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) {
|
||||||
isByteStream = true;
|
isByteStream = true;
|
||||||
}
|
}
|
||||||
else if (variable.equals(INBAND_BYTE_STREAM)) {
|
else if (variable.equals(InBandBytestreamManager.NAMESPACE)) {
|
||||||
isIBB = true;
|
isIBB = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -304,11 +326,11 @@ public class FileTransferNegotiator {
|
||||||
|
|
||||||
if (isByteStream && isIBB && field.getType().equals(FormField.TYPE_LIST_MULTI)) {
|
if (isByteStream && isIBB && field.getType().equals(FormField.TYPE_LIST_MULTI)) {
|
||||||
return new FaultTolerantNegotiator(connection,
|
return new FaultTolerantNegotiator(connection,
|
||||||
byteStreamTransferManager.createNegotiator(),
|
byteStreamTransferManager,
|
||||||
inbandTransferManager);
|
inbandTransferManager);
|
||||||
}
|
}
|
||||||
else if (isByteStream) {
|
else if (isByteStream) {
|
||||||
return byteStreamTransferManager.createNegotiator();
|
return byteStreamTransferManager;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return inbandTransferManager;
|
return inbandTransferManager;
|
||||||
|
@ -346,11 +368,11 @@ public class FileTransferNegotiator {
|
||||||
* the option of, accepting, rejecting, or not responding to a received file
|
* the option of, accepting, rejecting, or not responding to a received file
|
||||||
* transfer request.
|
* transfer request.
|
||||||
* <p/>
|
* <p/>
|
||||||
* 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
|
* type to send the file across. The two choices this implementation
|
||||||
* provides to the other user for file transfer are <a
|
* provides to the other user for file transfer are <a
|
||||||
* href="http://www.jabber.org/jeps/jep-0065.html">SOCKS5 Bytestreams</a>,
|
* href="http://www.jabber.org/jeps/jep-0065.html">SOCKS5 Bytestreams</a>,
|
||||||
* which is the prefered method of transfer, and <a
|
* which is the preferred method of transfer, and <a
|
||||||
* href="http://www.jabber.org/jeps/jep-0047.html">In-Band Bytestreams</a>,
|
* href="http://www.jabber.org/jeps/jep-0047.html">In-Band Bytestreams</a>,
|
||||||
* which is the fallback mechanism.
|
* which is the fallback mechanism.
|
||||||
* <p/>
|
* <p/>
|
||||||
|
@ -422,10 +444,10 @@ public class FileTransferNegotiator {
|
||||||
boolean isIBB = false;
|
boolean isIBB = false;
|
||||||
for (Iterator<String> it = field.getValues(); it.hasNext();) {
|
for (Iterator<String> it = field.getValues(); it.hasNext();) {
|
||||||
variable = it.next();
|
variable = it.next();
|
||||||
if (variable.equals(BYTE_STREAM) && !IBB_ONLY) {
|
if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) {
|
||||||
isByteStream = true;
|
isByteStream = true;
|
||||||
}
|
}
|
||||||
else if (variable.equals(INBAND_BYTE_STREAM)) {
|
else if (variable.equals(InBandBytestreamManager.NAMESPACE)) {
|
||||||
isIBB = true;
|
isIBB = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -438,10 +460,10 @@ public class FileTransferNegotiator {
|
||||||
|
|
||||||
if (isByteStream && isIBB) {
|
if (isByteStream && isIBB) {
|
||||||
return new FaultTolerantNegotiator(connection,
|
return new FaultTolerantNegotiator(connection,
|
||||||
byteStreamTransferManager.createNegotiator(), inbandTransferManager);
|
byteStreamTransferManager, inbandTransferManager);
|
||||||
}
|
}
|
||||||
else if (isByteStream) {
|
else if (isByteStream) {
|
||||||
return byteStreamTransferManager.createNegotiator();
|
return byteStreamTransferManager;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return inbandTransferManager;
|
return inbandTransferManager;
|
||||||
|
@ -453,9 +475,9 @@ public class FileTransferNegotiator {
|
||||||
FormField field = new FormField(STREAM_DATA_FIELD_NAME);
|
FormField field = new FormField(STREAM_DATA_FIELD_NAME);
|
||||||
field.setType(FormField.TYPE_LIST_MULTI);
|
field.setType(FormField.TYPE_LIST_MULTI);
|
||||||
if (!IBB_ONLY) {
|
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);
|
form.addField(field);
|
||||||
return form;
|
return form;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,402 +19,107 @@
|
||||||
*/
|
*/
|
||||||
package org.jivesoftware.smackx.filetransfer;
|
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.InputStream;
|
||||||
import java.io.OutputStream;
|
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
|
* 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
|
* @author Alexander Wenckus
|
||||||
* @see <a href="http://www.jabber.org/jeps/jep-0047.html">JEP-0047: In-Band
|
* @author Henning Staib
|
||||||
|
* @see <a href="http://xmpp.org/extensions/xep-0047.html">XEP-0047: In-Band
|
||||||
* Bytestreams (IBB)</a>
|
* Bytestreams (IBB)</a>
|
||||||
*/
|
*/
|
||||||
public class IBBTransferNegotiator extends StreamNegotiator {
|
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 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.
|
* @param connection The connection which this negotiator works on.
|
||||||
*/
|
*/
|
||||||
protected IBBTransferNegotiator(Connection connection) {
|
protected IBBTransferNegotiator(Connection connection) {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
}
|
this.manager = InBandBytestreamManager.getByteStreamManager(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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public OutputStream createOutgoingStream(String streamID, String initiator,
|
public OutputStream createOutgoingStream(String streamID, String initiator,
|
||||||
String target) throws XMPPException {
|
String target) throws XMPPException {
|
||||||
Open openIQ = new Open(streamID, DEFAULT_BLOCK_SIZE);
|
InBandBytestreamSession session = this.manager.establishSession(target, streamID);
|
||||||
openIQ.setTo(target);
|
session.setCloseBothStreamsEnabled(true);
|
||||||
openIQ.setType(IQ.Type.SET);
|
return session.getOutputStream();
|
||||||
|
}
|
||||||
|
|
||||||
// wait for the result from the peer
|
public InputStream createIncomingStream(StreamInitiation initiation)
|
||||||
PacketCollector collector = connection
|
throws XMPPException {
|
||||||
.createPacketCollector(new PacketIDFilter(openIQ.getPacketID()));
|
/*
|
||||||
connection.sendPacket(openIQ);
|
* In-Band Bytestream initiation listener must ignore next in-band
|
||||||
// We don't want to wait forever for the result
|
* bytestream request with given session ID
|
||||||
IQ openResponse = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
|
*/
|
||||||
collector.cancel();
|
this.manager.ignoreBytestreamRequestOnce(initiation.getSessionID());
|
||||||
|
|
||||||
if (openResponse == null) {
|
Packet streamInitiation = initiateIncomingStream(this.connection, initiation);
|
||||||
throw new XMPPException("No response from peer on IBB open");
|
return negotiateIncomingStream(streamInitiation);
|
||||||
}
|
}
|
||||||
|
|
||||||
IQ.Type type = openResponse.getType();
|
public PacketFilter getInitiationPacketFilter(String from, String streamID) {
|
||||||
if (!type.equals(IQ.Type.RESULT)) {
|
/*
|
||||||
if (type.equals(IQ.Type.ERROR)) {
|
* this method is always called prior to #negotiateIncomingStream() so
|
||||||
throw new XMPPException("Target returned an error",
|
* the In-Band Bytestream initiation listener must ignore the next
|
||||||
openResponse.getError());
|
* In-Band Bytestream request with the given session ID
|
||||||
}
|
*/
|
||||||
else {
|
this.manager.ignoreBytestreamRequestOnce(streamID);
|
||||||
throw new XMPPException("Target returned unknown response");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new IBBOutputStream(target, streamID, DEFAULT_BLOCK_SIZE);
|
return new AndFilter(new FromContainsFilter(from), new IBBOpenSidFilter(streamID));
|
||||||
}
|
}
|
||||||
|
|
||||||
public String[] getNamespaces() {
|
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() {
|
public void cleanup() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private class IBBOutputStream extends OutputStream {
|
/**
|
||||||
|
* This PacketFilter accepts an incoming In-Band Bytestream open request
|
||||||
protected byte[] buffer;
|
* with a specified session ID.
|
||||||
|
*/
|
||||||
protected int count = 0;
|
private static class IBBOpenSidFilter extends PacketTypeFilter {
|
||||||
|
|
||||||
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 {
|
|
||||||
|
|
||||||
private String sessionID;
|
private String sessionID;
|
||||||
|
|
||||||
public IBBOpenSidFilter(String sessionID) {
|
public IBBOpenSidFilter(String sessionID) {
|
||||||
|
super(Open.class);
|
||||||
if (sessionID == null) {
|
if (sessionID == null) {
|
||||||
throw new IllegalArgumentException("StreamID cannot be null");
|
throw new IllegalArgumentException("StreamID cannot be null");
|
||||||
}
|
}
|
||||||
|
@ -422,39 +127,26 @@ public class IBBTransferNegotiator extends StreamNegotiator {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean accept(Packet packet) {
|
public boolean accept(Packet packet) {
|
||||||
if (!IBBExtensions.Open.class.isInstance(packet)) {
|
if (super.accept(packet)) {
|
||||||
return false;
|
Open bytestream = (Open) packet;
|
||||||
}
|
|
||||||
IBBExtensions.Open open = (IBBExtensions.Open) packet;
|
|
||||||
String sessionID = open.getSessionID();
|
|
||||||
|
|
||||||
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 ByteStreamRequest(InBandBytestreamManager manager, Open byteStreamRequest) {
|
||||||
private String from;
|
super(manager, byteStreamRequest);
|
||||||
|
|
||||||
public IBBMessageSidFilter(String from, String sessionID) {
|
|
||||||
this.from = from;
|
|
||||||
this.sessionID = sessionID;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
* 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 not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
|
@ -19,119 +13,100 @@
|
||||||
*/
|
*/
|
||||||
package org.jivesoftware.smackx.filetransfer;
|
package org.jivesoftware.smackx.filetransfer;
|
||||||
|
|
||||||
import org.jivesoftware.smack.PacketCollector;
|
import java.io.IOException;
|
||||||
import org.jivesoftware.smack.SmackConfiguration;
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.PushbackInputStream;
|
||||||
|
|
||||||
import org.jivesoftware.smack.Connection;
|
import org.jivesoftware.smack.Connection;
|
||||||
import org.jivesoftware.smack.XMPPException;
|
import org.jivesoftware.smack.XMPPException;
|
||||||
import org.jivesoftware.smack.filter.AndFilter;
|
import org.jivesoftware.smack.filter.AndFilter;
|
||||||
import org.jivesoftware.smack.filter.FromMatchesFilter;
|
import org.jivesoftware.smack.filter.FromMatchesFilter;
|
||||||
import org.jivesoftware.smack.filter.PacketFilter;
|
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.IQ;
|
||||||
import org.jivesoftware.smack.packet.Packet;
|
import org.jivesoftware.smack.packet.Packet;
|
||||||
import org.jivesoftware.smack.packet.XMPPError;
|
import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager;
|
||||||
import org.jivesoftware.smack.util.StringUtils;
|
import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamRequest;
|
||||||
import org.jivesoftware.smackx.packet.Bytestream;
|
import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamSession;
|
||||||
import org.jivesoftware.smackx.packet.Bytestream.StreamHost;
|
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
|
||||||
import org.jivesoftware.smackx.packet.Bytestream.StreamHostUsed;
|
|
||||||
import org.jivesoftware.smackx.packet.StreamInitiation;
|
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
|
* Negotiates a SOCKS5 Bytestream to be used for file transfers. The implementation is based on the
|
||||||
* over a seperate socket. The actual transfer though takes place over a
|
* {@link Socks5BytestreamManager} and the {@link Socks5BytestreamRequest}.
|
||||||
* seperatly created socket.
|
*
|
||||||
* <p/>
|
* @author Henning Staib
|
||||||
* A SOCKS5 file transfer generally has three parites, the initiator, the
|
* @see <a href="http://xmpp.org/extensions/xep-0065.html">XEP-0065: SOCKS5 Bytestreams</a>
|
||||||
* 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.
|
|
||||||
* <p/>
|
|
||||||
* 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.
|
|
||||||
* <p/>
|
|
||||||
* 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 <a href="http://www.jabber.org/jeps/jep-0065.html">JEP-0065: SOCKS5
|
|
||||||
* Bytestreams</a>
|
|
||||||
*/
|
*/
|
||||||
public class Socks5TransferNegotiator extends StreamNegotiator {
|
public class Socks5TransferNegotiator extends StreamNegotiator {
|
||||||
|
|
||||||
protected static final String NAMESPACE = "http://jabber.org/protocol/bytestreams";
|
private Connection connection;
|
||||||
|
|
||||||
/**
|
private Socks5BytestreamManager manager;
|
||||||
* 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;
|
|
||||||
|
|
||||||
public static boolean isAllowLocalProxyHost = true;
|
Socks5TransferNegotiator(Connection connection) {
|
||||||
|
|
||||||
private final Connection connection;
|
|
||||||
|
|
||||||
private Socks5TransferNegotiatorManager transferNegotiatorManager;
|
|
||||||
|
|
||||||
public Socks5TransferNegotiator(Socks5TransferNegotiatorManager transferNegotiatorManager,
|
|
||||||
final Connection connection)
|
|
||||||
{
|
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.transferNegotiatorManager = transferNegotiatorManager;
|
this.manager = Socks5BytestreamManager.getBytestreamManager(this.connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PacketFilter getInitiationPacketFilter(String from, String sessionID) {
|
@Override
|
||||||
return new AndFilter(new FromMatchesFilter(from),
|
public OutputStream createOutgoingStream(String streamID, String initiator, String target)
|
||||||
new BytestreamSIDFilter(sessionID));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
@Override
|
||||||
* (non-Javadoc)
|
public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException,
|
||||||
*
|
InterruptedException {
|
||||||
* @see org.jivesoftware.smackx.filetransfer.StreamNegotiator#initiateDownload(
|
/*
|
||||||
* org.jivesoftware.smackx.packet.StreamInitiation, java.io.File)
|
* SOCKS5 initiation listener must ignore next SOCKS5 Bytestream request with given session
|
||||||
*/
|
* ID
|
||||||
InputStream negotiateIncomingStream(Packet streamInitiation)
|
*/
|
||||||
throws XMPPException {
|
this.manager.ignoreBytestreamRequestOnce(initiation.getSessionID());
|
||||||
Bytestream streamHostsInfo = (Bytestream) streamInitiation;
|
|
||||||
|
|
||||||
if (streamHostsInfo.getType().equals(IQ.Type.ERROR)) {
|
Packet streamInitiation = initiateIncomingStream(this.connection, initiation);
|
||||||
throw new XMPPException(streamHostsInfo.getError());
|
return negotiateIncomingStream(streamInitiation);
|
||||||
}
|
}
|
||||||
SelectedHostInfo selectedHost;
|
|
||||||
|
@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 {
|
try {
|
||||||
// select appropriate host
|
PushbackInputStream stream = new PushbackInputStream(session.getInputStream());
|
||||||
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());
|
|
||||||
int firstByte = stream.read();
|
int firstByte = stream.read();
|
||||||
stream.unread(firstByte);
|
stream.unread(firstByte);
|
||||||
return stream;
|
return stream;
|
||||||
|
@ -139,435 +114,51 @@ public class Socks5TransferNegotiator extends StreamNegotiator {
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
throw new XMPPException("Error establishing input stream", 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<StreamHost> 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:
|
|
||||||
* <p/>
|
|
||||||
* <pre>
|
|
||||||
* <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>
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @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 <b><i>Bytestream</b></i> 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<Bytestream.StreamHost> 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() {
|
public void cleanup() {
|
||||||
|
/* do nothing */
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class SelectedHostInfo {
|
/**
|
||||||
|
* This PacketFilter accepts an incoming SOCKS5 Bytestream request with a specified session ID.
|
||||||
protected XMPPException exception;
|
*/
|
||||||
|
private static class BytestreamSIDFilter extends PacketTypeFilter {
|
||||||
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 {
|
|
||||||
|
|
||||||
private String sessionID;
|
private String sessionID;
|
||||||
|
|
||||||
public BytestreamSIDFilter(String sessionID) {
|
public BytestreamSIDFilter(String sessionID) {
|
||||||
|
super(Bytestream.class);
|
||||||
if (sessionID == null) {
|
if (sessionID == null) {
|
||||||
throw new IllegalArgumentException("StreamID cannot be null");
|
throw new IllegalArgumentException("StreamID cannot be null");
|
||||||
}
|
}
|
||||||
this.sessionID = sessionID;
|
this.sessionID = sessionID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean accept(Packet packet) {
|
public boolean accept(Packet packet) {
|
||||||
if (!Bytestream.class.isInstance(packet)) {
|
if (super.accept(packet)) {
|
||||||
return false;
|
Bytestream bytestream = (Bytestream) packet;
|
||||||
}
|
|
||||||
Bytestream bytestream = (Bytestream) packet;
|
|
||||||
String sessionID = bytestream.getSessionID();
|
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<String, Integer> addressBlacklist
|
|
||||||
= new Cache<String, Integer>(100, BLACKLIST_LIFETIME);
|
|
||||||
|
|
||||||
private Connection connection;
|
|
||||||
|
|
||||||
private List<String> proxies;
|
|
||||||
|
|
||||||
private List<Bytestream.StreamHost> 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<Bytestream.StreamHost> 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<DiscoverInfo.Identity> 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<String>();
|
|
||||||
ServiceDiscoveryManager manager = ServiceDiscoveryManager
|
|
||||||
.getInstanceFor(connection);
|
|
||||||
try {
|
|
||||||
DiscoverItems discoItems = manager.discoverItems(connection.getServiceName());
|
|
||||||
Iterator<DiscoverItems.Item> 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<Bytestream.StreamHost> streamHosts = new ArrayList<Bytestream.StreamHost>();
|
|
||||||
Iterator<String> it = proxies.iterator();
|
|
||||||
IQ query;
|
|
||||||
PacketCollector collector;
|
|
||||||
Bytestream response;
|
|
||||||
while (it.hasNext()) {
|
|
||||||
String jid = it.next();
|
|
||||||
query = new IQ() {
|
|
||||||
public String getChildElementXML() {
|
|
||||||
return "<query xmlns=\"http://jabber.org/protocol/bytestreams\"/>";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
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<String, Socket> connectionMap = new HashMap<String, Socket>();
|
|
||||||
|
|
||||||
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--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -37,7 +37,7 @@ import java.io.OutputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After the file transfer negotiation process is completed according to
|
* 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
|
* negotiator. The stream negotiator will then negotiate the chosen stream and
|
||||||
* return the stream to transfer the file.
|
* 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
|
* Creates the initiation acceptance packet to forward to the stream
|
||||||
* initiator.
|
* 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.
|
* @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(
|
public StreamInitiation createInitiationAccept(
|
||||||
StreamInitiation streamInitiationOffer, String[] namespaces)
|
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
|
* Returns the packet filter that will return the initiation packet for the appropriate stream
|
||||||
* initiation.
|
* 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.
|
* @param streamID The stream ID related to the transfer.
|
||||||
* @return The <b><i>PacketFilter</b></i> that will return the packet relatable to the stream
|
* @return The <b><i>PacketFilter</b></i> that will return the packet relatable to the stream
|
||||||
* initiation.
|
* initiation.
|
||||||
|
@ -112,23 +112,26 @@ public abstract class StreamNegotiator {
|
||||||
public abstract PacketFilter getInitiationPacketFilter(String from, String streamID);
|
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
|
* This method handles the file stream download negotiation process. The
|
||||||
* appropriate stream negotiator's initiate incoming stream is called after
|
* appropriate stream negotiator's initiate incoming stream is called after
|
||||||
* an appropriate file transfer method is selected. The manager will respond
|
* an appropriate file transfer method is selected. The manager will respond
|
||||||
* to the initatior with the selected means of transfer, then it will handle
|
* to the initiator with the selected means of transfer, then it will handle
|
||||||
* any negotation specific to the particular transfer method. This method
|
* any negotiation specific to the particular transfer method. This method
|
||||||
* returns the InputStream, ready to transfer the file.
|
* returns the InputStream, ready to transfer the file.
|
||||||
*
|
*
|
||||||
* @param initiation The initation that triggered this download.
|
* @param initiation The initiation that triggered this download.
|
||||||
* @return After the negotation process is complete, the InputStream to
|
* @return After the negotiation process is complete, the InputStream to
|
||||||
* write a file to is returned.
|
* write a file to is returned.
|
||||||
* @throws XMPPException If an error occurs during this process an XMPPException is
|
* @throws XMPPException If an error occurs during this process an XMPPException is
|
||||||
* thrown.
|
* 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
|
* 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 streamID The streamID that uniquely identifies the file transfer.
|
||||||
* @param initiator The fully-qualified JID of the initiator of 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.
|
* transfer.
|
||||||
* @return The negotiated stream ready for data.
|
* @return The negotiated stream ready for data.
|
||||||
* @throws XMPPException If an error occurs during the negotiation process an
|
* @throws XMPPException If an error occurs during the negotiation process an
|
||||||
|
|
|
@ -1,241 +0,0 @@
|
||||||
/**
|
|
||||||
* $RCSfile$
|
|
||||||
* $Revision: $
|
|
||||||
* $Date: $
|
|
||||||
*
|
|
||||||
* Copyright 2003-2006 Jive Software.
|
|
||||||
*
|
|
||||||
* All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.jivesoftware.smackx.packet;
|
|
||||||
|
|
||||||
import org.jivesoftware.smack.packet.IQ;
|
|
||||||
import org.jivesoftware.smack.packet.PacketExtension;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The different extensions used throughtout the negotiation and transfer
|
|
||||||
* process.
|
|
||||||
*
|
|
||||||
* @author Alexander Wenckus
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class IBBExtensions {
|
|
||||||
|
|
||||||
public static final String NAMESPACE = "http://jabber.org/protocol/ibb";
|
|
||||||
|
|
||||||
private abstract static class IBB extends IQ {
|
|
||||||
final String sid;
|
|
||||||
|
|
||||||
private IBB(final String sid) {
|
|
||||||
this.sid = sid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the unique stream ID for this file transfer.
|
|
||||||
*
|
|
||||||
* @return Returns the unique stream ID for this file transfer.
|
|
||||||
*/
|
|
||||||
public String getSessionID() {
|
|
||||||
return sid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getNamespace() {
|
|
||||||
return NAMESPACE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a request to open the file transfer.
|
|
||||||
*
|
|
||||||
* @author Alexander Wenckus
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public static class Open extends IBB {
|
|
||||||
|
|
||||||
public static final String ELEMENT_NAME = "open";
|
|
||||||
|
|
||||||
private final int blockSize;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs an open packet.
|
|
||||||
*
|
|
||||||
* @param sid
|
|
||||||
* The streamID of the file transfer.
|
|
||||||
* @param blockSize
|
|
||||||
* The block size of the file transfer.
|
|
||||||
*/
|
|
||||||
public Open(final String sid, final int blockSize) {
|
|
||||||
super(sid);
|
|
||||||
this.blockSize = blockSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The size blocks in which the data will be sent.
|
|
||||||
*
|
|
||||||
* @return The size blocks in which the data will be sent.
|
|
||||||
*/
|
|
||||||
public int getBlockSize() {
|
|
||||||
return blockSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getElementName() {
|
|
||||||
return ELEMENT_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getChildElementXML() {
|
|
||||||
StringBuilder buf = new StringBuilder();
|
|
||||||
buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append("\" ");
|
|
||||||
buf.append("sid=\"").append(getSessionID()).append("\" ");
|
|
||||||
buf.append("block-size=\"").append(getBlockSize()).append("\"");
|
|
||||||
buf.append("/>");
|
|
||||||
return buf.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A data packet containing a portion of the file being sent encoded in
|
|
||||||
* base64.
|
|
||||||
*
|
|
||||||
* @author Alexander Wenckus
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public static class Data implements PacketExtension {
|
|
||||||
|
|
||||||
private long seq;
|
|
||||||
|
|
||||||
private String data;
|
|
||||||
|
|
||||||
public static final String ELEMENT_NAME = "data";
|
|
||||||
|
|
||||||
final String sid;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the unique stream ID identifying this file transfer.
|
|
||||||
*
|
|
||||||
* @return Returns the unique stream ID identifying this file transfer.
|
|
||||||
*/
|
|
||||||
public String getSessionID() {
|
|
||||||
return sid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getNamespace() {
|
|
||||||
return NAMESPACE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A constructor.
|
|
||||||
*
|
|
||||||
* @param sid
|
|
||||||
* The stream ID.
|
|
||||||
*/
|
|
||||||
public Data(final String sid) {
|
|
||||||
this.sid = sid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Data(final String sid, final long seq, final String data) {
|
|
||||||
this(sid);
|
|
||||||
this.seq = seq;
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getElementName() {
|
|
||||||
return ELEMENT_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the data contained in this packet.
|
|
||||||
*
|
|
||||||
* @return Returns the data contained in this packet.
|
|
||||||
*/
|
|
||||||
public String getData() {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the data contained in this packet.
|
|
||||||
*
|
|
||||||
* @param data
|
|
||||||
* The data encoded in base65
|
|
||||||
*/
|
|
||||||
public void setData(final String data) {
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the sequence of this packet in regard to the other data
|
|
||||||
* packets.
|
|
||||||
*
|
|
||||||
* @return Returns the sequence of this packet in regard to the other
|
|
||||||
* data packets.
|
|
||||||
*/
|
|
||||||
public long getSeq() {
|
|
||||||
return seq;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the sequence of this packet.
|
|
||||||
*
|
|
||||||
* @param seq
|
|
||||||
* A number between 0 and 65535
|
|
||||||
*/
|
|
||||||
public void setSeq(final long seq) {
|
|
||||||
this.seq = seq;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toXML() {
|
|
||||||
StringBuilder buf = new StringBuilder();
|
|
||||||
buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace())
|
|
||||||
.append("\" ");
|
|
||||||
buf.append("sid=\"").append(getSessionID()).append("\" ");
|
|
||||||
buf.append("seq=\"").append(getSeq()).append("\"");
|
|
||||||
buf.append(">");
|
|
||||||
buf.append(getData());
|
|
||||||
buf.append("</").append(getElementName()).append(">");
|
|
||||||
return buf.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the closing of the file transfer.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @author Alexander Wenckus
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public static class Close extends IBB {
|
|
||||||
public static final String ELEMENT_NAME = "close";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The constructor.
|
|
||||||
*
|
|
||||||
* @param sid
|
|
||||||
* The unique stream ID identifying this file transfer.
|
|
||||||
*/
|
|
||||||
public Close(String sid) {
|
|
||||||
super(sid);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getElementName() {
|
|
||||||
return ELEMENT_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getChildElementXML() {
|
|
||||||
StringBuilder buf = new StringBuilder();
|
|
||||||
buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append("\" ");
|
|
||||||
buf.append("sid=\"").append(getSessionID()).append("\"");
|
|
||||||
buf.append("/>");
|
|
||||||
return buf.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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<IQ> 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());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<IQ> 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());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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<Open, IQ>() {
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<IQ> 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<IQ> 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());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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<Message, IQ> 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<Message, IQ>() {
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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<Data, IQ> 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<Data, IQ>() {
|
||||||
|
|
||||||
|
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<Data, IQ> dataVerification = new Verification<Data, IQ>() {
|
||||||
|
|
||||||
|
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<IQ, IQ>() {
|
||||||
|
|
||||||
|
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<IQ, IQ>() {
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<IQ> 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<IQ> 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> 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> 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> byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
|
||||||
|
verify(listener, never()).incomingBytestreamRequest(byteStreamRequest.capture());
|
||||||
|
|
||||||
|
// capture reply to the In-Band Bytestream open request
|
||||||
|
ArgumentCaptor<IQ> 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> 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> 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> 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());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<IQ> 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> 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> 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> byteStreamRequest = ArgumentCaptor.forClass(BytestreamRequest.class);
|
||||||
|
verify(listener, never()).incomingBytestreamRequest(byteStreamRequest.capture());
|
||||||
|
|
||||||
|
// capture reply to the SOCKS5 Bytestream initiation
|
||||||
|
ArgumentCaptor<IQ> 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> 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> 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> 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());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<Bytestream, IQ>() {
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<String> addresses = new ArrayList<String>(proxy.getLocalAddresses());
|
||||||
|
addresses.add("1");
|
||||||
|
addresses.add("2");
|
||||||
|
addresses.add("3");
|
||||||
|
for (String address : addresses) {
|
||||||
|
proxy.addLocalAddress(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> 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<String> addresses = new ArrayList<String>(proxy.getLocalAddresses());
|
||||||
|
addresses.add("1");
|
||||||
|
addresses.add("2");
|
||||||
|
addresses.add("3");
|
||||||
|
|
||||||
|
proxy.replaceLocalAddresses(addresses);
|
||||||
|
|
||||||
|
List<String> 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<String> addresses = new ArrayList<String>();
|
||||||
|
addresses.add(address);
|
||||||
|
socks5Proxy.replaceLocalAddresses(addresses);
|
||||||
|
}
|
||||||
|
catch (UnknownHostException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
socks5Proxy.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<String, Socket> connectionMap = new ConcurrentHashMap<String, Socket>();
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* This mocked connection can used to collect packets that require a reply using a
|
||||||
|
* PacketCollector.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* <code>
|
||||||
|
* PacketCollector collector = connection.createPacketCollector(new PacketFilter());
|
||||||
|
* connection.sendPacket(packet);
|
||||||
|
* Packet reply = collector.nextResult();
|
||||||
|
* </code>
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @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<Object> addIncoming = new Answer<Object>() {
|
||||||
|
|
||||||
|
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<Packet> answer = new Answer<Packet>() {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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:
|
||||||
|
* <ul>
|
||||||
|
* <li>add responses to packets sent over the mocked XMPP connection by the
|
||||||
|
* method to test in the order the tested method awaits them</li>
|
||||||
|
* <li>call the method to test</li>
|
||||||
|
* <li>call {@link #verifyAll()} to run assertions on the request/response pairs
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* <code>
|
||||||
|
* 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();
|
||||||
|
* }
|
||||||
|
* </code>
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* 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.)
|
||||||
|
* <p>
|
||||||
|
* 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 <code>true</code> to print XML messages to the console while
|
||||||
|
* verifying the protocol.
|
||||||
|
*/
|
||||||
|
public boolean printProtocol = false;
|
||||||
|
|
||||||
|
// responses to requests are taken form this queue
|
||||||
|
Queue<Packet> responses = new LinkedList<Packet>();
|
||||||
|
|
||||||
|
// list of verifications
|
||||||
|
List<Verification<?, ?>[]> verificationList = new ArrayList<Verification<?, ?>[]>();
|
||||||
|
|
||||||
|
// list of requests
|
||||||
|
List<Packet> requests = new ArrayList<Packet>();
|
||||||
|
|
||||||
|
// list of all responses
|
||||||
|
List<Packet> responsesList = new ArrayList<Packet>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<Packet> getResponses() {
|
||||||
|
return responses;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all collected requests.
|
||||||
|
*
|
||||||
|
* @return list of requests
|
||||||
|
*/
|
||||||
|
public List<Packet> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* For convenience there are some useful predefined implementations.
|
||||||
|
*
|
||||||
|
* @param <T> class of the request
|
||||||
|
* @param <S> class of the response
|
||||||
|
*
|
||||||
|
* @author Henning Staib
|
||||||
|
*/
|
||||||
|
public interface Verification<T extends Packet, S extends Packet> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that the "To" field of the request corresponds with the "From" field of
|
||||||
|
* the response.
|
||||||
|
*/
|
||||||
|
public static Verification<Packet, Packet> correspondingSenderReceiver = new Verification<Packet, Packet>() {
|
||||||
|
|
||||||
|
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<IQ, Packet> requestTypeGET = new Verification<IQ, Packet>() {
|
||||||
|
|
||||||
|
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<IQ, Packet> requestTypeSET = new Verification<IQ, Packet>() {
|
||||||
|
|
||||||
|
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<IQ, Packet> requestTypeRESULT = new Verification<IQ, Packet>() {
|
||||||
|
|
||||||
|
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<IQ, Packet> requestTypeERROR = new Verification<IQ, Packet>() {
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
}
|
|
@ -65,13 +65,13 @@ import java.util.List;
|
||||||
*
|
*
|
||||||
* @author Gaston Dombiak
|
* @author Gaston Dombiak
|
||||||
*/
|
*/
|
||||||
public class RosterTest extends SmackTestCase {
|
public class RosterSmackTest extends SmackTestCase {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for RosterTest.
|
* Constructor for RosterSmackTest.
|
||||||
* @param name
|
* @param name
|
||||||
*/
|
*/
|
||||||
public RosterTest(String name) {
|
public RosterSmackTest(String name) {
|
||||||
super(name);
|
super(name);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<byte[]> queue = new SynchronousQueue<byte[]>();
|
||||||
|
|
||||||
|
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<byte[]> queue = new SynchronousQueue<byte[]>();
|
||||||
|
|
||||||
|
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<byte[]> queue = new SynchronousQueue<byte[]>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<byte[]> queue = new SynchronousQueue<byte[]>();
|
||||||
|
|
||||||
|
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.
|
||||||
|
* <p>
|
||||||
|
* 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<byte[]> queue = new SynchronousQueue<byte[]>();
|
||||||
|
|
||||||
|
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.
|
||||||
|
* <p>
|
||||||
|
* Support for bidirectional Socks5 bytestream:
|
||||||
|
* <ul>
|
||||||
|
* <li>Openfire (3.6.4 and below) - no</li>
|
||||||
|
* <li>ejabberd (2.0.5 and higher) - yes</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* 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<byte[]> queue = new SynchronousQueue<byte[]>();
|
||||||
|
|
||||||
|
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<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue