From 2a4d110b22378fe21730dd4db295e063caa3d789 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Wed, 8 May 2019 11:05:47 +0200 Subject: [PATCH] Add Socks5Exception and improve SOCKS5 bytestream exception messages The exception message now also contains the stream hosts and their exception. --- .../smackx/bytestreams/BytestreamRequest.java | 12 +-- .../socks5/Socks5BytestreamRequest.java | 47 ++++++++-- .../bytestreams/socks5/Socks5Exception.java | 87 +++++++++++++++++++ .../socks5/Socks5ByteStreamRequestTest.java | 45 ++-------- 4 files changed, 142 insertions(+), 49 deletions(-) create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Exception.java diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/BytestreamRequest.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/BytestreamRequest.java index 5c2a81d3e..059e11187 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/BytestreamRequest.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/BytestreamRequest.java @@ -16,13 +16,13 @@ */ package org.jivesoftware.smackx.bytestreams; -import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamRequest; import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamRequest; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5Exception.CouldNotConnectToAnyProvidedSocks5Host; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5Exception.NoSocks5StreamHostsProvided; import org.jxmpp.jid.Jid; @@ -57,10 +57,12 @@ public interface BytestreamRequest { * @throws XMPPErrorException if an error occurred while accepting the bytestream request * @throws InterruptedException if the thread was interrupted while waiting in a blocking * operation - * @throws NoResponseException - * @throws SmackException + * @throws NotConnectedException + * @throws CouldNotConnectToAnyProvidedSocks5Host + * @throws NoSocks5StreamHostsProvided */ - BytestreamSession accept() throws InterruptedException, XMPPErrorException, SmackException; + BytestreamSession accept() throws InterruptedException, XMPPErrorException, CouldNotConnectToAnyProvidedSocks5Host, + NotConnectedException, NoSocks5StreamHostsProvided; /** * Rejects the bytestream request by sending a reject error to the initiator. diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamRequest.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamRequest.java index cd226f1d5..92e17a381 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamRequest.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamRequest.java @@ -19,6 +19,8 @@ package org.jivesoftware.smackx.bytestreams.socks5; import java.io.IOException; import java.net.Socket; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.TimeoutException; import org.jivesoftware.smack.SmackException; @@ -29,6 +31,8 @@ import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.StanzaError; import org.jivesoftware.smackx.bytestreams.BytestreamRequest; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5Exception.CouldNotConnectToAnyProvidedSocks5Host; +import org.jivesoftware.smackx.bytestreams.socks5.Socks5Exception.NoSocks5StreamHostsProvided; import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream; import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost; @@ -197,15 +201,19 @@ public class Socks5BytestreamRequest implements BytestreamRequest { * @return the socket to send/receive data * @throws InterruptedException if the current thread was interrupted while waiting * @throws XMPPErrorException - * @throws SmackException + * @throws NotConnectedException + * @throws CouldNotConnectToAnyProvidedSocks5Host + * @throws NoSocks5StreamHostsProvided */ @Override - public Socks5BytestreamSession accept() throws InterruptedException, XMPPErrorException, SmackException { + public Socks5BytestreamSession accept() throws InterruptedException, XMPPErrorException, + CouldNotConnectToAnyProvidedSocks5Host, NotConnectedException, NoSocks5StreamHostsProvided { Collection streamHosts = this.bytestreamRequest.getStreamHosts(); + Map streamHostsExceptions = new HashMap<>(); // throw exceptions if request contains no stream hosts if (streamHosts.size() == 0) { - cancelRequest(); + cancelRequest(streamHostsExceptions); } StreamHost selectedHost = null; @@ -245,6 +253,7 @@ public class Socks5BytestreamRequest implements BytestreamRequest { } catch (TimeoutException | IOException | SmackException | XMPPException e) { + streamHostsExceptions.put(streamHost, e); incrementConnectionFailures(address); } @@ -252,7 +261,7 @@ public class Socks5BytestreamRequest implements BytestreamRequest { // throw exception if connecting to all SOCKS5 proxies failed if (selectedHost == null || socket == null) { - cancelRequest(); + cancelRequest(streamHostsExceptions); } // send used-host confirmation @@ -277,16 +286,38 @@ public class Socks5BytestreamRequest implements BytestreamRequest { /** * Cancels the SOCKS5 Bytestream request by sending an error to the initiator and building a * XMPP exception. - * @throws XMPPErrorException + * + * @param streamHosts the stream hosts. * @throws NotConnectedException * @throws InterruptedException + * @throws CouldNotConnectToAnyProvidedSocks5Host as expected result. + * @throws NoSocks5StreamHostsProvided */ - private void cancelRequest() throws XMPPErrorException, NotConnectedException, InterruptedException { - String errorMessage = "Could not establish socket with any provided host"; + private void cancelRequest(Map streamHostsExceptions) + throws NotConnectedException, InterruptedException, CouldNotConnectToAnyProvidedSocks5Host, NoSocks5StreamHostsProvided { + final Socks5Exception.NoSocks5StreamHostsProvided noHostsProvidedException; + final Socks5Exception.CouldNotConnectToAnyProvidedSocks5Host couldNotConnectException; + final String errorMessage; + + if (streamHostsExceptions.isEmpty()) { + noHostsProvidedException = new Socks5Exception.NoSocks5StreamHostsProvided(); + couldNotConnectException = null; + errorMessage = noHostsProvidedException.getMessage(); + } else { + noHostsProvidedException = null; + couldNotConnectException = Socks5Exception.CouldNotConnectToAnyProvidedSocks5Host.construct(streamHostsExceptions); + errorMessage = couldNotConnectException.getMessage(); + } + StanzaError.Builder error = StanzaError.from(StanzaError.Condition.item_not_found, errorMessage); IQ errorIQ = IQ.createErrorResponse(this.bytestreamRequest, error); this.manager.getConnection().sendStanza(errorIQ); - throw new XMPPErrorException(errorIQ, error.build()); + + if (noHostsProvidedException != null) { + throw noHostsProvidedException; + } else { + throw couldNotConnectException; + } } /** diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Exception.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Exception.java new file mode 100644 index 000000000..c4aa4fe17 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5Exception.java @@ -0,0 +1,87 @@ +/** + * + * Copyright 2019 Florian Schmaus. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.bytestreams.socks5; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; + +import org.jivesoftware.smack.SmackException; + +import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost; + +public abstract class Socks5Exception extends SmackException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + protected Socks5Exception(String message) { + super(message); + } + + public static final class NoSocks5StreamHostsProvided extends Socks5Exception { + /** + * + */ + private static final long serialVersionUID = 1L; + + NoSocks5StreamHostsProvided() { + super("No SOCKS5 stream hosts provided."); + } + } + + public static final class CouldNotConnectToAnyProvidedSocks5Host extends Socks5Exception { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final Map streamHostsExceptions; + + private CouldNotConnectToAnyProvidedSocks5Host(String message, Map streamHostsExceptions) { + super(message); + this.streamHostsExceptions = Collections.unmodifiableMap(streamHostsExceptions); + } + + public Map getStreamHostsExceptions() { + return streamHostsExceptions; + } + + static CouldNotConnectToAnyProvidedSocks5Host construct(Map streamHostsExceptions) { + assert !streamHostsExceptions.isEmpty(); + + StringBuilder sb = new StringBuilder(256); + sb.append("Could not establish socket with any provided SOCKS5 stream host."); + Iterator it = streamHostsExceptions.keySet().iterator(); + while (it.hasNext()) { + StreamHost streamHost = it.next(); + Exception exception = streamHostsExceptions.get(streamHost); + + sb.append(' ').append(streamHost).append(" Exception: '").append(exception).append('\''); + if (it.hasNext()) { + sb.append(','); + } + } + + String message = sb.toString(); + return new CouldNotConnectToAnyProvidedSocks5Host(message, streamHostsExceptions); + } + } +} diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamRequestTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamRequestTest.java index 37098a646..5f3b373b9 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamRequestTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamRequestTest.java @@ -20,7 +20,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.InputStream; import java.io.OutputStream; @@ -30,7 +30,6 @@ import java.net.Socket; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.StanzaError; @@ -90,9 +89,7 @@ public class Socks5ByteStreamRequestTest { */ @Test public void shouldFailIfRequestHasNoStreamHosts() throws Exception { - - try { - + assertThrows(Socks5Exception.NoSocks5StreamHostsProvided.class, () -> { // build SOCKS5 Bytestream initialization request with no SOCKS5 proxies Bytestream bytestreamInitialization = Socks5PacketUtils.createBytestreamInitiation( initiatorJID, targetJID, sessionID); @@ -106,12 +103,7 @@ public class Socks5ByteStreamRequestTest { // accept the stream (this is the call that is tested here) byteStreamRequest.accept(); - - fail("exception should be thrown"); - } - catch (XMPPErrorException e) { - assertTrue(e.getStanzaError().getDescriptiveText("en").contains("Could not establish socket with any provided host")); - } + }); // verify targets response assertEquals(1, protocol.getRequests().size()); @@ -132,9 +124,7 @@ public class Socks5ByteStreamRequestTest { */ @Test public void shouldFailIfRequestHasInvalidStreamHosts() throws Exception { - - try { - + assertThrows(Socks5Exception.CouldNotConnectToAnyProvidedSocks5Host.class, () -> { // build SOCKS5 Bytestream initialization request Bytestream bytestreamInitialization = Socks5PacketUtils.createBytestreamInitiation( initiatorJID, targetJID, sessionID); @@ -150,12 +140,7 @@ public class Socks5ByteStreamRequestTest { // accept the stream (this is the call that is tested here) byteStreamRequest.accept(); - - fail("exception should be thrown"); - } - catch (XMPPErrorException e) { - assertTrue(e.getStanzaError().getDescriptiveText("en").contains("Could not establish socket with any provided host")); - } + }); // verify targets response assertEquals(1, protocol.getRequests().size()); @@ -186,7 +171,7 @@ public class Socks5ByteStreamRequestTest { // try to connect several times for (int i = 0; i < 2; i++) { - try { + assertThrows(Socks5Exception.CouldNotConnectToAnyProvidedSocks5Host.class, () -> { // build SOCKS5 Bytestream request with the bytestream initialization Socks5BytestreamRequest byteStreamRequest = new Socks5BytestreamRequest( byteStreamManager, bytestreamInitialization); @@ -197,13 +182,7 @@ public class Socks5ByteStreamRequestTest { // accept the stream (this is the call that is tested here) byteStreamRequest.accept(); - - fail("exception should be thrown"); - } - catch (XMPPErrorException e) { - assertTrue(e.getStanzaError().getDescriptiveText("en").contains( - "Could not establish socket with any provided host")); - } + }); // verify targets response assertEquals(1, protocol.getRequests().size()); @@ -278,7 +257,7 @@ public class Socks5ByteStreamRequestTest { // try to connect several times for (int i = 0; i < 10; i++) { - try { + assertThrows(Socks5Exception.CouldNotConnectToAnyProvidedSocks5Host.class, () -> { // build SOCKS5 Bytestream request with the bytestream initialization Socks5BytestreamRequest byteStreamRequest = new Socks5BytestreamRequest( byteStreamManager, bytestreamInitialization); @@ -289,13 +268,7 @@ public class Socks5ByteStreamRequestTest { // accept the stream (this is the call that is tested here) byteStreamRequest.accept(); - - fail("exception should be thrown"); - } - catch (XMPPErrorException e) { - assertTrue(e.getStanzaError().getDescriptiveText("en").contains( - "Could not establish socket with any provided host")); - } + }); // verify targets response assertEquals(1, protocol.getRequests().size());