/** * * 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.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.StanzaError; import org.jivesoftware.smack.test.util.NetworkUtil; import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream; import org.jivesoftware.util.ConnectionUtils; import org.jivesoftware.util.Protocol; 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; /** * Tests for the Socks5BytestreamRequest class. * * @author Henning Staib */ public class Socks5ByteStreamRequestTest { // 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 proxyJID = JidTestUtil.MUC_EXAMPLE_ORG; private static final String proxyAddress = "127.0.0.1"; private static final String sessionID = "session_id"; /** * 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 { final Protocol protocol = new Protocol(); final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, targetJID); assertThrows(Socks5Exception.NoSocks5StreamHostsProvided.class, () -> { // 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(); }); // verify targets response assertEquals(1, protocol.getRequests().size()); Stanza targetResponse = protocol.getRequests().remove(0); assertTrue(IQ.class.isInstance(targetResponse)); assertEquals(initiatorJID, targetResponse.getTo()); assertEquals(IQ.Type.error, ((IQ) targetResponse).getType()); assertEquals(StanzaError.Condition.item_not_found, 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 if an exception occurs. */ @Test public void shouldFailIfRequestHasInvalidStreamHosts() throws Exception { final Protocol protocol = new Protocol(); final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, targetJID); assertThrows(Socks5Exception.CouldNotConnectToAnyProvidedSocks5Host.class, () -> { // 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(); }); // verify targets response assertEquals(1, protocol.getRequests().size()); Stanza targetResponse = protocol.getRequests().remove(0); assertTrue(IQ.class.isInstance(targetResponse)); assertEquals(initiatorJID, targetResponse.getTo()); assertEquals(IQ.Type.error, ((IQ) targetResponse).getType()); assertEquals(StanzaError.Condition.item_not_found, 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 { final Protocol protocol = new Protocol(); final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, targetJID); // build SOCKS5 Bytestream initialization request Bytestream bytestreamInitialization = Socks5PacketUtils.createBytestreamInitiation( initiatorJID, targetJID, sessionID); // Add an unreachable stream host. bytestreamInitialization.addStreamHost(JidCreate.from("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++) { assertThrows(Socks5Exception.CouldNotConnectToAnyProvidedSocks5Host.class, () -> { // 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(); }); // verify targets response assertEquals(1, protocol.getRequests().size()); Stanza targetResponse = protocol.getRequests().remove(0); assertTrue(IQ.class.isInstance(targetResponse)); assertEquals(initiatorJID, targetResponse.getTo()); assertEquals(IQ.Type.error, ((IQ) targetResponse).getType()); assertEquals(StanzaError.Condition.item_not_found, targetResponse.getError().getCondition()); } // create test data for stream byte[] data = new byte[] { 1, 2, 3 }; try (Socks5TestProxy socks5Proxy = new Socks5TestProxy()) { assertTrue(socks5Proxy.isRunning()); // add a valid SOCKS5 proxy bytestreamInitialization.addStreamHost(proxyJID, proxyAddress, socks5Proxy.getPort()); // 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()); Stanza 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 { final Protocol protocol = new Protocol(); final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, targetJID); // build SOCKS5 Bytestream initialization request Bytestream bytestreamInitialization = Socks5PacketUtils.createBytestreamInitiation( initiatorJID, targetJID, sessionID); bytestreamInitialization.addStreamHost(JidCreate.from("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++) { assertThrows(Socks5Exception.CouldNotConnectToAnyProvidedSocks5Host.class, () -> { // build SOCKS5 Bytestream request with the bytestream initialization Socks5BytestreamRequest byteStreamRequest = new Socks5BytestreamRequest( byteStreamManager, bytestreamInitialization); // set timeouts byteStreamRequest.setTotalConnectTimeout(600); byteStreamRequest.setMinimumConnectTimeout(300); byteStreamRequest.setConnectFailureThreshold(0); // accept the stream (this is the call that is tested here) byteStreamRequest.accept(); }); // verify targets response assertEquals(1, protocol.getRequests().size()); Stanza targetResponse = protocol.getRequests().remove(0); assertTrue(IQ.class.isInstance(targetResponse)); assertEquals(initiatorJID, targetResponse.getTo()); assertEquals(IQ.Type.error, ((IQ) targetResponse).getType()); assertEquals(StanzaError.Condition.item_not_found, targetResponse.getError().getCondition()); } } /** * 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 { final Protocol protocol = new Protocol(); final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, targetJID); // start a local SOCKS5 proxy try (Socks5TestProxy socks5Proxy = new Socks5TestProxy()) { // create a fake SOCKS5 proxy that doesn't respond to a request ServerSocket unresponsiveSocks5Socket = NetworkUtil.getSocketOnLoopback(); try { // build SOCKS5 Bytestream initialization request Bytestream bytestreamInitialization = Socks5PacketUtils.createBytestreamInitiation( initiatorJID, targetJID, sessionID); bytestreamInitialization.addStreamHost(proxyJID, proxyAddress, unresponsiveSocks5Socket.getLocalPort()); bytestreamInitialization.addStreamHost(proxyJID, proxyAddress, socks5Proxy.getPort()); // 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 = unresponsiveSocks5Socket.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()); Stanza 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()); } finally { unresponsiveSocks5Socket.close(); } } } /** * Accepting the SOCKS5 Bytestream request should be successfully. * * @throws Exception should not happen */ @Test public void shouldAcceptSocks5BytestreamRequestAndReceiveData() throws Exception { final Protocol protocol = new Protocol(); final XMPPConnection connection = ConnectionUtils.createMockedConnection(protocol, targetJID); // start a local SOCKS5 proxy try (Socks5TestProxy socks5Proxy = new Socks5TestProxy()) { // build SOCKS5 Bytestream initialization request Bytestream bytestreamInitialization = Socks5PacketUtils.createBytestreamInitiation( initiatorJID, targetJID, sessionID); bytestreamInitialization.addStreamHost(proxyJID, proxyAddress, socks5Proxy.getPort()); // 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()); Stanza 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()); } } }