/** * * 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.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.jupiter.api.Assertions.assertThrows; 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.util.ExceptionUtil; import org.jivesoftware.smack.util.NetworkUtil; 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.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 XEP-0065 Section 5.2 A2 * @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() { @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("Unexpected throwable: " + actualCause + '.' + ExceptionUtil.getStackTrace(actualCause), TimeoutException.class, actualCause.getClass()); } /** * 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() { @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() { @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() { @Override public void verify(Bytestream request, Bytestream response) { assertEquals(response.getSessionID(), request.getSessionID()); List 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 streamHostUsedVerification1 = new Verification() { @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 streamHostUsedVerification2 = new Verification() { @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 streamHostUsedVerification = new Verification() { @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 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() { @Override public void verify(Bytestream request, IQ response) { assertEquals(targetJID, request.getToActivate().getTarget()); } }, Verification.correspondingSenderReceiver, Verification.requestTypeSET); } }