Smack/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamManagerTest...

1099 lines
52 KiB
Java

/**
*
* Copyright the original author or authors
*
* 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.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.mock;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.util.List;
import java.util.concurrent.TimeoutException;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.SmackException.FeatureNotSupportedException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.packet.ErrorIQ;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.StanzaError;
import org.jivesoftware.smack.test.util.NetworkUtil;
import org.jivesoftware.smack.util.ExceptionUtil;
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Identity;
import org.jivesoftware.smackx.disco.packet.DiscoverInfoBuilder;
import org.jivesoftware.smackx.disco.packet.DiscoverItems;
import org.jivesoftware.smackx.disco.packet.DiscoverItems.Item;
import org.jivesoftware.util.ConnectionUtils;
import org.jivesoftware.util.Protocol;
import org.jivesoftware.util.Verification;
import org.junit.jupiter.api.Test;
import org.jxmpp.jid.DomainBareJid;
import org.jxmpp.jid.EntityFullJid;
import org.jxmpp.jid.JidTestUtil;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
/**
* Test for Socks5BytestreamManager.
*
* @author Henning Staib
*/
public class Socks5ByteStreamManagerTest {
// settings
private static final EntityFullJid initiatorJID = JidTestUtil.DUMMY_AT_EXAMPLE_ORG_SLASH_DUMMYRESOURCE;
private static final EntityFullJid targetJID = JidTestUtil.FULL_JID_1_RESOURCE_1;
private static final DomainBareJid xmppServer = initiatorJID.asDomainBareJid();
private static final DomainBareJid proxyJID = JidTestUtil.MUC_EXAMPLE_ORG;
private static final String proxyAddress = "127.0.0.1";
/**
* Test that {@link Socks5BytestreamManager#getBytestreamManager(XMPPConnection)} returns one
* bytestream manager for every connection.
*/
@Test
public void shouldHaveOneManagerForEveryConnection() {
// mock two connections
XMPPConnection connection1 = mock(XMPPConnection.class);
XMPPConnection connection2 = mock(XMPPConnection.class);
/*
* create service discovery managers for the connections because the
* ConnectionCreationListener is not called when creating mocked connections
*/
ServiceDiscoveryManager.getInstanceFor(connection1);
ServiceDiscoveryManager.getInstanceFor(connection2);
// get bytestream manager for the first connection twice
Socks5BytestreamManager conn1ByteStreamManager1 = Socks5BytestreamManager.getBytestreamManager(connection1);
Socks5BytestreamManager conn1ByteStreamManager2 = Socks5BytestreamManager.getBytestreamManager(connection1);
// get bytestream manager for second connection
Socks5BytestreamManager conn2ByteStreamManager1 = Socks5BytestreamManager.getBytestreamManager(connection2);
// assertions
assertEquals(conn1ByteStreamManager1, conn1ByteStreamManager2);
assertNotSame(conn1ByteStreamManager1, conn2ByteStreamManager1);
}
/**
* The SOCKS5 Bytestream feature should be removed form the service discovery manager if Socks5
* bytestream feature is disabled.
*
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException if Smack detected an exceptional situation.
* @throws XMPPErrorException if there was an XMPP error returned.
*/
@Test
public void shouldDisableService() throws XMPPErrorException, SmackException, InterruptedException {
final Protocol protocol = new Protocol();
final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, initiatorJID);
Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
ServiceDiscoveryManager discoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
assertTrue(discoveryManager.includesFeature(Bytestream.NAMESPACE));
byteStreamManager.disableService();
assertFalse(discoveryManager.includesFeature(Bytestream.NAMESPACE));
}
/**
* Invoking {@link Socks5BytestreamManager#establishSession(org.jxmpp.jid.Jid)} should throw an exception
* if the given target does not support SOCKS5 Bytestream.
* @throws XMPPException if an XMPP protocol error was received.
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException if Smack detected an exceptional situation.
* @throws IOException if an I/O error occurred.
*/
@Test
public void shouldFailIfTargetDoesNotSupportSocks5()
throws XMPPException, SmackException, InterruptedException, IOException {
final Protocol protocol = new Protocol();
final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, initiatorJID);
Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
FeatureNotSupportedException e = assertThrows(FeatureNotSupportedException.class, () -> {
// build empty discover info as reply if targets features are queried
DiscoverInfo discoverInfo = DiscoverInfo.builder("disco-1").build();
protocol.addResponse(discoverInfo);
// start SOCKS5 Bytestream
byteStreamManager.establishSession(targetJID);
});
assertTrue(e.getFeature().equals("SOCKS5 Bytestream"));
assertTrue(e.getJid().equals(targetJID));
}
/**
* Invoking {@link Socks5BytestreamManager#establishSession(org.jxmpp.jid.Jid, String)} should fail if XMPP
* server doesn't return any proxies.
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException if Smack detected an exceptional situation.
* @throws XMPPException if an XMPP protocol error was received.
* @throws IOException if an I/O error occurred.
*/
@Test
public void shouldFailIfNoSocks5ProxyFound1()
throws SmackException, InterruptedException, IOException, XMPPException {
final Protocol protocol = new Protocol();
final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, initiatorJID);
final String sessionID = "session_id_shouldFailIfNoSocks5ProxyFound1";
// get Socks5ByteStreamManager for connection
Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
byteStreamManager.setAnnounceLocalStreamHost(false);
/**
* create responses in the order they should be queried specified by the XEP-0065
* specification
*/
// build discover info that supports the SOCKS5 feature
DiscoverInfoBuilder discoverInfo = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
discoverInfo.addFeature(Bytestream.NAMESPACE);
// return that SOCKS5 is supported if target is queried
protocol.addResponse(discoverInfo.build(), Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build discover items with no proxy items
DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
initiatorJID);
// return the item with no proxy if XMPP server is queried
protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
SmackException e = assertThrows(SmackException.class, () -> {
// start SOCKS5 Bytestream
byteStreamManager.establishSession(targetJID, sessionID);
fail("exception should be thrown");
});
protocol.verifyAll();
assertTrue(e.getMessage().contains("no SOCKS5 proxies available"));
}
/**
* Invoking {@link Socks5BytestreamManager#establishSession(org.jxmpp.jid.Jid, String)} should fail if no
* proxy is a SOCKS5 proxy.
*
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException if Smack detected an exceptional situation.
* @throws XMPPException if an XMPP protocol error was received.
* @throws IOException if an I/O error occurred.
*/
@Test
public void shouldFailIfNoSocks5ProxyFound2()
throws SmackException, InterruptedException, IOException, XMPPException {
final Protocol protocol = new Protocol();
final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, initiatorJID);
final String sessionID = "session_id_shouldFailIfNoSocks5ProxyFound2";
// get Socks5ByteStreamManager for connection
Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
byteStreamManager.setAnnounceLocalStreamHost(false);
/**
* create responses in the order they should be queried specified by the XEP-0065
* specification
*/
// build discover info that supports the SOCKS5 feature
DiscoverInfoBuilder discoverInfo = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
discoverInfo.addFeature(Bytestream.NAMESPACE);
// return that SOCKS5 is supported if target is queried
protocol.addResponse(discoverInfo.build(), Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build discover items containing a proxy item
DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
initiatorJID);
Item item = new Item(proxyJID);
discoverItems.addItem(item);
// return the proxy item if XMPP server is queried
protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build discover info for proxy containing information about NOT being a Socks5
// proxy
DiscoverInfoBuilder proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
Identity identity = new Identity("noproxy", proxyJID.toString(), "bytestreams");
proxyInfo.addIdentity(identity);
// return the proxy identity if proxy is queried
protocol.addResponse(proxyInfo.build(), Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
SmackException e = assertThrows(SmackException.class, () -> {
// start SOCKS5 Bytestream
byteStreamManager.establishSession(targetJID, sessionID);
});
protocol.verifyAll();
assertTrue(e.getMessage().contains("no SOCKS5 proxies available"));
}
/**
* Invoking {@link Socks5BytestreamManager#establishSession(org.jxmpp.jid.Jid, String)} should fail if no
* SOCKS5 proxy can be found. If it turns out that a proxy is not a SOCKS5 proxy it should not
* be queried again.
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException if Smack detected an exceptional situation.
* @throws XMPPException if an XMPP protocol error was received.
* @throws IOException if an I/O error occurred.
*/
@Test
public void shouldBlacklistNonSocks5Proxies() throws SmackException, InterruptedException, IOException, XMPPException {
final Protocol protocol = new Protocol();
final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, initiatorJID);
final String sessionID = "session_id_shouldBlacklistNonSocks5Proxies";
// get Socks5ByteStreamManager for connection
Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
byteStreamManager.setAnnounceLocalStreamHost(false);
/**
* create responses in the order they should be queried specified by the XEP-0065
* specification
*/
// build discover info that supports the SOCKS5 feature
DiscoverInfoBuilder discoverInfoBuilder = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
discoverInfoBuilder.addFeature(Bytestream.NAMESPACE);
DiscoverInfo discoverInfo = discoverInfoBuilder.build();
// return that SOCKS5 is supported if target is queried
protocol.addResponse(discoverInfo, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build discover items containing a proxy item
DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
initiatorJID);
Item item = new Item(proxyJID);
discoverItems.addItem(item);
// return the proxy item if XMPP server is queried
protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build discover info for proxy containing information about NOT being a Socks5
// proxy
DiscoverInfoBuilder proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
Identity identity = new Identity("noproxy", proxyJID.toString(), "bytestreams");
proxyInfo.addIdentity(identity);
// return the proxy identity if proxy is queried
protocol.addResponse(proxyInfo.build(), Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
SmackException e = assertThrows(SmackException.class, () -> {
// start SOCKS5 Bytestream
byteStreamManager.establishSession(targetJID, sessionID);
fail("exception should be thrown");
});
protocol.verifyAll();
assertTrue(e.getMessage().contains("no SOCKS5 proxies available"));
/* retry to establish SOCKS5 Bytestream */
// add responses for service discovery again
protocol.addResponse(discoverInfo, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
e = assertThrows(SmackException.class, () -> {
// start SOCKS5 Bytestream
byteStreamManager.establishSession(targetJID, sessionID);
});
/*
* #verifyAll() tests if the number of requests and responses corresponds and should
* fail if the invalid proxy is queried again
*/
protocol.verifyAll();
assertTrue(e.getMessage().contains("no SOCKS5 proxies available"));
}
/**
* Invoking {@link Socks5BytestreamManager#establishSession(org.jxmpp.jid.Jid, String)} should fail if the
* target does not accept a SOCKS5 Bytestream. See <a
* href="http://xmpp.org/extensions/xep-0065.html#usecase-alternate">XEP-0065 Section 5.2 A2</a>
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException if Smack detected an exceptional situation.
* @throws XMPPException if an XMPP protocol error was received.
* @throws IOException if an I/O error occurred.
*/
@Test
public void shouldFailIfTargetDoesNotAcceptSocks5Bytestream() throws SmackException, InterruptedException, IOException, XMPPException {
final Protocol protocol = new Protocol();
final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, initiatorJID);
final String sessionID = "session_id_shouldFailIfTargetDoesNotAcceptSocks5Bytestream";
// get Socks5ByteStreamManager for connection
Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
byteStreamManager.setAnnounceLocalStreamHost(false);
/**
* create responses in the order they should be queried specified by the XEP-0065
* specification
*/
// build discover info that supports the SOCKS5 feature
DiscoverInfoBuilder discoverInfo = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
discoverInfo.addFeature(Bytestream.NAMESPACE);
// return that SOCKS5 is supported if target is queried
protocol.addResponse(discoverInfo.build(), Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build discover items containing a proxy item
DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
initiatorJID);
Item item = new Item(proxyJID);
discoverItems.addItem(item);
// return the proxy item if XMPP server is queried
protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build discover info for proxy containing information about being a SOCKS5 proxy
DiscoverInfoBuilder proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
Identity identity = new Identity("proxy", proxyJID.toString(), "bytestreams");
proxyInfo.addIdentity(identity);
// return the socks5 bytestream proxy identity if proxy is queried
protocol.addResponse(proxyInfo.build(), Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build a socks5 stream host info containing the address and the port of the
// proxy
Bytestream streamHostInfo = Socks5PacketUtils.createBytestreamResponse(proxyJID,
initiatorJID);
streamHostInfo.addStreamHost(proxyJID, proxyAddress, 7778);
// return stream host info if it is queried
protocol.addResponse(streamHostInfo, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build error packet to reject SOCKS5 Bytestream
StanzaError stanzaError = StanzaError.getBuilder(StanzaError.Condition.not_acceptable).build();
IQ rejectPacket = new ErrorIQ(stanzaError);
rejectPacket.setFrom(targetJID);
rejectPacket.setTo(initiatorJID);
// return error packet as response to the bytestream initiation
protocol.addResponse(rejectPacket, Verification.correspondingSenderReceiver,
Verification.requestTypeSET);
XMPPErrorException e = assertThrows(XMPPErrorException.class, () -> {
// start SOCKS5 Bytestream
byteStreamManager.establishSession(targetJID, sessionID);
});
protocol.verifyAll();
assertEquals(rejectPacket.getError(), e.getStanzaError());
}
/**
* Invoking {@link Socks5BytestreamManager#establishSession(org.jxmpp.jid.Jid, String)} should fail if the
* proxy used by target is invalid.
*
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException if Smack detected an exceptional situation.
* @throws XMPPException if an XMPP protocol error was received.
* @throws IOException if an I/O error occurred.
*/
@Test
public void shouldFailIfTargetUsesInvalidSocks5Proxy()
throws SmackException, InterruptedException, IOException, XMPPException {
final Protocol protocol = new Protocol();
final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, initiatorJID);
final String sessionID = "session_id_shouldFailIfTargetUsesInvalidSocks5Proxy";
// get Socks5ByteStreamManager for connection
Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
// TODO: It appears that it is not required to disable the local stream host for this unit test.
byteStreamManager.setAnnounceLocalStreamHost(false);
/**
* create responses in the order they should be queried specified by the XEP-0065
* specification
*/
// build discover info that supports the SOCKS5 feature
DiscoverInfoBuilder discoverInfo = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
discoverInfo.addFeature(Bytestream.NAMESPACE);
// return that SOCKS5 is supported if target is queried
protocol.addResponse(discoverInfo.build(), Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build discover items containing a proxy item
DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
initiatorJID);
Item item = new Item(proxyJID);
discoverItems.addItem(item);
// return the proxy item if XMPP server is queried
protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build discover info for proxy containing information about being a SOCKS5 proxy
DiscoverInfoBuilder proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
Identity identity = new Identity("proxy", proxyJID.toString(), "bytestreams");
proxyInfo.addIdentity(identity);
// return the socks5 bytestream proxy identity if proxy is queried
protocol.addResponse(proxyInfo.build(), Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build a socks5 stream host info containing the address and the port of the
// proxy
Bytestream streamHostInfo = Socks5PacketUtils.createBytestreamResponse(proxyJID,
initiatorJID);
streamHostInfo.addStreamHost(proxyJID, proxyAddress, 7778);
// return stream host info if it is queried
protocol.addResponse(streamHostInfo, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build used stream host response with unknown proxy
Bytestream streamHostUsedPacket = Socks5PacketUtils.createBytestreamResponse(targetJID,
initiatorJID);
streamHostUsedPacket.setSessionID(sessionID);
streamHostUsedPacket.setUsedHost(JidCreate.from("invalid.proxy"));
// return used stream host info as response to the bytestream initiation
protocol.addResponse(streamHostUsedPacket, Verification.correspondingSenderReceiver,
Verification.requestTypeSET);
SmackException e = assertThrows(SmackException.class, () -> {
// start SOCKS5 Bytestream
byteStreamManager.establishSession(targetJID, sessionID);
});
protocol.verifyAll();
assertTrue(e.getMessage().contains("Remote user responded with unknown host"));
}
/**
* Invoking {@link Socks5BytestreamManager#establishSession(org.jxmpp.jid.Jid, String)} should fail if
* initiator can not connect to the SOCKS5 proxy used by target.
*
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException if Smack detected an exceptional situation.
* @throws XMPPException if an XMPP protocol error was received.
* @throws XmppStringprepException if the provided string is invalid.
*/
@Test
public void shouldFailIfInitiatorCannotConnectToSocks5Proxy()
throws SmackException, InterruptedException, XMPPException, XmppStringprepException {
final Protocol protocol = new Protocol();
final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, initiatorJID);
final String sessionID = "session_id_shouldFailIfInitiatorCannotConnectToSocks5Proxy";
// TODO: The following two variables should be named initatorProxyJid and initiatorProxyAddress.
final DomainBareJid proxyJID = JidCreate.domainBareFrom("s5b-proxy.initiator.org");
// Use an TEST-NET-1 address from RFC 5737 to act as black hole.
final String proxyAddress = "192.0.2.1";
// get Socks5ByteStreamManager for connection
Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
byteStreamManager.setAnnounceLocalStreamHost(false);
byteStreamManager.setProxyConnectionTimeout(3000);
/**
* create responses in the order they should be queried specified by the XEP-0065
* specification
*/
// build discover info that supports the SOCKS5 feature
DiscoverInfoBuilder discoverInfoBuilder = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
discoverInfoBuilder.addFeature(Bytestream.NAMESPACE);
// return that SOCKS5 is supported if target is queried
protocol.addResponse(discoverInfoBuilder.build(), Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build discover items containing a proxy item
DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
initiatorJID);
Item item = new Item(proxyJID);
discoverItems.addItem(item);
// return the proxy item if XMPP server is queried
protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build discover info for proxy containing information about being a SOCKS5 proxy
DiscoverInfoBuilder proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
Identity identity = new Identity("proxy", proxyJID.toString(), "bytestreams");
proxyInfo.addIdentity(identity);
// return the socks5 bytestream proxy identity if proxy is queried
protocol.addResponse(proxyInfo.build(), Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build a socks5 stream host info containing the address and the port of the
// proxy
Bytestream streamHostInfo = Socks5PacketUtils.createBytestreamResponse(proxyJID,
initiatorJID);
streamHostInfo.addStreamHost(proxyJID, proxyAddress, 7778);
// return stream host info if it is queried
protocol.addResponse(streamHostInfo, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build used stream host response
Bytestream streamHostUsedPacket = Socks5PacketUtils.createBytestreamResponse(targetJID,
initiatorJID);
streamHostUsedPacket.setSessionID(sessionID);
streamHostUsedPacket.setUsedHost(proxyJID);
// return used stream host info as response to the bytestream initiation
protocol.addResponse(streamHostUsedPacket, new Verification<Bytestream, Bytestream>() {
@Override
public void verify(Bytestream request, Bytestream response) {
// verify SOCKS5 Bytestream request
assertEquals(response.getSessionID(), request.getSessionID());
assertEquals(1, request.getStreamHosts().size());
StreamHost streamHost = (StreamHost) request.getStreamHosts().toArray()[0];
assertEquals(response.getUsedHost().getJID(), streamHost.getJID());
}
}, Verification.correspondingSenderReceiver, Verification.requestTypeSET);
IOException e = assertThrows(IOException.class, () -> {
// start SOCKS5 Bytestream
byteStreamManager.establishSession(targetJID, sessionID);
});
// initiator can't connect to proxy because it is not running
protocol.verifyAll();
Throwable actualCause = e.getCause();
assertEquals(TimeoutException.class, actualCause.getClass(), "Unexpected throwable: " + actualCause + '.' + ExceptionUtil.getStackTrace(actualCause));
}
/**
* Invoking {@link Socks5BytestreamManager#establishSession(org.jxmpp.jid.Jid, String)} should successfully
* negotiate and return a SOCKS5 Bytestream connection.
*
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException if Smack detected an exceptional situation.
* @throws XMPPException if an XMPP protocol error was received.
* @throws IOException if an I/O error occurred.
*/
@Test
public void shouldNegotiateSocks5BytestreamAndTransferData()
throws SmackException, InterruptedException, IOException, XMPPException {
final Protocol protocol = new Protocol();
final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, initiatorJID);
final String sessionID = "session_id_shouldNegotiateSocks5BytestreamAndTransferData";
// get Socks5ByteStreamManager for connection
Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
byteStreamManager.setAnnounceLocalStreamHost(false);
/**
* create responses in the order they should be queried specified by the XEP-0065
* specification
*/
// build discover info that supports the SOCKS5 feature
DiscoverInfoBuilder discoverInfo = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
discoverInfo.addFeature(Bytestream.NAMESPACE);
// return that SOCKS5 is supported if target is queried
protocol.addResponse(discoverInfo.build(), Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build discover items containing a proxy item
DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
initiatorJID);
Item item = new Item(proxyJID);
discoverItems.addItem(item);
// return the proxy item if XMPP server is queried
protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build discover info for proxy containing information about being a SOCKS5 proxy
DiscoverInfoBuilder proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
Identity identity = new Identity("proxy", proxyJID.toString(), "bytestreams");
proxyInfo.addIdentity(identity);
// return the socks5 bytestream proxy identity if proxy is queried
protocol.addResponse(proxyInfo.build(), Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build a socks5 stream host info containing the address and the port of the
// proxy
ServerSocket proxyServerSocket = NetworkUtil.getSocketOnLoopback();
Bytestream streamHostInfo = Socks5PacketUtils.createBytestreamResponse(proxyJID,
initiatorJID);
streamHostInfo.addStreamHost(proxyJID, proxyAddress, proxyServerSocket.getLocalPort());
// return stream host info if it is queried
protocol.addResponse(streamHostInfo, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build used stream host response
Bytestream streamHostUsedPacket = Socks5PacketUtils.createBytestreamResponse(targetJID,
initiatorJID);
streamHostUsedPacket.setSessionID(sessionID);
streamHostUsedPacket.setUsedHost(proxyJID);
// return used stream host info as response to the bytestream initiation
protocol.addResponse(streamHostUsedPacket, new Verification<Bytestream, Bytestream>() {
@Override
public void verify(Bytestream request, Bytestream response) {
assertEquals(response.getSessionID(), request.getSessionID());
assertEquals(1, request.getStreamHosts().size());
StreamHost streamHost = (StreamHost) request.getStreamHosts().toArray()[0];
assertEquals(response.getUsedHost().getJID(), streamHost.getJID());
}
}, Verification.correspondingSenderReceiver, Verification.requestTypeSET);
// build response to proxy activation
IQ activationResponse = Socks5PacketUtils.createActivationConfirmation(proxyJID,
initiatorJID);
// return proxy activation response if proxy should be activated
protocol.addResponse(activationResponse, new Verification<Bytestream, IQ>() {
@Override
public void verify(Bytestream request, IQ response) {
assertEquals(targetJID, request.getToActivate().getTarget());
}
}, Verification.correspondingSenderReceiver, Verification.requestTypeSET);
// start a local SOCKS5 proxy
try (Socks5TestProxy socks5Proxy = new Socks5TestProxy(proxyServerSocket)) {
// create digest to get the socket opened by target
String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);
// finally call the method that should be tested
OutputStream outputStream = byteStreamManager.establishSession(targetJID, sessionID).getOutputStream();
// test the established bytestream
InputStream inputStream = socks5Proxy.getSocket(digest).getInputStream();
byte[] data = new byte[] { 1, 2, 3 };
outputStream.write(data);
byte[] result = new byte[3];
inputStream.read(result);
assertArrayEquals(data, result);
}
protocol.verifyAll();
}
/**
* If multiple network addresses are added to the local SOCKS5 proxy, all of them should be
* contained in the SOCKS5 Bytestream request.
*
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException if Smack detected an exceptional situation.
* @throws IOException if an I/O error occurred.
* @throws XMPPException if an XMPP protocol error was received.
* @throws TimeoutException if there was a timeout.
*/
@Test
public void shouldUseMultipleAddressesForLocalSocks5Proxy()
throws SmackException, InterruptedException, IOException, TimeoutException, XMPPException {
final Protocol protocol = new Protocol();
final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, initiatorJID);
final String sessionID = "session_id_shouldUseMultipleAddressesForLocalSocks5Proxy";
// start a local SOCKS5 proxy
Socks5Proxy socks5Proxy = new Socks5Proxy();
socks5Proxy.start();
try {
assertTrue(socks5Proxy.isRunning());
// get Socks5ByteStreamManager for connection
Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
/**
* create responses in the order they should be queried specified by the XEP-0065
* specification
*/
// build discover info that supports the SOCKS5 feature
DiscoverInfoBuilder discoverInfo = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
discoverInfo.addFeature(Bytestream.NAMESPACE);
// return that SOCKS5 is supported if target is queried
protocol.addResponse(discoverInfo.build(), Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build discover items containing no proxy item
DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
initiatorJID);
// return the discover item if XMPP server is queried
protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build used stream host response
Bytestream streamHostUsedPacket = Socks5PacketUtils.createBytestreamResponse(targetJID,
initiatorJID);
streamHostUsedPacket.setSessionID(sessionID);
streamHostUsedPacket.setUsedHost(initiatorJID); // local proxy used
final String secondStreamHostIp = "192.0.0.1";
// return used stream host info as response to the bytestream initiation
protocol.addResponse(streamHostUsedPacket, new Verification<Bytestream, Bytestream>() {
@Override
public void verify(Bytestream request, Bytestream response) {
assertEquals(response.getSessionID(), request.getSessionID());
List<StreamHost> streamHosts = request.getStreamHosts();
StreamHost streamHost1 = streamHosts.get(0);
assertEquals(response.getUsedHost().getJID(), streamHost1.getJID());
// Get the last stream host. Note that there may be multiple, but since this unit test added
// secondStreamHostIp as last, it should also be the last entry since the API contract assures that
// the order is preserved.
StreamHost streamHost2 = streamHosts.get(streamHosts.size() - 1);
assertEquals(response.getUsedHost().getJID(), streamHost2.getJID());
assertEquals(secondStreamHostIp, streamHost2.getAddress().toString());
}
}, Verification.correspondingSenderReceiver, Verification.requestTypeSET);
// create digest to get the socket opened by target
String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);
// connect to proxy as target
socks5Proxy.addTransfer(digest);
StreamHost streamHost = new StreamHost(targetJID,
socks5Proxy.getLocalAddresses().get(0),
socks5Proxy.getPort());
Socks5Client socks5Client = new Socks5Client(streamHost, digest);
InputStream inputStream = socks5Client.getSocket(10000).getInputStream();
// add another network address before establishing SOCKS5 Bytestream
socks5Proxy.addLocalAddress(InetAddress.getByName(secondStreamHostIp));
// finally call the method that should be tested
OutputStream outputStream = byteStreamManager.establishSession(targetJID, sessionID).getOutputStream();
// test the established bytestream
byte[] data = new byte[] { 1, 2, 3 };
outputStream.write(data);
byte[] result = new byte[3];
inputStream.read(result);
assertArrayEquals(data, result);
protocol.verifyAll();
} finally {
socks5Proxy.stop();
}
}
/**
* Invoking {@link Socks5BytestreamManager#establishSession(org.jxmpp.jid.Jid, String)} the first time
* should successfully negotiate a SOCKS5 Bytestream via the second SOCKS5 proxy and should
* prioritize this proxy for a second SOCKS5 Bytestream negotiation.
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException if Smack detected an exceptional situation.
* @throws XMPPException if an XMPP protocol error was received.
* @throws IOException if an I/O error occurred.
*
*/
@Test
public void shouldPrioritizeSecondSocks5ProxyOnSecondAttempt() throws SmackException, InterruptedException, IOException, XMPPException {
final Protocol protocol = new Protocol();
final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, initiatorJID);
final String sessionID = "session_id_shouldPrioritizeSecondSocks5ProxyOnSecondAttempt";
// get Socks5ByteStreamManager for connection
Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
byteStreamManager.setAnnounceLocalStreamHost(false);
assertTrue(byteStreamManager.isProxyPrioritizationEnabled());
Verification<Bytestream, Bytestream> streamHostUsedVerification1 = new Verification<Bytestream, Bytestream>() {
@Override
public void verify(Bytestream request, Bytestream response) {
assertEquals(response.getSessionID(), request.getSessionID());
assertEquals(2, request.getStreamHosts().size());
// verify that the used stream host is the second in list
StreamHost streamHost = (StreamHost) request.getStreamHosts().toArray()[1];
assertEquals(response.getUsedHost().getJID(), streamHost.getJID());
}
};
// start a local SOCKS5 proxy
try (Socks5TestProxy socks5Proxy = new Socks5TestProxy()) {
createResponses(protocol, sessionID, streamHostUsedVerification1, socks5Proxy);
// create digest to get the socket opened by target
String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);
// call the method that should be tested
OutputStream outputStream = byteStreamManager.establishSession(targetJID, sessionID).getOutputStream();
// test the established bytestream
InputStream inputStream = socks5Proxy.getSocket(digest).getInputStream();
byte[] data = new byte[] { 1, 2, 3 };
outputStream.write(data);
byte[] result = new byte[3];
inputStream.read(result);
assertArrayEquals(data, result);
protocol.verifyAll();
Verification<Bytestream, Bytestream> streamHostUsedVerification2 = new Verification<Bytestream, Bytestream>() {
@Override
public void verify(Bytestream request, Bytestream response) {
assertEquals(response.getSessionID(), request.getSessionID());
assertEquals(2, request.getStreamHosts().size());
// verify that the used stream host is the first in list
StreamHost streamHost = (StreamHost) request.getStreamHosts().toArray()[0];
assertEquals(response.getUsedHost().getJID(), streamHost.getJID());
}
};
createResponses(protocol, sessionID, streamHostUsedVerification2, socks5Proxy);
// call the method that should be tested again
outputStream = byteStreamManager.establishSession(targetJID, sessionID).getOutputStream();
// test the established bytestream
inputStream = socks5Proxy.getSocket(digest).getInputStream();
outputStream.write(data);
inputStream.read(result);
assertArrayEquals(data, result);
protocol.verifyAll();
}
}
/**
* Invoking {@link Socks5BytestreamManager#establishSession(org.jxmpp.jid.Jid, String)} the first time
* should successfully negotiate a SOCKS5 Bytestream via the second SOCKS5 proxy. The second
* negotiation should run in the same manner if prioritization is disabled.
*
* @throws IOException if an I/O error occurred.
* @throws InterruptedException if the calling thread was interrupted.
* @throws SmackException if Smack detected an exceptional situation.
* @throws XMPPException if an XMPP protocol error was received.
*
*/
@Test
public void shouldNotPrioritizeSocks5ProxyIfPrioritizationDisabled() throws IOException, SmackException, InterruptedException, XMPPException {
final Protocol protocol = new Protocol();
final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, initiatorJID);
final String sessionID = "session_id_shouldNotPrioritizeSocks5ProxyIfPrioritizationDisabled";
// get Socks5ByteStreamManager for connection
Socks5BytestreamManager byteStreamManager = Socks5BytestreamManager.getBytestreamManager(connection);
byteStreamManager.setAnnounceLocalStreamHost(false);
byteStreamManager.setProxyPrioritizationEnabled(false);
assertFalse(byteStreamManager.isProxyPrioritizationEnabled());
Verification<Bytestream, Bytestream> streamHostUsedVerification = new Verification<Bytestream, Bytestream>() {
@Override
public void verify(Bytestream request, Bytestream response) {
assertEquals(response.getSessionID(), request.getSessionID());
assertEquals(2, request.getStreamHosts().size());
// verify that the used stream host is the second in list
StreamHost streamHost = (StreamHost) request.getStreamHosts().toArray()[1];
assertEquals(response.getUsedHost().getJID(), streamHost.getJID());
}
};
// start a local SOCKS5 proxy
try (Socks5TestProxy socks5Proxy = new Socks5TestProxy()) {
createResponses(protocol, sessionID, streamHostUsedVerification, socks5Proxy);
// create digest to get the socket opened by target
String digest = Socks5Utils.createDigest(sessionID, initiatorJID, targetJID);
// call the method that should be tested
OutputStream outputStream = byteStreamManager.establishSession(targetJID, sessionID).getOutputStream();
// test the established bytestream
InputStream inputStream = socks5Proxy.getSocket(digest).getInputStream();
byte[] data = new byte[] { 1, 2, 3 };
outputStream.write(data);
byte[] result = new byte[3];
inputStream.read(result);
assertArrayEquals(data, result);
protocol.verifyAll();
createResponses(protocol, sessionID, streamHostUsedVerification, socks5Proxy);
// call the method that should be tested again
outputStream = byteStreamManager.establishSession(targetJID, sessionID).getOutputStream();
// test the established bytestream
inputStream = socks5Proxy.getSocket(digest).getInputStream();
outputStream.write(data);
inputStream.read(result);
assertArrayEquals(data, result);
}
protocol.verifyAll();
}
private static void createResponses(Protocol protocol, String sessionID,
Verification<Bytestream, Bytestream> streamHostUsedVerification, Socks5TestProxy socks5TestProxy)
throws XmppStringprepException {
// build discover info that supports the SOCKS5 feature
DiscoverInfoBuilder discoverInfo = Socks5PacketUtils.createDiscoverInfo(targetJID, initiatorJID);
discoverInfo.addFeature(Bytestream.NAMESPACE);
// return that SOCKS5 is supported if target is queried
protocol.addResponse(discoverInfo.build(), Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build discover items containing a proxy item
DiscoverItems discoverItems = Socks5PacketUtils.createDiscoverItems(xmppServer,
initiatorJID);
discoverItems.addItem(new Item(JidCreate.from("proxy2.xmpp-server")));
discoverItems.addItem(new Item(proxyJID));
// return the proxy item if XMPP server is queried
protocol.addResponse(discoverItems, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
/*
* build discover info for proxy "proxy2.xmpp-server" containing information about being a
* SOCKS5 proxy
*/
DiscoverInfoBuilder proxyInfo1 = Socks5PacketUtils.createDiscoverInfo(JidCreate.from("proxy2.xmpp-server"),
initiatorJID);
Identity identity1 = new Identity("proxy", "proxy2.xmpp-server", "bytestreams");
proxyInfo1.addIdentity(identity1);
// return the SOCKS5 bytestream proxy identity if proxy is queried
protocol.addResponse(proxyInfo1.build(), Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build discover info for proxy containing information about being a SOCKS5 proxy
DiscoverInfoBuilder proxyInfo2 = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
Identity identity2 = new Identity("proxy", proxyJID.toString(), "bytestreams");
proxyInfo2.addIdentity(identity2);
// return the SOCKS5 bytestream proxy identity if proxy is queried
protocol.addResponse(proxyInfo2.build(), Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
/*
* build a SOCKS5 stream host info for "proxy2.xmpp-server" containing the address and the
* port of the proxy
*/
Bytestream streamHostInfo1 = Socks5PacketUtils.createBytestreamResponse(
JidCreate.from("proxy2.xmpp-server"), initiatorJID);
streamHostInfo1.addStreamHost(JidCreate.from("proxy2.xmpp-server"), proxyAddress, socks5TestProxy.getPort());
// return stream host info if it is queried
protocol.addResponse(streamHostInfo1, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build a SOCKS5 stream host info containing the address and the port of the proxy
Bytestream streamHostInfo2 = Socks5PacketUtils.createBytestreamResponse(proxyJID,
initiatorJID);
streamHostInfo2.addStreamHost(proxyJID, proxyAddress, socks5TestProxy.getPort());
// return stream host info if it is queried
protocol.addResponse(streamHostInfo2, Verification.correspondingSenderReceiver,
Verification.requestTypeGET);
// build used stream host response
Bytestream streamHostUsedPacket = Socks5PacketUtils.createBytestreamResponse(targetJID,
initiatorJID);
streamHostUsedPacket.setSessionID(sessionID);
streamHostUsedPacket.setUsedHost(proxyJID);
// return used stream host info as response to the bytestream initiation
protocol.addResponse(streamHostUsedPacket, streamHostUsedVerification,
Verification.correspondingSenderReceiver, Verification.requestTypeSET);
// build response to proxy activation
IQ activationResponse = Socks5PacketUtils.createActivationConfirmation(proxyJID,
initiatorJID);
// return proxy activation response if proxy should be activated
protocol.addResponse(activationResponse, new Verification<Bytestream, IQ>() {
@Override
public void verify(Bytestream request, IQ response) {
assertEquals(targetJID, request.getToActivate().getTarget());
}
}, Verification.correspondingSenderReceiver, Verification.requestTypeSET);
}
}