From c5a546554b9300adec41bab86d618a973bafc2a1 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 25 Jan 2021 19:51:45 +0100 Subject: [PATCH] Rework WebSocket code Related to SMACK-835. --- .../smack/AbstractXMPPConnection.java | 45 +++-- .../org/jivesoftware/smack/SmackFuture.java | 15 +- .../ModularXmppClientToServerConnection.java | 25 +-- ...ClientToServerConnectionConfiguration.java | 6 +- ...entToServerConnectionModuleDescriptor.java | 5 +- .../smack/c2s/StreamOpenAndCloseFactory.java | 8 +- .../c2s/XmppClientToServerTransport.java | 4 +- ...rXmppClientToServerConnectionInternal.java | 19 +- .../org/jivesoftware/smack/fsm/State.java | 23 ++- .../smack/fsm/StateTransitionResult.java | 10 +- .../smack/packet/AbstractStreamOpen.java | 3 +- .../smack/packet/StreamClose.java | 8 +- .../jivesoftware/smack/packet/StreamOpen.java | 4 +- smack-java8-full/build.gradle | 3 +- .../smack/full/WebSocketConnectionTest.java | 110 ++++++++++++ .../igniterealtime/smack/smackrepl/Nio.java | 36 +--- .../smack/smackrepl/WebSocketConnection.java | 47 +++-- .../smack/smackrepl/XmppTools.java | 46 ++++- .../smack/tcp/XMPPTCPConnection.java | 8 +- .../smack/tcp/XmppTcpTransportModule.java | 32 ++-- .../websocket/okhttp/LoggingInterceptor.java | 34 ++-- .../websocket/okhttp/OkHttpWebSocket.java | 161 ++++++++--------- .../okhttp/OkHttpWebSocketFactory.java | 7 +- .../OkHttpWebSocketFactoryServiceTest.java | 6 +- .../WebSocketConnectionAttemptState.java | 127 ++++++++++---- .../smack/websocket/WebSocketException.java | 16 +- .../XmppWebSocketTransportModule.java | 162 +++++++++--------- ...mppWebSocketTransportModuleDescriptor.java | 57 +++++- .../websocket/impl/AbstractWebSocket.java | 70 +++++++- .../websocket/impl/WebSocketFactory.java | 6 +- .../impl/WebSocketFactoryService.java | 8 +- ...cureWebSocketRemoteConnectionEndpoint.java | 39 +++++ ...cureWebSocketRemoteConnectionEndpoint.java | 39 +++++ .../WebSocketRemoteConnectionEndpoint.java | 90 +++++++--- ...bSocketRemoteConnectionEndpointLookup.java | 106 ++++++------ .../XmppWebSocketTransportModuleTest.java | 45 ----- ...WebSocketRemoteConnectionEndpointTest.java | 10 +- .../test/WebSocketFactoryServiceTestUtil.java | 11 +- 38 files changed, 953 insertions(+), 498 deletions(-) create mode 100644 smack-java8-full/src/main/java/org/jivesoftware/smack/full/WebSocketConnectionTest.java create mode 100644 smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/InsecureWebSocketRemoteConnectionEndpoint.java create mode 100644 smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/SecureWebSocketRemoteConnectionEndpoint.java diff --git a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java index e246210c0..5a2009db8 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -1,6 +1,6 @@ /** * - * Copyright 2009 Jive Software, 2018-2020 Florian Schmaus. + * Copyright 2009 Jive Software, 2018-2021 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -2201,18 +2201,29 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { return SMACK_REACTOR.schedule(runnable, delay, unit, ScheduledAction.Kind.NonBlocking); } - protected void onStreamOpen(XmlPullParser parser) { - // We found an opening stream. - if ("jabber:client".equals(parser.getNamespace(null))) { - streamId = parser.getAttributeValue("", "id"); - incomingStreamXmlEnvironment = XmlEnvironment.from(parser); + /** + * Must be called when a XMPP stream open tag is encountered. Sets values like the stream ID and the incoming stream + * XML environment. + *

+ * This method also returns a matching stream close tag. For example if the stream open is {@code }, then + * {@code } is returned. But if it is {@code }, then {@code } is returned. + * Or if it is {@code }, then {@code } is returned. + *

+ * + * @param parser an XML parser that is positioned at the start of the stream open. + * @return a String representing the corresponding stream end tag. + */ + protected String onStreamOpen(XmlPullParser parser) { + assert StreamOpen.ETHERX_JABBER_STREAMS_NAMESPACE.equals(parser.getNamespace()); + assert StreamOpen.UNPREFIXED_ELEMENT.equals(parser.getName()); - String reportedServerDomainString = parser.getAttributeValue("", "from"); - if (reportedServerDomainString == null) { - // RFC 6120 § 4.7.1. makes no explicit statement whether or not 'from' in the stream open from the server - // in c2s connections is required or not. - return; - } + streamId = parser.getAttributeValue("id"); + incomingStreamXmlEnvironment = XmlEnvironment.from(parser); + + String reportedServerDomainString = parser.getAttributeValue("from"); + // RFC 6120 § 4.7.1. makes no explicit statement whether or not 'from' in the stream open from the server + // in c2s connections is required or not. + if (reportedServerDomainString != null) { DomainBareJid reportedServerDomain; try { reportedServerDomain = JidCreate.domainBareFrom(reportedServerDomainString); @@ -2226,6 +2237,12 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { + "' as reported by server could not be transformed to a valid JID", e); } } + + String prefix = parser.getPrefix(); + if (StringUtils.isNotEmpty(prefix)) { + return ""; + } + return ""; } protected final void sendStreamOpen() throws NotConnectedException, InterruptedException { @@ -2233,7 +2250,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { // possible. The 'to' attribute is *always* available. The 'from' attribute if set by the user and no external // mechanism is used to determine the local entity (user). And the 'id' attribute is available after the first // response from the server (see e.g. RFC 6120 § 9.1.1 Step 2.) - CharSequence to = getXMPPServiceDomain(); + DomainBareJid to = getXMPPServiceDomain(); CharSequence from = null; CharSequence localpart = config.getUsername(); if (localpart != null) { @@ -2247,7 +2264,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { updateOutgoingStreamXmlEnvironmentOnStreamOpen(streamOpen); } - protected AbstractStreamOpen getStreamOpen(CharSequence to, CharSequence from, String id, String lang) { + protected AbstractStreamOpen getStreamOpen(DomainBareJid to, CharSequence from, String id, String lang) { return new StreamOpen(to, from, id, lang); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackFuture.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackFuture.java index 08646728a..6cd744bf0 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackFuture.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackFuture.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017-2020 Florian Schmaus + * Copyright 2017-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,6 +75,10 @@ public abstract class SmackFuture implements Future, @Override public final synchronized boolean isDone() { + return result != null || exception != null || cancelled; + } + + public final synchronized boolean wasSuccessful() { return result != null; } @@ -162,6 +166,10 @@ public abstract class SmackFuture implements Future, return result; } + public E getExceptionIfAvailable() { + return exception; + } + protected final synchronized void maybeInvokeCallbacks() { if (cancelled) { return; @@ -326,6 +334,11 @@ public abstract class SmackFuture implements Future, return future; } + public static boolean await(Collection> futures, long timeout) + throws InterruptedException { + return await(futures, timeout, TimeUnit.MILLISECONDS); + } + public static boolean await(Collection> futures, long timeout, TimeUnit unit) throws InterruptedException { CountDownLatch latch = new CountDownLatch(futures.size()); for (SmackFuture future : futures) { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnection.java index 7f677871e..e7a2db3aa 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnection.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2020 Florian Schmaus + * Copyright 2018-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -139,13 +139,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne } @Override - public void setCurrentConnectionExceptionAndNotify(Exception exception) { - ModularXmppClientToServerConnection.this.setCurrentConnectionExceptionAndNotify(exception); - } - - @Override - public void onStreamOpen(XmlPullParser parser) { - ModularXmppClientToServerConnection.this.onStreamOpen(parser); + public String onStreamOpen(XmlPullParser parser) { + return ModularXmppClientToServerConnection.this.onStreamOpen(parser); } @Override @@ -571,7 +566,7 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne } @Override - protected AbstractStreamOpen getStreamOpen(CharSequence to, CharSequence from, String id, String lang) { + protected AbstractStreamOpen getStreamOpen(DomainBareJid to, CharSequence from, String id, String lang) { StreamOpenAndCloseFactory streamOpenAndCloseFactory = activeTransport.getStreamOpenAndCloseFactory(); return streamOpenAndCloseFactory.createStreamOpen(to, from, id, lang); } @@ -720,6 +715,11 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne throw SmackException.NoEndpointsDiscoveredException.from(lookupFailures); } + if (!lookupFailures.isEmpty()) { + // TODO: Put those non-fatal lookup failures into a sink of the connection so that the user is able to + // be aware of them. + } + // Even though the outgoing elements queue is unrelated to the lookup remote connection endpoints state, we // do start the queue at this point. The transports will need it available, and we use the state's reset() // function to close the queue again on failure. @@ -1110,7 +1110,12 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne XmppClientToServerTransport.Stats stats = entry.getValue(); StringUtils.appendHeading(appendable, transportClass.getName()); - appendable.append(stats.toString()).append('\n'); + if (stats != null) { + appendable.append(stats.toString()); + } else { + appendable.append("No stats available."); + } + appendable.append('\n'); } for (Map.Entry entry : filtersStats.entrySet()) { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionConfiguration.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionConfiguration.java index f67ab5a11..03982d647 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionConfiguration.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionConfiguration.java @@ -1,6 +1,6 @@ /** * - * Copyright 2019-2020 Florian Schmaus + * Copyright 2019-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,6 +62,10 @@ public final class ModularXmppClientToServerConnectionConfiguration extends Conn // configuration, e.g. there is no edge from disconnected to connected. throw new IllegalStateException(e); } + + for (ModularXmppClientToServerConnectionModuleDescriptor moduleDescriptor : moduleDescriptors) { + moduleDescriptor.validateConfiguration(this); + } } public void printStateGraphInDotFormat(PrintWriter pw, boolean breakStateName) { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionModuleDescriptor.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionModuleDescriptor.java index 2c1054a36..02b4cd80a 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionModuleDescriptor.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionModuleDescriptor.java @@ -1,6 +1,6 @@ /** * - * Copyright 2019-2020 Florian Schmaus + * Copyright 2019-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,9 @@ public abstract class ModularXmppClientToServerConnectionModuleDescriptor { protected abstract ModularXmppClientToServerConnectionModule constructXmppConnectionModule( ModularXmppClientToServerConnectionInternal connectionInternal); + protected void validateConfiguration(ModularXmppClientToServerConnectionConfiguration configuration) { + } + public abstract static class Builder { private final ModularXmppClientToServerConnectionConfiguration.Builder connectionConfigurationBuilder; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/StreamOpenAndCloseFactory.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/StreamOpenAndCloseFactory.java index 4a15467d2..e79f5d599 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/c2s/StreamOpenAndCloseFactory.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/StreamOpenAndCloseFactory.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Aditya Borikar. + * Copyright 2020 Aditya Borikar, 2021 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,12 @@ package org.jivesoftware.smack.c2s; import org.jivesoftware.smack.packet.AbstractStreamClose; import org.jivesoftware.smack.packet.AbstractStreamOpen; +import org.jxmpp.jid.DomainBareJid; + public interface StreamOpenAndCloseFactory { - AbstractStreamOpen createStreamOpen(CharSequence to, CharSequence from, String id, String lang); + + AbstractStreamOpen createStreamOpen(DomainBareJid to, CharSequence from, String id, String lang); AbstractStreamClose createStreamClose(); + } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/XmppClientToServerTransport.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/XmppClientToServerTransport.java index 4bedb51b6..a4f0db206 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/c2s/XmppClientToServerTransport.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/XmppClientToServerTransport.java @@ -1,6 +1,6 @@ /** * - * Copyright 2019-2020 Florian Schmaus + * Copyright 2019-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,8 @@ public abstract class XmppClientToServerTransport { protected abstract void loadConnectionEndpoints(LookupConnectionEndpointsSuccess lookupConnectionEndpointsSuccess); + public abstract boolean hasUseableConnectionEndpoints(); + /** * Notify the transport that new outgoing data is available. Usually this method does not need to be called * explicitly, only if the filters are modified so that they potentially produced new data. diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/ModularXmppClientToServerConnectionInternal.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/ModularXmppClientToServerConnectionInternal.java index 139f1194f..bc8b9d441 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/ModularXmppClientToServerConnectionInternal.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/ModularXmppClientToServerConnectionInternal.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Florian Schmaus + * Copyright 2020-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ */ package org.jivesoftware.smack.c2s.internal; +import java.io.IOException; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; @@ -39,8 +40,10 @@ import org.jivesoftware.smack.packet.Nonza; import org.jivesoftware.smack.packet.TopLevelStreamElement; import org.jivesoftware.smack.packet.XmlEnvironment; import org.jivesoftware.smack.util.Consumer; +import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.util.Supplier; import org.jivesoftware.smack.xml.XmlPullParser; +import org.jivesoftware.smack.xml.XmlPullParserException; public abstract class ModularXmppClientToServerConnectionInternal { @@ -85,9 +88,19 @@ public abstract class ModularXmppClientToServerConnectionInternal { public abstract void notifyConnectionError(Exception e); - public abstract void setCurrentConnectionExceptionAndNotify(Exception exception); + public final String onStreamOpen(String streamOpen) { + XmlPullParser streamOpenParser; + try { + streamOpenParser = PacketParserUtils.getParserFor(streamOpen); + } catch (XmlPullParserException | IOException e) { + // Should never happen. + throw new AssertionError(e); + } + String streamClose = onStreamOpen(streamOpenParser); + return streamClose; + } - public abstract void onStreamOpen(XmlPullParser parser); + public abstract String onStreamOpen(XmlPullParser parser); public abstract void onStreamClosed(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/State.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/State.java index a4a7b76ea..0bd7c42db 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/fsm/State.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/State.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2020 Florian Schmaus + * Copyright 2018-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.io.IOException; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.c2s.XmppClientToServerTransport; import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; import org.jivesoftware.smack.c2s.internal.WalkStateGraphContext; @@ -75,4 +76,24 @@ public abstract class State { } } + public abstract static class AbstractTransport extends State { + + private final XmppClientToServerTransport transport; + + protected AbstractTransport(XmppClientToServerTransport transport, StateDescriptor stateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + super(stateDescriptor, connectionInternal); + this.transport = transport; + } + + @Override + public StateTransitionResult.TransitionImpossible isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) + throws SmackException { + if (!transport.hasUseableConnectionEndpoints()) { + return new StateTransitionResult.TransitionImpossibleBecauseNoEndpointsDiscovered(transport); + } + + return super.isTransitionToPossible(walkStateGraphContext); + } + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateTransitionResult.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateTransitionResult.java index a9acb4f32..7bc68af5f 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateTransitionResult.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateTransitionResult.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2020 Florian Schmaus + * Copyright 2018-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ */ package org.jivesoftware.smack.fsm; +import org.jivesoftware.smack.c2s.XmppClientToServerTransport; + public abstract class StateTransitionResult { private final String message; @@ -92,4 +94,10 @@ public abstract class StateTransitionResult { super(stateDescriptor.getFullStateName(false) + " is not implemented (yet)"); } } + + public static class TransitionImpossibleBecauseNoEndpointsDiscovered extends TransitionImpossibleReason { + public TransitionImpossibleBecauseNoEndpointsDiscovered(XmppClientToServerTransport transport) { + super("The transport " + transport + " did not discover any endpoints"); + } + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractStreamOpen.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractStreamOpen.java index 698e7888d..c3ebea932 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractStreamOpen.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractStreamOpen.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Florian Schmaus, Aditya Borikar + * Copyright 2020-2021 Florian Schmaus, 2020 Aditya Borikar * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import org.jivesoftware.smack.util.XmlStringBuilder; * be achieved through {@link XMPPConnection#sendNonza(Nonza)}. */ public abstract class AbstractStreamOpen implements Nonza { + public static final String ETHERX_JABBER_STREAMS_NAMESPACE = "http://etherx.jabber.org/streams"; public static final String CLIENT_NAMESPACE = "jabber:client"; public static final String SERVER_NAMESPACE = "jabber:server"; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamClose.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamClose.java index fd284a2e5..2d7f6e9c8 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamClose.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamClose.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018 Florian Schmaus + * Copyright 2018-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ public final class StreamClose extends AbstractStreamClose { public static final StreamClose INSTANCE = new StreamClose(); + public static final String STRING = ""; + private StreamClose() { } @@ -39,4 +41,8 @@ public final class StreamClose extends AbstractStreamClose { return StreamOpen.ELEMENT; } + @Override + public String toString() { + return STRING; + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamOpen.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamOpen.java index 959adc1fa..1c0c922a6 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamOpen.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamOpen.java @@ -23,7 +23,9 @@ import org.jivesoftware.smack.util.XmlStringBuilder; * The stream open tag. */ public final class StreamOpen extends AbstractStreamOpen { - public static final String ELEMENT = "stream:stream"; + public static final String UNPREFIXED_ELEMENT = "stream"; + + public static final String ELEMENT = "stream:" + UNPREFIXED_ELEMENT; public StreamOpen(CharSequence to) { this(to, null, null, null, StreamContentNamespace.client); diff --git a/smack-java8-full/build.gradle b/smack-java8-full/build.gradle index 8aadd7927..362566afc 100644 --- a/smack-java8-full/build.gradle +++ b/smack-java8-full/build.gradle @@ -12,7 +12,8 @@ dependencies { api project(':smack-openpgp') api project(':smack-resolver-minidns') api project(':smack-resolver-minidns-dox') - api project(':smack-websocket') + // TODO: Change this to smack-websocket-java11 once it arrives. + api project(':smack-websocket-okhttp') api project(':smack-tcp') testImplementation(testFixtures(project(":smack-core"))) diff --git a/smack-java8-full/src/main/java/org/jivesoftware/smack/full/WebSocketConnectionTest.java b/smack-java8-full/src/main/java/org/jivesoftware/smack/full/WebSocketConnectionTest.java new file mode 100644 index 000000000..54c3d30d5 --- /dev/null +++ b/smack-java8-full/src/main/java/org/jivesoftware/smack/full/WebSocketConnectionTest.java @@ -0,0 +1,110 @@ +/** + * + * Copyright 2021 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.smack.full; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Date; +import java.util.logging.Logger; + +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; + +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; +import org.jivesoftware.smack.debugger.ConsoleDebugger; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.websocket.XmppWebSocketTransportModuleDescriptor; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; + +import org.jxmpp.util.XmppDateTime; + +public class WebSocketConnectionTest { + + static { + SmackConfiguration.DEBUG = true; + } + + public static void main(String[] args) + throws URISyntaxException, SmackException, IOException, XMPPException, InterruptedException { + String jid, password, websocketEndpoint, messageTo = null; + if (args.length < 3 || args.length > 4) { + throw new IllegalArgumentException(); + } + + jid = args[0]; + password = args[1]; + websocketEndpoint = args[2]; + if (args.length >= 4) { + messageTo = args[3]; + } + + testWebSocketConnection(jid, password, websocketEndpoint, messageTo); + } + + public static void testWebSocketConnection(String jid, String password, String websocketEndpoint) + throws URISyntaxException, SmackException, IOException, XMPPException, InterruptedException { + testWebSocketConnection(jid, password, websocketEndpoint, null); + } + + public static void testWebSocketConnection(String jid, String password, String websocketEndpoint, String messageTo) + throws URISyntaxException, SmackException, IOException, XMPPException, InterruptedException { + ModularXmppClientToServerConnectionConfiguration.Builder builder = ModularXmppClientToServerConnectionConfiguration.builder(); + builder.removeAllModules() + .setXmppAddressAndPassword(jid, password) + .setDebuggerFactory(ConsoleDebugger.Factory.INSTANCE) + ; + + XmppWebSocketTransportModuleDescriptor.Builder websocketBuilder = XmppWebSocketTransportModuleDescriptor.getBuilder(builder); + websocketBuilder.explicitlySetWebSocketEndpointAndDiscovery(websocketEndpoint, false); + builder.addModule(websocketBuilder.build()); + + ModularXmppClientToServerConnectionConfiguration config = builder.build(); + ModularXmppClientToServerConnection connection = new ModularXmppClientToServerConnection(config); + + connection.setReplyTimeout(5 * 60 * 1000); + + connection.addConnectionStateMachineListener((event, c) -> { + Logger.getAnonymousLogger().info("Connection event: " + event); + }); + + connection.connect(); + + connection.login(); + + if (messageTo != null) { + Message message = connection.getStanzaFactory().buildMessageStanza() + .to(messageTo) + .setBody("It is alive! " + XmppDateTime.formatXEP0082Date(new Date())) + .build() + ; + connection.sendStanza(message); + } + + Thread.sleep(1000); + + connection.disconnect(); + + ModularXmppClientToServerConnection.Stats connectionStats = connection.getStats(); + ServiceDiscoveryManager.Stats serviceDiscoveryManagerStats = ServiceDiscoveryManager.getInstanceFor(connection).getStats(); + + // CHECKSTYLE:OFF + System.out.println("WebSocket successfully finished, yeah!\n" + connectionStats + '\n' + serviceDiscoveryManagerStats); + // CHECKSTYLE:ON + } +} diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/Nio.java b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/Nio.java index af4b939d6..09d296bb5 100644 --- a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/Nio.java +++ b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/Nio.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2020 Florian Schmaus + * Copyright 2018-2021 Florian Schmaus * * This file is part of smack-repl. * @@ -25,8 +25,6 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; -import java.util.Date; -import java.util.logging.Logger; import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.SmackException; @@ -38,18 +36,11 @@ import org.jivesoftware.smack.compression.XMPPInputOutputStream; import org.jivesoftware.smack.compression.XMPPInputOutputStream.FlushMethod; import org.jivesoftware.smack.debugger.ConsoleDebugger; import org.jivesoftware.smack.debugger.SmackDebuggerFactory; -import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.sm.StreamManagementModuleDescriptor; import org.jivesoftware.smack.tcp.XmppTcpTransportModuleDescriptor; -import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; - -import org.jxmpp.util.XmppDateTime; - public class Nio { - private static final Logger LOGGER = Logger.getLogger(Nio.class.getName()); - public static void main(String[] args) throws SmackException, IOException, XMPPException, InterruptedException { doNio(args[0], args[1], args[2]); } @@ -111,30 +102,7 @@ public class Nio { connection.setReplyTimeout(5 * 60 * 1000); - connection.addConnectionStateMachineListener((event, c) -> { - LOGGER.info("Connection event: " + event); - }); - - connection.connect(); - - connection.login(); - - Message message = connection.getStanzaFactory().buildMessageStanza() - .to("flo@geekplace.eu") - .setBody("It is alive! " + XmppDateTime.formatXEP0082Date(new Date())) - .build(); - connection.sendStanza(message); - - Thread.sleep(1000); - - connection.disconnect(); - - ModularXmppClientToServerConnection.Stats connectionStats = connection.getStats(); - ServiceDiscoveryManager.Stats serviceDiscoveryManagerStats = ServiceDiscoveryManager.getInstanceFor(connection).getStats(); - - // CHECKSTYLE:OFF - System.out.println("NIO successfully finished, yeah!\n" + connectionStats + '\n' + serviceDiscoveryManagerStats); - // CHECKSTYLE:ON + XmppTools.modularConnectionTest(connection, "flo@geekplace.eu"); } } diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/WebSocketConnection.java b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/WebSocketConnection.java index ee117dcb8..f693fb739 100644 --- a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/WebSocketConnection.java +++ b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/WebSocketConnection.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Aditya Borikar + * Copyright 2021 Florian Schmaus * * This file is part of smack-repl. * @@ -21,7 +21,6 @@ package org.igniterealtime.smack.smackrepl; import java.io.IOException; -import java.net.URI; import java.net.URISyntaxException; import org.jivesoftware.smack.SmackException; @@ -29,25 +28,51 @@ import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; +import org.jivesoftware.smack.util.TLSUtils; import org.jivesoftware.smack.websocket.XmppWebSocketTransportModuleDescriptor; public class WebSocketConnection { - public static void main(String[] args) throws SmackException, IOException, XMPPException, InterruptedException, URISyntaxException { - ModularXmppClientToServerConnectionConfiguration.Builder builder = ModularXmppClientToServerConnectionConfiguration.builder(); - builder.removeAllModules(); - builder.setXmppAddressAndPassword(args[0], args[1]); + public static void main(String[] args) + throws URISyntaxException, SmackException, IOException, XMPPException, InterruptedException { + String jid, password, websocketEndpoint, messageTo = null; + if (args.length < 3 || args.length > 4) { + throw new IllegalArgumentException(); + } + + jid = args[0]; + password = args[1]; + websocketEndpoint = args[2]; + if (args.length >= 4) { + messageTo = args[3]; + } + + TLSUtils.setDefaultTrustStoreTypeToJksIfRequired(); + + testWebSocketConnection(jid, password, websocketEndpoint, messageTo); + } + + public static void testWebSocketConnection(String jid, String password, String websocketEndpoint) + throws URISyntaxException, SmackException, IOException, XMPPException, InterruptedException { + testWebSocketConnection(jid, password, websocketEndpoint, null); + } + + public static void testWebSocketConnection(String jid, String password, String websocketEndpoint, String messageTo) + throws URISyntaxException, SmackException, IOException, XMPPException, InterruptedException { + ModularXmppClientToServerConnectionConfiguration.Builder builder = ModularXmppClientToServerConnectionConfiguration.builder(); + builder.removeAllModules() + .setXmppAddressAndPassword(jid, password) + ; - // Set a fallback uri into websocket transport descriptor and add this descriptor into connection builder. XmppWebSocketTransportModuleDescriptor.Builder websocketBuilder = XmppWebSocketTransportModuleDescriptor.getBuilder(builder); - websocketBuilder.explicitlySetWebSocketEndpointAndDiscovery(new URI(args[2]), false); + websocketBuilder.explicitlySetWebSocketEndpointAndDiscovery(websocketEndpoint, false); builder.addModule(websocketBuilder.build()); ModularXmppClientToServerConnectionConfiguration config = builder.build(); ModularXmppClientToServerConnection connection = new ModularXmppClientToServerConnection(config); - connection.connect(); - connection.login(); - connection.disconnect(); + connection.setReplyTimeout(5 * 60 * 1000); + + XmppTools.modularConnectionTest(connection, messageTo); } } diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/XmppTools.java b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/XmppTools.java index 958dbe1b1..eeb6c7a40 100644 --- a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/XmppTools.java +++ b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/XmppTools.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016 Florian Schmaus + * Copyright 2016-2021 Florian Schmaus * * This file is part of smack-repl. * @@ -23,6 +23,8 @@ package org.igniterealtime.smack.smackrepl; import java.io.IOException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; +import java.util.Date; +import java.util.logging.Logger; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException.NoResponseException; @@ -30,15 +32,19 @@ import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; +import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.tcp.XMPPTCPConnection; import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; import org.jivesoftware.smack.util.TLSUtils; - +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.iqregister.AccountManager; import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Localpart; +import org.jxmpp.stringprep.XmppStringprepException; +import org.jxmpp.util.XmppDateTime; public class XmppTools { @@ -106,4 +112,40 @@ public class XmppTools { connection.disconnect(); } } + + public static void modularConnectionTest(ModularXmppClientToServerConnection connection, String messageTo) throws XMPPException, SmackException, IOException, InterruptedException { + connection.addConnectionStateMachineListener((event, c) -> { + Logger.getAnonymousLogger().info("Connection event: " + event); + }); + + connection.connect(); + + connection.login(); + + XmppTools.sendItsAlive(messageTo, connection); + + Thread.sleep(1000); + + connection.disconnect(); + + ModularXmppClientToServerConnection.Stats connectionStats = connection.getStats(); + ServiceDiscoveryManager.Stats serviceDiscoveryManagerStats = ServiceDiscoveryManager.getInstanceFor(connection).getStats(); + + // CHECKSTYLE:OFF + System.out.println("NIO successfully finished, yeah!\n" + connectionStats + '\n' + serviceDiscoveryManagerStats); + // CHECKSTYLE:ON + } + + public static void sendItsAlive(String to, XMPPConnection connection) + throws XmppStringprepException, NotConnectedException, InterruptedException { + if (to == null) { + return; + } + + Message message = connection.getStanzaFactory().buildMessageStanza() + .to(to) + .setBody("It is alive! " + XmppDateTime.formatXEP0082Date(new Date())) + .build(); + connection.sendStanza(message); + } } diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java index 8e3c8d282..d6e513036 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java @@ -83,6 +83,7 @@ import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.StartTls; import org.jivesoftware.smack.packet.StreamError; +import org.jivesoftware.smack.packet.StreamOpen; import org.jivesoftware.smack.proxy.ProxyInfo; import org.jivesoftware.smack.sasl.packet.SaslNonza; import org.jivesoftware.smack.sm.SMUtils; @@ -961,6 +962,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { switch (eventType) { case START_ELEMENT: final String name = parser.getName(); + final String namespace = parser.getNamespace(); + switch (name) { case Message.ELEMENT: case IQ.IQ_ELEMENT: @@ -972,7 +975,9 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } break; case "stream": - onStreamOpen(parser); + if (StreamOpen.ETHERX_JABBER_STREAMS_NAMESPACE.equals(namespace)) { + onStreamOpen(parser); + } break; case "error": StreamError streamError = PacketParserUtils.parseStreamError(parser); @@ -989,7 +994,6 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { openStreamAndResetParser(); break; case "failure": - String namespace = parser.getNamespace(null); switch (namespace) { case "urn:ietf:params:xml:ns:xmpp-tls": // TLS negotiation has failed. The server will close the connection diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java index 276784e84..1719633a0 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java @@ -1,6 +1,6 @@ /** * - * Copyright 2019-2020 Florian Schmaus + * Copyright 2019-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,14 +80,12 @@ import org.jivesoftware.smack.tcp.rce.RemoteXmppTcpConnectionEndpoints; import org.jivesoftware.smack.tcp.rce.RemoteXmppTcpConnectionEndpoints.Result; import org.jivesoftware.smack.tcp.rce.Rfc6120TcpRemoteConnectionEndpoint; import org.jivesoftware.smack.util.CollectionUtil; -import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.UTF8; import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure; -import org.jivesoftware.smack.xml.XmlPullParser; -import org.jivesoftware.smack.xml.XmlPullParserException; +import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.Jid; import org.jxmpp.jid.util.JidUtil; import org.jxmpp.xml.splitter.Utf8ByteXmppXmlSplitter; @@ -213,6 +211,8 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM } final String prefixXmlns = "xmlns:" + prefix; + // TODO: Use the return value of onStreamOpen(), which now returns the + // corresponding stream close tag, instead of creating it here. final StringBuilder streamClose = new StringBuilder(32); final StringBuilder streamOpen = new StringBuilder(256); @@ -253,14 +253,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM this.streamOpen = streamOpen.toString(); this.streamClose = streamClose.toString(); - XmlPullParser streamOpenParser; - try { - streamOpenParser = PacketParserUtils.getParserFor(this.streamOpen); - } catch (XmlPullParserException | IOException e) { - // Should never happen. - throw new AssertionError(e); - } - connectionInternal.onStreamOpen(streamOpenParser); + connectionInternal.onStreamOpen(this.streamOpen); } @Override @@ -586,7 +579,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM public StreamOpenAndCloseFactory getStreamOpenAndCloseFactory() { return new StreamOpenAndCloseFactory() { @Override - public StreamOpen createStreamOpen(CharSequence to, CharSequence from, String id, String lang) { + public StreamOpen createStreamOpen(DomainBareJid to, CharSequence from, String id, String lang) { String xmlLang = connectionInternal.connection.getConfiguration().getXmlLang(); StreamOpen streamOpen = new StreamOpen(to, from, id, xmlLang, StreamOpen.StreamContentNamespace.client); return streamOpen; @@ -603,6 +596,11 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM discoveredTcpEndpoints = null; } + @Override + public boolean hasUseableConnectionEndpoints() { + return discoveredTcpEndpoints != null; + } + @Override protected List> lookupConnectionEndpoints() { // Assert that there are no stale discovered endpoints prior performing the lookup. @@ -750,10 +748,10 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM return new EstablishingTcpConnectionState(stateDescriptor, connectionInternal); } - final class EstablishingTcpConnectionState extends State { + final class EstablishingTcpConnectionState extends State.AbstractTransport { private EstablishingTcpConnectionState(EstablishingTcpConnectionStateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { - super(stateDescriptor, connectionInternal); + super(tcpNioTransport, stateDescriptor, connectionInternal); } @Override @@ -777,6 +775,10 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM connectionInternal.setTransport(tcpNioTransport); + // TODO: It appears this should be done in a generic way. I'd assume we always + // have to wait for stream features after the connection was established. If this is true then consider + // moving this into State.AbstractTransport. But I am not yet 100% positive that this is the case for every + // transport. Hence keep it here for now. connectionInternal.newStreamOpenWaitForFeaturesSequence("stream features after initial connection"); return new TcpSocketConnectedResult(remoteAddress); diff --git a/smack-websocket-okhttp/src/main/java/org/jivesoftware/smack/websocket/okhttp/LoggingInterceptor.java b/smack-websocket-okhttp/src/main/java/org/jivesoftware/smack/websocket/okhttp/LoggingInterceptor.java index e6e14e0e0..99697617e 100644 --- a/smack-websocket-okhttp/src/main/java/org/jivesoftware/smack/websocket/okhttp/LoggingInterceptor.java +++ b/smack-websocket-okhttp/src/main/java/org/jivesoftware/smack/websocket/okhttp/LoggingInterceptor.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Aditya Borikar + * Copyright 2020 Aditya Borikar, 2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.jivesoftware.smack.websocket.okhttp; import java.io.IOException; -import java.nio.charset.Charset; import java.util.Iterator; import java.util.logging.Level; import java.util.logging.Logger; @@ -27,39 +26,34 @@ import org.jivesoftware.smack.debugger.SmackDebugger; import okhttp3.Headers; import okhttp3.Response; -import org.jxmpp.xml.splitter.Utf8ByteXmppXmlSplitter; import org.jxmpp.xml.splitter.XmlPrettyPrinter; import org.jxmpp.xml.splitter.XmppXmlSplitter; public final class LoggingInterceptor { - private static final Logger LOGGER = Logger.getAnonymousLogger(); - private static final int MAX_ELEMENT_SIZE = 64 * 1024; - private final SmackDebugger debugger; - private final Utf8ByteXmppXmlSplitter incomingTextSplitter; - private final Utf8ByteXmppXmlSplitter outgoingTextSplitter; + private static final Logger LOGGER = Logger.getLogger(LoggingInterceptor.class.getName()); - public LoggingInterceptor(SmackDebugger smackDebugger) { + private final SmackDebugger debugger; + private final XmppXmlSplitter incomingXmlSplitter; + private final XmppXmlSplitter outgoingXmlSplitter; + + LoggingInterceptor(SmackDebugger smackDebugger) { this.debugger = smackDebugger; XmlPrettyPrinter incomingTextPrinter = XmlPrettyPrinter.builder() .setPrettyWriter(sb -> debugger.incomingStreamSink(sb)) .setTabWidth(4) .build(); - XmppXmlSplitter incomingXmlSplitter = new XmppXmlSplitter(MAX_ELEMENT_SIZE, null, - incomingTextPrinter); - incomingTextSplitter = new Utf8ByteXmppXmlSplitter(incomingXmlSplitter); + incomingXmlSplitter = new XmppXmlSplitter(incomingTextPrinter); XmlPrettyPrinter outgoingTextPrinter = XmlPrettyPrinter.builder() .setPrettyWriter(sb -> debugger.outgoingStreamSink(sb)) .setTabWidth(4) .build(); - XmppXmlSplitter outgoingXmlSplitter = new XmppXmlSplitter(MAX_ELEMENT_SIZE, null, - outgoingTextPrinter); - outgoingTextSplitter = new Utf8ByteXmppXmlSplitter(outgoingXmlSplitter); + outgoingXmlSplitter = new XmppXmlSplitter(outgoingTextPrinter); } // Open response received here isn't in the form of an Xml an so, there isn't much to format. - public void interceptOpenResponse(Response response) { + void interceptOpenResponse(Response response) { Headers headers = response.headers(); Iterator iterator = headers.iterator(); StringBuilder sb = new StringBuilder(); @@ -70,18 +64,18 @@ public final class LoggingInterceptor { debugger.incomingStreamSink(sb); } - public void interceptReceivedText(String text) { + void interceptReceivedText(String text) { try { - incomingTextSplitter.write(text.getBytes(Charset.defaultCharset())); + incomingXmlSplitter.write(text); } catch (IOException e) { // Connections shouldn't be terminated due to exceptions encountered during debugging. hence only log them. LOGGER.log(Level.WARNING, "IOException encountered while parsing received text: " + text, e); } } - public void interceptSentText(String text) { + void interceptSentText(String text) { try { - outgoingTextSplitter.write(text.getBytes(Charset.defaultCharset())); + outgoingXmlSplitter.write(text); } catch (IOException e) { // Connections shouldn't be terminated due to exceptions encountered during debugging, hence only log them. LOGGER.log(Level.WARNING, "IOException encountered while parsing outgoing text: " + text, e); diff --git a/smack-websocket-okhttp/src/main/java/org/jivesoftware/smack/websocket/okhttp/OkHttpWebSocket.java b/smack-websocket-okhttp/src/main/java/org/jivesoftware/smack/websocket/okhttp/OkHttpWebSocket.java index 8523b350a..59d2dba7c 100644 --- a/smack-websocket-okhttp/src/main/java/org/jivesoftware/smack/websocket/okhttp/OkHttpWebSocket.java +++ b/smack-websocket-okhttp/src/main/java/org/jivesoftware/smack/websocket/okhttp/OkHttpWebSocket.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Aditya Borikar + * Copyright 2020 Aditya Borikar, 2020-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,22 +16,17 @@ */ package org.jivesoftware.smack.websocket.okhttp; -import java.io.IOException; +import java.net.URI; import java.util.logging.Level; import java.util.logging.Logger; import javax.net.ssl.SSLSession; -import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.SmackFuture; import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; -import org.jivesoftware.smack.packet.TopLevelStreamElement; -import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.websocket.WebSocketException; -import org.jivesoftware.smack.websocket.elements.WebSocketOpenElement; import org.jivesoftware.smack.websocket.impl.AbstractWebSocket; import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint; -import org.jivesoftware.smack.xml.XmlPullParserException; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -43,135 +38,119 @@ public final class OkHttpWebSocket extends AbstractWebSocket { private static final Logger LOGGER = Logger.getLogger(OkHttpWebSocket.class.getName()); - private static OkHttpClient okHttpClient = null; + private static final OkHttpClient okHttpClient = new OkHttpClient(); + + // This is a potential candidate to be placed into AbstractWebSocket, but I keep it here until smack-websocket-java11 + // arrives. + private final SmackFuture.InternalSmackFuture future = new SmackFuture.InternalSmackFuture<>(); - private final ModularXmppClientToServerConnectionInternal connectionInternal; private final LoggingInterceptor interceptor; - private String openStreamHeader; - private WebSocket currentWebSocket; - private WebSocketConnectionPhase phase; - private WebSocketRemoteConnectionEndpoint connectedEndpoint; + private final WebSocket okHttpWebSocket; - public OkHttpWebSocket(ModularXmppClientToServerConnectionInternal connectionInternal) { - this.connectionInternal = connectionInternal; + public OkHttpWebSocket(WebSocketRemoteConnectionEndpoint endpoint, + ModularXmppClientToServerConnectionInternal connectionInternal) { + super(endpoint, connectionInternal); - if (okHttpClient == null) { - // Creates an instance of okHttp client. - OkHttpClient.Builder builder = new OkHttpClient.Builder(); - okHttpClient = builder.build(); - } - // Add some mechanism to enable and disable this interceptor. if (connectionInternal.smackDebugger != null) { interceptor = new LoggingInterceptor(connectionInternal.smackDebugger); } else { interceptor = null; } - } - @Override - public void connect(WebSocketRemoteConnectionEndpoint endpoint) throws InterruptedException, SmackException, XMPPException { - final String currentUri = endpoint.getWebSocketEndpoint().toString(); + final URI uri = endpoint.getUri(); + final String url = uri.toString(); + Request request = new Request.Builder() - .url(currentUri) + .url(url) .header("Sec-WebSocket-Protocol", "xmpp") .build(); - WebSocketListener listener = new WebSocketListener() { + okHttpWebSocket = okHttpClient.newWebSocket(request, listener); + } - @Override - public void onOpen(WebSocket webSocket, Response response) { - LOGGER.log(Level.FINER, "WebSocket is open"); - phase = WebSocketConnectionPhase.openFrameSent; - if (interceptor != null) { - interceptor.interceptOpenResponse(response); - } - send(new WebSocketOpenElement(connectionInternal.connection.getXMPPServiceDomain())); + private final WebSocketListener listener = new WebSocketListener() { + + @Override + public void onOpen(WebSocket webSocket, Response response) { + LOGGER.log(Level.FINER, "OkHttp invoked onOpen() for {0}. Response: {1}", + new Object[] { webSocket, response }); + + if (interceptor != null) { + interceptor.interceptOpenResponse(response); } - @Override - public void onMessage(WebSocket webSocket, String text) { - if (interceptor != null) { - interceptor.interceptReceivedText(text); - } - if (isCloseElement(text)) { - connectionInternal.onStreamClosed(); - return; - } + future.setResult(OkHttpWebSocket.this); + } - String closingStream = ""; - switch (phase) { - case openFrameSent: - if (isOpenElement(text)) { - // Converts the element received into element. - openStreamHeader = getStreamFromOpenElement(text); - phase = WebSocketConnectionPhase.exchangingTopLevelStreamElements; - - try { - connectionInternal.onStreamOpen(PacketParserUtils.getParserFor(openStreamHeader)); - } catch (XmlPullParserException | IOException e) { - LOGGER.log(Level.WARNING, "Exception caught:", e); - } - } else { - LOGGER.log(Level.WARNING, "Unexpected Frame received", text); - } - break; - case exchangingTopLevelStreamElements: - connectionInternal.parseAndProcessElement(openStreamHeader + text + closingStream); - break; - default: - LOGGER.log(Level.INFO, "Default text: " + text); - } + @Override + public void onMessage(WebSocket webSocket, String text) { + if (interceptor != null) { + interceptor.interceptReceivedText(text); } - @Override - public void onFailure(WebSocket webSocket, Throwable t, Response response) { - LOGGER.log(Level.INFO, "Exception caught", t); - WebSocketException websocketException = new WebSocketException(t); - if (connectionInternal.connection.isConnected()) { - connectionInternal.notifyConnectionError(websocketException); - } else { - connectionInternal.setCurrentConnectionExceptionAndNotify(websocketException); - } + onIncomingWebSocketElement(text); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable throwable, Response response) { + LOGGER.log(Level.FINER, "OkHttp invoked onFailure() for " + webSocket + ". Response: " + response, throwable); + WebSocketException websocketException = new WebSocketException(throwable); + + // If we are already connected, then we need to notify the connection that it got tear down. Otherwise we + // need to notify the thread calling connect() that the connection failed. + if (future.wasSuccessful()) { + connectionInternal.notifyConnectionError(websocketException); + } else { + future.setException(websocketException); } - }; + } - // Creates an instance of websocket through okHttpClient. - currentWebSocket = okHttpClient.newWebSocket(request, listener); + @Override + public void onClosing(WebSocket webSocket, int code, String reason) { + LOGGER.log(Level.FINER, "OkHttp invoked onClosing() for " + webSocket + ". Code: " + code + ". Reason: " + reason); + } - // Open a new stream and wait until features are received. - connectionInternal.waitForFeaturesReceived("Waiting to receive features"); + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + LOGGER.log(Level.FINER, "OkHttp invoked onClosed() for " + webSocket + ". Code: " + code + ". Reason: " + reason); + } - connectedEndpoint = endpoint; + }; + + @Override + public SmackFuture getFuture() { + return future; } @Override - public void send(TopLevelStreamElement element) { - String textToBeSent = element.toXML().toString(); + public void send(String element) { if (interceptor != null) { - interceptor.interceptSentText(textToBeSent); + interceptor.interceptSentText(element); } - currentWebSocket.send(textToBeSent); + okHttpWebSocket.send(element); } @Override public void disconnect(int code, String message) { - currentWebSocket.close(code, message); - LOGGER.log(Level.INFO, "WebSocket has been closed with message: " + message); + LOGGER.log(Level.INFO, "WebSocket closing with code: " + code + " and message: " + message); + okHttpWebSocket.close(code, message); } @Override public boolean isConnectionSecure() { - return connectedEndpoint.isSecureEndpoint(); + return endpoint.isSecureEndpoint(); } @Override public boolean isConnected() { - return connectedEndpoint == null ? false : true; + // TODO: Do we need this method at all if we create an AbstractWebSocket object for every endpoint? + return true; } @Override public SSLSession getSSLSession() { + // TODO: What shall we do about this method, as it appears that OkHttp does not provide access to the used SSLSession? return null; } } diff --git a/smack-websocket-okhttp/src/main/java/org/jivesoftware/smack/websocket/okhttp/OkHttpWebSocketFactory.java b/smack-websocket-okhttp/src/main/java/org/jivesoftware/smack/websocket/okhttp/OkHttpWebSocketFactory.java index ea6b459bc..64246a7cc 100644 --- a/smack-websocket-okhttp/src/main/java/org/jivesoftware/smack/websocket/okhttp/OkHttpWebSocketFactory.java +++ b/smack-websocket-okhttp/src/main/java/org/jivesoftware/smack/websocket/okhttp/OkHttpWebSocketFactory.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Florian Schmaus. + * Copyright 2020-2021 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,13 @@ package org.jivesoftware.smack.websocket.okhttp; import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; import org.jivesoftware.smack.websocket.impl.AbstractWebSocket; import org.jivesoftware.smack.websocket.impl.WebSocketFactory; +import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint; public class OkHttpWebSocketFactory implements WebSocketFactory { @Override - public AbstractWebSocket create(ModularXmppClientToServerConnectionInternal connectionInternal) { - return new OkHttpWebSocket(connectionInternal); + public AbstractWebSocket create(WebSocketRemoteConnectionEndpoint endpoint, ModularXmppClientToServerConnectionInternal connectionInternal) { + return new OkHttpWebSocket(endpoint, connectionInternal); } } diff --git a/smack-websocket-okhttp/src/test/java/org/jivesoftware/smack/websocket/okhttp/OkHttpWebSocketFactoryServiceTest.java b/smack-websocket-okhttp/src/test/java/org/jivesoftware/smack/websocket/okhttp/OkHttpWebSocketFactoryServiceTest.java index c1fadf85a..70d4c6084 100644 --- a/smack-websocket-okhttp/src/test/java/org/jivesoftware/smack/websocket/okhttp/OkHttpWebSocketFactoryServiceTest.java +++ b/smack-websocket-okhttp/src/test/java/org/jivesoftware/smack/websocket/okhttp/OkHttpWebSocketFactoryServiceTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Florian Schmaus. + * Copyright 2020-2021 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ */ package org.jivesoftware.smack.websocket.okhttp; +import java.net.URISyntaxException; + import org.jivesoftware.smack.websocket.test.WebSocketFactoryServiceTestUtil; import org.junit.jupiter.api.Test; @@ -23,7 +25,7 @@ import org.junit.jupiter.api.Test; public class OkHttpWebSocketFactoryServiceTest { @Test - public void createWebSocketTest() { + public void createWebSocketTest() throws URISyntaxException { WebSocketFactoryServiceTestUtil.createWebSocketTest(OkHttpWebSocket.class); } diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/WebSocketConnectionAttemptState.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/WebSocketConnectionAttemptState.java index c17aedaf6..7fb322596 100644 --- a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/WebSocketConnectionAttemptState.java +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/WebSocketConnectionAttemptState.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Aditya Borikar, Florian Schmaus. + * Copyright 2020 Aditya Borikar, 2020-2021 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,22 +19,29 @@ package org.jivesoftware.smack.websocket; import java.util.ArrayList; import java.util.List; +import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; +import org.jivesoftware.smack.SmackFuture; import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.fsm.StateTransitionResult; +import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.websocket.XmppWebSocketTransportModule.EstablishingWebSocketConnectionState; import org.jivesoftware.smack.websocket.impl.AbstractWebSocket; import org.jivesoftware.smack.websocket.impl.WebSocketFactoryService; import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint; +import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpointLookup; public final class WebSocketConnectionAttemptState { private final ModularXmppClientToServerConnectionInternal connectionInternal; private final XmppWebSocketTransportModule.XmppWebSocketTransport.DiscoveredWebSocketEndpoints discoveredEndpoints; - private WebSocketRemoteConnectionEndpoint connectedEndpoint; + private AbstractWebSocket webSocket; WebSocketConnectionAttemptState(ModularXmppClientToServerConnectionInternal connectionInternal, XmppWebSocketTransportModule.XmppWebSocketTransport.DiscoveredWebSocketEndpoints discoveredWebSocketEndpoints, EstablishingWebSocketConnectionState establishingWebSocketConnectionState) { assert discoveredWebSocketEndpoints != null; + assert !discoveredWebSocketEndpoints.result.isEmpty(); + this.connectionInternal = connectionInternal; this.discoveredEndpoints = discoveredWebSocketEndpoints; } @@ -44,48 +51,96 @@ public final class WebSocketConnectionAttemptState { * * @return {@link AbstractWebSocket} with which connection is establised * @throws InterruptedException if the calling thread was interrupted - * @throws WebSocketException if encounters a websocket exception */ - AbstractWebSocket establishWebSocketConnection() throws InterruptedException, WebSocketException { - List endpoints = discoveredEndpoints.result.discoveredRemoteConnectionEndpoints; + @SuppressWarnings({"incomplete-switch", "MissingCasesInEnumSwitch"}) + StateTransitionResult.Failure establishWebSocketConnection() throws InterruptedException { + final WebSocketRemoteConnectionEndpointLookup.Result endpointLookupResult = discoveredEndpoints.result; + final List failures = new ArrayList<>(endpointLookupResult.discoveredEndpointCount()); - if (endpoints.isEmpty()) { - throw new WebSocketException(new Throwable("No Endpoints discovered to establish connection")); - } + webSocket = null; - List connectionFailureList = new ArrayList<>(); - AbstractWebSocket websocket = WebSocketFactoryService.createWebSocket(connectionInternal); - - // Keep iterating over available endpoints until a connection is establised or all endpoints are tried to create a connection with. - for (WebSocketRemoteConnectionEndpoint endpoint : endpoints) { - try { - websocket.connect(endpoint); - connectedEndpoint = endpoint; - break; - } catch (Throwable t) { - connectionFailureList.add(t); - - // If the number of entries in connectionFailureList is equal to the number of endpoints, - // it means that all endpoints have been tried and have been unsuccessful. - if (connectionFailureList.size() == endpoints.size()) { - WebSocketException websocketException = new WebSocketException(connectionFailureList); - throw new WebSocketException(websocketException); - } + SecurityMode securityMode = connectionInternal.connection.getConfiguration().getSecurityMode(); + switch (securityMode) { + case required: + case ifpossible: + establishWebSocketConnection(endpointLookupResult.discoveredSecureEndpoints, failures); + if (webSocket != null) { + return null; } } - assert connectedEndpoint != null; + establishWebSocketConnection(endpointLookupResult.discoveredInsecureEndpoints, failures); + if (webSocket != null) { + return null; + } - // Return connected websocket when no failure occurs. - return websocket; + StateTransitionResult.Failure failure = FailedToConnectToAnyWebSocketEndpoint.create(failures); + return failure; } - /** - * Returns the connected websocket endpoint. - * - * @return connected websocket endpoint - */ - public WebSocketRemoteConnectionEndpoint getConnectedEndpoint() { - return connectedEndpoint; + private void establishWebSocketConnection(List webSocketEndpoints, + List failures) throws InterruptedException { + final int endpointCount = webSocketEndpoints.size(); + + List> futures = new ArrayList<>(endpointCount); + { + List webSockets = new ArrayList<>(endpointCount); + // First only create the AbstractWebSocket instances, in case a constructor throws. + for (WebSocketRemoteConnectionEndpoint endpoint : webSocketEndpoints) { + AbstractWebSocket webSocket = WebSocketFactoryService.createWebSocket(endpoint, connectionInternal); + webSockets.add(webSocket); + } + + for (AbstractWebSocket webSocket : webSockets) { + SmackFuture future = webSocket.getFuture(); + futures.add(future); + } + } + + SmackFuture.await(futures, connectionInternal.connection.getReplyTimeout()); + + for (SmackFuture future : futures) { + AbstractWebSocket connectedWebSocket = future.getIfAvailable(); + if (connectedWebSocket == null) { + Exception exception = future.getExceptionIfAvailable(); + assert exception != null; + failures.add(exception); + continue; + } + + if (webSocket == null) { + webSocket = connectedWebSocket; + // Continue here since we still need to read out the failure exceptions from potential further remaining + // futures and close remaining successfully connected ones. + continue; + } + + connectedWebSocket.disconnect(1000, "Using other connection endpoint at " + webSocket.getEndpoint()); + } + } + + public AbstractWebSocket getConnectedWebSocket() { + return webSocket; + } + + public static final class FailedToConnectToAnyWebSocketEndpoint extends StateTransitionResult.Failure { + + private final List failures; + + private FailedToConnectToAnyWebSocketEndpoint(String failureMessage, List failures) { + super(failureMessage); + this.failures = failures; + } + + public List getFailures() { + return failures; + } + + private static FailedToConnectToAnyWebSocketEndpoint create(List failures) { + StringBuilder sb = new StringBuilder(256); + StringUtils.appendTo(failures, sb, e -> sb.append(e.getMessage())); + String message = sb.toString(); + return new FailedToConnectToAnyWebSocketEndpoint(message, failures); + } } } diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/WebSocketException.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/WebSocketException.java index 554bb4257..5f6c602e9 100644 --- a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/WebSocketException.java +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/WebSocketException.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Aditya Borikar + * Copyright 2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,23 +16,11 @@ */ package org.jivesoftware.smack.websocket; -import java.util.Collections; -import java.util.List; - public final class WebSocketException extends Exception { private static final long serialVersionUID = 1L; - private final List throwableList; - - public WebSocketException(List throwableList) { - this.throwableList = throwableList; - } - public WebSocketException(Throwable throwable) { - this.throwableList = Collections.singletonList(throwable); + super("WebSocketException: " + throwable.getMessage(), throwable); } - public List getThrowableList() { - return throwableList; - } } diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/XmppWebSocketTransportModule.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/XmppWebSocketTransportModule.java index db80a1b6e..8a0a4f8a6 100644 --- a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/XmppWebSocketTransportModule.java +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/XmppWebSocketTransportModule.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Aditya Borikar + * Copyright 2020 Aditya Borikar, 2020-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,16 @@ */ package org.jivesoftware.smack.websocket; -import java.io.IOException; -import java.net.URI; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Queue; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.net.ssl.SSLSession; import org.jivesoftware.smack.AsyncButOrdered; -import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackFuture; import org.jivesoftware.smack.SmackFuture.InternalSmackFuture; import org.jivesoftware.smack.XMPPException; @@ -54,13 +50,13 @@ import org.jivesoftware.smack.websocket.XmppWebSocketTransportModule.XmppWebSock import org.jivesoftware.smack.websocket.elements.WebSocketCloseElement; import org.jivesoftware.smack.websocket.elements.WebSocketOpenElement; import org.jivesoftware.smack.websocket.impl.AbstractWebSocket; +import org.jivesoftware.smack.websocket.rce.InsecureWebSocketRemoteConnectionEndpoint; +import org.jivesoftware.smack.websocket.rce.SecureWebSocketRemoteConnectionEndpoint; import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint; import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpointLookup; import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpointLookup.Result; import org.jxmpp.jid.DomainBareJid; -import org.jxmpp.jid.impl.JidCreate; -import org.jxmpp.stringprep.XmppStringprepException; /** * The websocket transport module that goes with Smack's modular architecture. @@ -101,31 +97,37 @@ public final class XmppWebSocketTransportModule } } - final class EstablishingWebSocketConnectionState extends State { + final class EstablishingWebSocketConnectionState extends State.AbstractTransport { protected EstablishingWebSocketConnectionState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { - super(stateDescriptor, connectionInternal); + super(websocketTransport, stateDescriptor, connectionInternal); } @Override - public AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) - throws IOException, SmackException, InterruptedException, XMPPException { + public AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws InterruptedException, + NoResponseException, NotConnectedException, SmackException, XMPPException { WebSocketConnectionAttemptState connectionAttemptState = new WebSocketConnectionAttemptState( connectionInternal, discoveredWebSocketEndpoints, this); - try { - websocket = connectionAttemptState.establishWebSocketConnection(); - } catch (InterruptedException | WebSocketException e) { - StateTransitionResult.Failure failure = new StateTransitionResult.FailureCausedByException(e); + StateTransitionResult.Failure failure = connectionAttemptState.establishWebSocketConnection(); + if (failure != null) { return failure; } + websocket = connectionAttemptState.getConnectedWebSocket(); + connectionInternal.setTransport(websocketTransport); - WebSocketRemoteConnectionEndpoint connectedEndpoint = connectionAttemptState.getConnectedEndpoint(); + // TODO: It appears this should be done in a generic way. I'd assume we always + // have to wait for stream features after the connection was established. But I + // am not yet 100% positive that this is the case for every transport. Hence keep it here for now(?). + // See also similar comment in XmppTcpTransportModule. + // Maybe move this into ConnectedButUnauthenticated state's transitionInto() method? That seems to be the + // right place. + connectionInternal.newStreamOpenWaitForFeaturesSequence("stream features after initial connection"); // Construct a WebSocketConnectedResult using the connected endpoint. - return new WebSocketConnectedResult(connectedEndpoint); + return new WebSocketConnectedResult(websocket.getEndpoint()); } } @@ -140,7 +142,7 @@ public final class XmppWebSocketTransportModule final WebSocketRemoteConnectionEndpoint connectedEndpoint; public WebSocketConnectedResult(WebSocketRemoteConnectionEndpoint connectedEndpoint) { - super("WebSocket connection establised with endpoint: " + connectedEndpoint.getWebSocketEndpoint()); + super("WebSocket connection establised with endpoint: " + connectedEndpoint); this.connectedEndpoint = connectedEndpoint; } } @@ -164,6 +166,12 @@ public final class XmppWebSocketTransportModule discoveredWebSocketEndpoints = null; } + @Override + public boolean hasUseableConnectionEndpoints() { + return discoveredWebSocketEndpoints != null; + } + + @SuppressWarnings("incomplete-switch") @Override protected List> lookupConnectionEndpoints() { // Assert that there are no stale discovered endpoints prior performing the lookup. @@ -172,51 +180,56 @@ public final class XmppWebSocketTransportModule InternalSmackFuture websocketEndpointsLookupFuture = new InternalSmackFuture<>(); connectionInternal.asyncGo(() -> { + Result result = null; - WebSocketRemoteConnectionEndpoint providedEndpoint = null; + ModularXmppClientToServerConnectionConfiguration configuration = connectionInternal.connection.getConfiguration(); + DomainBareJid host = configuration.getXMPPServiceDomain(); - // Check if there is a websocket endpoint already configured. - URI uri = moduleDescriptor.getExplicitlyProvidedUri(); - if (uri != null) { - providedEndpoint = new WebSocketRemoteConnectionEndpoint(uri); + if (moduleDescriptor.isWebSocketEndpointDiscoveryEnabled()) { + // Fetch remote endpoints. + result = WebSocketRemoteConnectionEndpointLookup.lookup(host); } - if (!moduleDescriptor.isWebSocketEndpointDiscoveryEnabled()) { - // If discovery is disabled, assert that the provided endpoint isn't null. - assert providedEndpoint != null; - - SecurityMode mode = connectionInternal.connection.getConfiguration().getSecurityMode(); - if ((providedEndpoint.isSecureEndpoint() && - mode.equals(SecurityMode.disabled)) - || (!providedEndpoint.isSecureEndpoint() && - mode.equals(SecurityMode.required))) { - throw new IllegalStateException("Explicitly configured uri: " + providedEndpoint.getWebSocketEndpoint().toString() - + " does not comply with the configured security mode: " + mode); + WebSocketRemoteConnectionEndpoint providedEndpoint = moduleDescriptor.getExplicitlyProvidedEndpoint(); + if (providedEndpoint != null) { + // If there was not automatic lookup that produced a result, then create a result now. + if (result == null) { + result = new Result(); } - // Generate Result for explicitly configured endpoint. - Result manualResult = new Result(Arrays.asList(providedEndpoint), null); - - LookupConnectionEndpointsResult endpointsResult = new DiscoveredWebSocketEndpoints(manualResult); - - websocketEndpointsLookupFuture.setResult(endpointsResult); - } else { - DomainBareJid host = connectionInternal.connection.getXMPPServiceDomain(); - ModularXmppClientToServerConnectionConfiguration configuration = connectionInternal.connection.getConfiguration(); - SecurityMode mode = configuration.getSecurityMode(); - - // Fetch remote endpoints. - Result xep0156result = WebSocketRemoteConnectionEndpointLookup.lookup(host, mode); - - List discoveredEndpoints = xep0156result.discoveredRemoteConnectionEndpoints; - - // Generate result considering both manual and fetched endpoints. - Result finalResult = new Result(discoveredEndpoints, xep0156result.getLookupFailures()); - - LookupConnectionEndpointsResult endpointsResult = new DiscoveredWebSocketEndpoints(finalResult); - - websocketEndpointsLookupFuture.setResult(endpointsResult); + // We insert the provided endpoint at the beginning of the list, so that it is used first. + final int INSERT_INDEX = 0; + if (providedEndpoint instanceof SecureWebSocketRemoteConnectionEndpoint) { + SecureWebSocketRemoteConnectionEndpoint secureEndpoint = (SecureWebSocketRemoteConnectionEndpoint) providedEndpoint; + result.discoveredSecureEndpoints.add(INSERT_INDEX, secureEndpoint); + } else if (providedEndpoint instanceof InsecureWebSocketRemoteConnectionEndpoint) { + InsecureWebSocketRemoteConnectionEndpoint insecureEndpoint = (InsecureWebSocketRemoteConnectionEndpoint) providedEndpoint; + result.discoveredInsecureEndpoints.add(INSERT_INDEX, insecureEndpoint); + } else { + throw new AssertionError(); + } } + + if (moduleDescriptor.isImplicitWebSocketEndpointEnabled()) { + String urlWithoutScheme = "://" + host + ":5443/ws"; + + SecureWebSocketRemoteConnectionEndpoint implicitSecureEndpoint = SecureWebSocketRemoteConnectionEndpoint.from( + WebSocketRemoteConnectionEndpoint.SECURE_WEB_SOCKET_SCHEME + urlWithoutScheme); + result.discoveredSecureEndpoints.add(implicitSecureEndpoint); + + InsecureWebSocketRemoteConnectionEndpoint implicitInsecureEndpoint = InsecureWebSocketRemoteConnectionEndpoint.from( + WebSocketRemoteConnectionEndpoint.INSECURE_WEB_SOCKET_SCHEME + urlWithoutScheme); + result.discoveredInsecureEndpoints.add(implicitInsecureEndpoint); + } + + final LookupConnectionEndpointsResult endpointsResult; + if (result.isEmpty()) { + endpointsResult = new WebSocketEndpointsDiscoveryFailed(result.lookupFailures); + } else { + endpointsResult = new DiscoveredWebSocketEndpoints(result); + } + + websocketEndpointsLookupFuture.setResult(endpointsResult); }); return Collections.singletonList(websocketEndpointsLookupFuture); @@ -238,11 +251,11 @@ public final class XmppWebSocketTransportModule @Override protected void notifyAboutNewOutgoingElements() { - Queue outgoingElementsQueue = connectionInternal.outgoingElementsQueue; + final Queue outgoingElementsQueue = connectionInternal.outgoingElementsQueue; asyncButOrderedOutgoingElementsQueue.performAsyncButOrdered(outgoingElementsQueue, () -> { - // Once new outgoingElement is notified, send the top level stream element obtained by polling. - TopLevelStreamElement topLevelStreamElement = outgoingElementsQueue.poll(); - websocket.send(topLevelStreamElement); + for (TopLevelStreamElement topLevelStreamElement; (topLevelStreamElement = outgoingElementsQueue.poll()) != null;) { + websocket.send(topLevelStreamElement); + } }); } @@ -268,15 +281,11 @@ public final class XmppWebSocketTransportModule @Override public StreamOpenAndCloseFactory getStreamOpenAndCloseFactory() { + // TODO: Create extra class for this? return new StreamOpenAndCloseFactory() { @Override - public AbstractStreamOpen createStreamOpen(CharSequence to, CharSequence from, String id, String lang) { - try { - return new WebSocketOpenElement(JidCreate.domainBareFrom(to)); - } catch (XmppStringprepException e) { - Logger.getAnonymousLogger().log(Level.WARNING, "Couldn't create OpenElement", e); - return null; - } + public AbstractStreamOpen createStreamOpen(DomainBareJid to, CharSequence from, String id, String lang) { + return new WebSocketOpenElement(to); } @Override public AbstractStreamClose createStreamClose() { @@ -295,10 +304,6 @@ public final class XmppWebSocketTransportModule assert result != null; this.result = result; } - - public WebSocketRemoteConnectionEndpointLookup.Result getResult() { - return result; - } } /** @@ -308,10 +313,13 @@ public final class XmppWebSocketTransportModule final class WebSocketEndpointsDiscoveryFailed implements LookupConnectionEndpointsFailed { final List lookupFailures; - WebSocketEndpointsDiscoveryFailed( - WebSocketRemoteConnectionEndpointLookup.Result result) { - assert result != null; - lookupFailures = Collections.unmodifiableList(result.lookupFailures); + WebSocketEndpointsDiscoveryFailed(RemoteConnectionEndpointLookupFailure lookupFailure) { + this(Collections.singletonList(lookupFailure)); + } + + WebSocketEndpointsDiscoveryFailed(List lookupFailures) { + assert lookupFailures != null; + this.lookupFailures = Collections.unmodifiableList(lookupFailures); } @Override diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/XmppWebSocketTransportModuleDescriptor.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/XmppWebSocketTransportModuleDescriptor.java index 292fdceb0..b0d02e7c9 100644 --- a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/XmppWebSocketTransportModuleDescriptor.java +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/XmppWebSocketTransportModuleDescriptor.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Aditya Borikar + * Copyright 2020 Aditya Borikar, 2020-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.net.URISyntaxException; import java.util.HashSet; import java.util.Set; +import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModule; @@ -29,6 +30,7 @@ import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionIn import org.jivesoftware.smack.fsm.StateDescriptor; import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.websocket.XmppWebSocketTransportModule.EstablishingWebSocketConnectionStateDescriptor; +import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint; /** * The descriptor class for {@link XmppWebSocketTransportModule}. @@ -37,12 +39,43 @@ import org.jivesoftware.smack.websocket.XmppWebSocketTransportModule.Establishin * use {@link ModularXmppClientToServerConnectionConfiguration.Builder#addModule(ModularXmppClientToServerConnectionModuleDescriptor)}. */ public final class XmppWebSocketTransportModuleDescriptor extends ModularXmppClientToServerConnectionModuleDescriptor { - private boolean performWebSocketEndpointDiscovery; - private URI uri; + private final boolean performWebSocketEndpointDiscovery; + private final boolean implicitWebSocketEndpoint; + private final URI uri; + private final WebSocketRemoteConnectionEndpoint wsRce; public XmppWebSocketTransportModuleDescriptor(Builder builder) { this.performWebSocketEndpointDiscovery = builder.performWebSocketEndpointDiscovery; + this.implicitWebSocketEndpoint = builder.implicitWebSocketEndpoint; + this.uri = builder.uri; + if (uri != null) { + wsRce = WebSocketRemoteConnectionEndpoint.from(uri); + } else { + wsRce = null; + } + } + + @Override + @SuppressWarnings({"incomplete-switch", "MissingCasesInEnumSwitch"}) + protected void validateConfiguration(ModularXmppClientToServerConnectionConfiguration configuration) { + if (wsRce == null) { + return; + } + + SecurityMode securityMode = configuration.getSecurityMode(); + switch (securityMode) { + case required: + if (!wsRce.isSecureEndpoint()) { + throw new IllegalArgumentException("The provided WebSocket endpoint " + wsRce + " is not a secure endpoint, but the connection configuration requires secure endpoints"); + } + break; + case disabled: + if (wsRce.isSecureEndpoint()) { + throw new IllegalArgumentException("The provided WebSocket endpoint " + wsRce + " is a secure endpoint, but the connection configuration has security disabled"); + } + break; + } } /** @@ -53,6 +86,10 @@ public final class XmppWebSocketTransportModuleDescriptor extends ModularXmppCli return performWebSocketEndpointDiscovery; } + public boolean isImplicitWebSocketEndpointEnabled() { + return implicitWebSocketEndpoint; + } + /** * Returns explicitly configured websocket endpoint uri. * @return uri @@ -61,6 +98,10 @@ public final class XmppWebSocketTransportModuleDescriptor extends ModularXmppCli return uri; } + WebSocketRemoteConnectionEndpoint getExplicitlyProvidedEndpoint() { + return wsRce; + } + @Override protected Set> getStateDescriptors() { Set> res = new HashSet<>(); @@ -99,6 +140,7 @@ public final class XmppWebSocketTransportModuleDescriptor extends ModularXmppCli */ public static final class Builder extends ModularXmppClientToServerConnectionModuleDescriptor.Builder { private boolean performWebSocketEndpointDiscovery = true; + private boolean implicitWebSocketEndpoint = true; private URI uri; private Builder( @@ -119,15 +161,20 @@ public final class XmppWebSocketTransportModuleDescriptor extends ModularXmppCli public Builder explicitlySetWebSocketEndpoint(CharSequence endpoint) throws URISyntaxException { URI endpointUri = new URI(endpoint.toString()); - return explicitlySetWebSocketEndpointAndDiscovery(endpointUri, true); + return explicitlySetWebSocketEndpoint(endpointUri); } - public Builder explicitlySetWebSocketEndpoint(CharSequence endpoint, boolean performWebSocketEndpointDiscovery) + public Builder explicitlySetWebSocketEndpointAndDiscovery(CharSequence endpoint, boolean performWebSocketEndpointDiscovery) throws URISyntaxException { URI endpointUri = new URI(endpoint.toString()); return explicitlySetWebSocketEndpointAndDiscovery(endpointUri, performWebSocketEndpointDiscovery); } + public Builder disableImplicitWebsocketEndpoint() { + implicitWebSocketEndpoint = false; + return this; + } + @Override public ModularXmppClientToServerConnectionModuleDescriptor build() { return new XmppWebSocketTransportModuleDescriptor(this); diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/impl/AbstractWebSocket.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/impl/AbstractWebSocket.java index d7e76e230..1c338e733 100644 --- a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/impl/AbstractWebSocket.java +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/impl/AbstractWebSocket.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Aditya Borikar. + * Copyright 2020 Aditya Borikar, 2020-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,40 +18,92 @@ package org.jivesoftware.smack.websocket.impl; import javax.net.ssl.SSLSession; +import org.jivesoftware.smack.SmackFuture; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; import org.jivesoftware.smack.packet.TopLevelStreamElement; +import org.jivesoftware.smack.packet.XmlEnvironment; import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint; public abstract class AbstractWebSocket { - protected enum WebSocketConnectionPhase { - openFrameSent, - exchangingTopLevelStreamElements + protected final ModularXmppClientToServerConnectionInternal connectionInternal; + + protected final WebSocketRemoteConnectionEndpoint endpoint; + + protected AbstractWebSocket(WebSocketRemoteConnectionEndpoint endpoint, + ModularXmppClientToServerConnectionInternal connectionInternal) { + this.endpoint = endpoint; + this.connectionInternal = connectionInternal; } - protected static String getStreamFromOpenElement(String openElement) { + public final WebSocketRemoteConnectionEndpoint getEndpoint() { + return endpoint; + } + + private String streamOpen; + private String streamClose; + + protected final void onIncomingWebSocketElement(String element) { + // TODO: Once smack-websocket-java15 is there, we have to re-evaluate if the async operation here is still + // required, or if it should only be performed if OkHTTP is used. + if (isOpenElement(element)) { + // Transform the XMPP WebSocket element to a RFC 6120 open tag. + streamOpen = getStreamFromOpenElement(element); + streamClose = connectionInternal.onStreamOpen(streamOpen); + return; + } + + if (isCloseElement(element)) { + connectionInternal.onStreamClosed(); + return; + } + + connectionInternal.withSmackDebugger(debugger -> debugger.onIncomingElementCompleted()); + + // TODO: Do we need to wrap the element again in the stream open to get the + // correct XML scoping (just like the modular TCP connection does)? It appears + // that this not really required, as onStreamOpen() will set the incomingStreamEnvironment, which is used for + // parsing. + String wrappedCompleteElement = streamOpen + element + streamClose; + connectionInternal.parseAndProcessElement(wrappedCompleteElement); + } + + static String getStreamFromOpenElement(String openElement) { String streamElement = openElement.replaceFirst("\\A\\s*\\z", ">"); return streamElement; } - protected static boolean isOpenElement(String text) { + // TODO: Make this method less fragile, e.g. by parsing a little bit into the element to ensure that this is an + // element qualified by the correct namespace. + static boolean isOpenElement(String text) { if (text.startsWith(" element qualified by the correct namespace. The fragility comes due the fact that the element could, + // inter alia, be specified as + // + static boolean isCloseElement(String text) { if (text.startsWith("")) { return true; } return false; } - public abstract void connect(WebSocketRemoteConnectionEndpoint endpoint) throws Throwable; + public abstract SmackFuture getFuture(); - public abstract void send(TopLevelStreamElement element); + public final void send(TopLevelStreamElement element) { + XmlEnvironment outgoingStreamXmlEnvironment = connectionInternal.getOutgoingStreamXmlEnvironment(); + String elementString = element.toXML(outgoingStreamXmlEnvironment).toString(); + send(elementString); + } + + protected abstract void send(String element); public abstract void disconnect(int code, String message); diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/impl/WebSocketFactory.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/impl/WebSocketFactory.java index ddba0f2e8..81e6eed2c 100644 --- a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/impl/WebSocketFactory.java +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/impl/WebSocketFactory.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Florian Schmaus. + * Copyright 2020-2021 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,11 @@ package org.jivesoftware.smack.websocket.impl; import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint; public interface WebSocketFactory { - AbstractWebSocket create(ModularXmppClientToServerConnectionInternal connectionInternal); + AbstractWebSocket create(WebSocketRemoteConnectionEndpoint endpoint, + ModularXmppClientToServerConnectionInternal connectionInternal); } diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/impl/WebSocketFactoryService.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/impl/WebSocketFactoryService.java index 0929c1d96..8eb5e507a 100644 --- a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/impl/WebSocketFactoryService.java +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/impl/WebSocketFactoryService.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Florian Schmaus. + * Copyright 2020-2021 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,12 +20,14 @@ import java.util.Iterator; import java.util.ServiceLoader; import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint; public final class WebSocketFactoryService { private static final ServiceLoader SERVICE_LOADER = ServiceLoader.load(WebSocketFactory.class); - public static AbstractWebSocket createWebSocket(ModularXmppClientToServerConnectionInternal connectionInternal) { + public static AbstractWebSocket createWebSocket(WebSocketRemoteConnectionEndpoint endpoint, + ModularXmppClientToServerConnectionInternal connectionInternal) { assert connectionInternal != null; Iterator websocketFactories = SERVICE_LOADER.iterator(); @@ -34,7 +36,7 @@ public final class WebSocketFactoryService { } WebSocketFactory websocketFactory = websocketFactories.next(); - return websocketFactory.create(connectionInternal); + return websocketFactory.create(endpoint, connectionInternal); } } diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/InsecureWebSocketRemoteConnectionEndpoint.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/InsecureWebSocketRemoteConnectionEndpoint.java new file mode 100644 index 000000000..de013e111 --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/InsecureWebSocketRemoteConnectionEndpoint.java @@ -0,0 +1,39 @@ +/** + * + * Copyright 2020-2021 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.smack.websocket.rce; + +import java.net.URI; + +public class InsecureWebSocketRemoteConnectionEndpoint extends WebSocketRemoteConnectionEndpoint { + + protected InsecureWebSocketRemoteConnectionEndpoint(URI uri) { + super(uri); + } + + @Override + public final boolean isSecureEndpoint() { + return false; + } + + public static final InsecureWebSocketRemoteConnectionEndpoint from(CharSequence cs) { + URI uri = URI.create(cs.toString()); + if (!uri.getScheme().equals(INSECURE_WEB_SOCKET_SCHEME)) { + throw new IllegalArgumentException(uri + " is not a insecure WebSocket"); + } + return new InsecureWebSocketRemoteConnectionEndpoint(uri); + } +} diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/SecureWebSocketRemoteConnectionEndpoint.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/SecureWebSocketRemoteConnectionEndpoint.java new file mode 100644 index 000000000..d4b8bcc29 --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/SecureWebSocketRemoteConnectionEndpoint.java @@ -0,0 +1,39 @@ +/** + * + * Copyright 2020-2021 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.smack.websocket.rce; + +import java.net.URI; + +public class SecureWebSocketRemoteConnectionEndpoint extends WebSocketRemoteConnectionEndpoint { + + protected SecureWebSocketRemoteConnectionEndpoint(URI uri) { + super(uri); + } + + @Override + public final boolean isSecureEndpoint() { + return true; + } + + public static final SecureWebSocketRemoteConnectionEndpoint from(CharSequence cs) { + URI uri = URI.create(cs.toString()); + if (!uri.getScheme().equals(SECURE_WEB_SOCKET_SCHEME)) { + throw new IllegalArgumentException(uri + " is not a secure WebSocket"); + } + return new SecureWebSocketRemoteConnectionEndpoint(uri); + } +} diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/WebSocketRemoteConnectionEndpoint.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/WebSocketRemoteConnectionEndpoint.java index eab43afc3..6de5b9611 100644 --- a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/WebSocketRemoteConnectionEndpoint.java +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/WebSocketRemoteConnectionEndpoint.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Aditya Borikar + * Copyright 2020-2021 Aditya Borikar, Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,66 +20,100 @@ import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; +import java.util.Arrays; import java.util.Collection; -import java.util.Collections; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smack.datatypes.UInt16; import org.jivesoftware.smack.util.rce.RemoteConnectionEndpoint; -public final class WebSocketRemoteConnectionEndpoint implements RemoteConnectionEndpoint { +public abstract class WebSocketRemoteConnectionEndpoint implements RemoteConnectionEndpoint { + + public static final String INSECURE_WEB_SOCKET_SCHEME = "ws"; + public static final String SECURE_WEB_SOCKET_SCHEME = INSECURE_WEB_SOCKET_SCHEME + "s"; private static final Logger LOGGER = Logger.getAnonymousLogger(); private final URI uri; + private final UInt16 port; - public WebSocketRemoteConnectionEndpoint(String uri) throws URISyntaxException { - this(new URI(uri)); - } - - public WebSocketRemoteConnectionEndpoint(URI uri) { + protected WebSocketRemoteConnectionEndpoint(URI uri) { this.uri = uri; - String scheme = uri.getScheme(); - if (!(scheme.equals("ws") || scheme.equals("wss"))) { - throw new IllegalArgumentException("Only allowed protocols are ws and wss"); + int portInt = uri.getPort(); + if (portInt >= 0) { + port = UInt16.from(portInt); + } else { + port = null; } } - public URI getWebSocketEndpoint() { + public final URI getUri() { return uri; } - public boolean isSecureEndpoint() { - if (uri.getScheme().equals("wss")) { - return true; - } - return false; - } - @Override - public CharSequence getHost() { + public final String getHost() { return uri.getHost(); } @Override public UInt16 getPort() { - return UInt16.from(uri.getPort()); + return port; + } + + public abstract boolean isSecureEndpoint(); + + private List inetAddresses; + + private void resolveInetAddressesIfRequired() { + if (inetAddresses != null) { + return; + } + + String host = getHost(); + InetAddress[] addresses; + try { + addresses = InetAddress.getAllByName(host); + } catch (UnknownHostException e) { + LOGGER.log(Level.WARNING, "Could not resolve IP addresses of " + host, e); + return; + } + inetAddresses = Arrays.asList(addresses); } @Override public Collection getInetAddresses() { - try { - InetAddress address = InetAddress.getByName(getHost().toString()); - return Collections.singletonList(address); - } catch (UnknownHostException e) { - LOGGER.log(Level.INFO, "Unknown Host Exception ", e); - } - return null; + resolveInetAddressesIfRequired(); + return inetAddresses; } @Override public String getDescription() { return null; } + + @Override + public String toString() { + return uri.toString(); + } + + public static WebSocketRemoteConnectionEndpoint from(CharSequence uriCharSequence) throws URISyntaxException { + String uriString = uriCharSequence.toString(); + URI uri = URI.create(uriString); + return from(uri); + } + + public static WebSocketRemoteConnectionEndpoint from(URI uri) { + String scheme = uri.getScheme(); + switch (scheme) { + case INSECURE_WEB_SOCKET_SCHEME: + return new InsecureWebSocketRemoteConnectionEndpoint(uri); + case SECURE_WEB_SOCKET_SCHEME: + return new SecureWebSocketRemoteConnectionEndpoint(uri); + default: + throw new IllegalArgumentException("Only allowed protocols are " + INSECURE_WEB_SOCKET_SCHEME + " and " + SECURE_WEB_SOCKET_SCHEME); + } + } } diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/WebSocketRemoteConnectionEndpointLookup.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/WebSocketRemoteConnectionEndpointLookup.java index 4ea558325..347180f69 100644 --- a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/WebSocketRemoteConnectionEndpointLookup.java +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/WebSocketRemoteConnectionEndpointLookup.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Aditya Borikar + * Copyright 2020 Aditya Borikar, Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,9 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; -import java.util.Iterator; +import java.util.Collections; import java.util.List; -import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.altconnections.HttpLookupMethod; import org.jivesoftware.smack.altconnections.HttpLookupMethod.LinkRelation; import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure; @@ -33,9 +32,8 @@ import org.jxmpp.jid.DomainBareJid; public final class WebSocketRemoteConnectionEndpointLookup { - public static Result lookup(DomainBareJid domainBareJid, SecurityMode securityMode) { + public static Result lookup(DomainBareJid domainBareJid) { List lookupFailures = new ArrayList<>(1); - List discoveredRemoteConnectionEndpoints = new ArrayList<>(); List rcUriList = null; try { @@ -45,67 +43,69 @@ public final class WebSocketRemoteConnectionEndpointLookup { } catch (IOException | XmlPullParserException | URISyntaxException e) { lookupFailures.add(new RemoteConnectionEndpointLookupFailure.HttpLookupFailure( domainBareJid, e)); - return new Result(discoveredRemoteConnectionEndpoints, lookupFailures); + return new Result(lookupFailures); } - if (rcUriList.isEmpty()) { - throw new IllegalStateException("No endpoints were found inside host-meta"); + List discoveredSecureEndpoints = new ArrayList<>(rcUriList.size()); + List discoveredInsecureEndpoints = new ArrayList<>(rcUriList.size()); + + for (URI webSocketUri : rcUriList) { + WebSocketRemoteConnectionEndpoint wsRce = WebSocketRemoteConnectionEndpoint.from(webSocketUri); + if (wsRce instanceof SecureWebSocketRemoteConnectionEndpoint) { + SecureWebSocketRemoteConnectionEndpoint secureWsRce = (SecureWebSocketRemoteConnectionEndpoint) wsRce; + discoveredSecureEndpoints.add(secureWsRce); + } else if (wsRce instanceof InsecureWebSocketRemoteConnectionEndpoint) { + InsecureWebSocketRemoteConnectionEndpoint insecureWsRce = (InsecureWebSocketRemoteConnectionEndpoint) wsRce; + discoveredInsecureEndpoints.add(insecureWsRce); + } else { + // WebSocketRemoteConnectionEndpoint.from() must return an instance which type is one of the above. + throw new AssertionError(); + } } - // Convert rcUriList to List - Iterator iterator = rcUriList.iterator(); - List rceList = new ArrayList<>(); - while (iterator.hasNext()) { - rceList.add(new WebSocketRemoteConnectionEndpoint(iterator.next())); - } - - switch (securityMode) { - case ifpossible: - // If security mode equals `if-possible`, give priority to secure endpoints over insecure endpoints. - - // Seprate secure and unsecure endpoints. - List secureEndpointsForSecurityModeIfPossible = new ArrayList<>(); - List insecureEndpointsForSecurityModeIfPossible = new ArrayList<>(); - for (WebSocketRemoteConnectionEndpoint uri : rceList) { - if (uri.isSecureEndpoint()) { - secureEndpointsForSecurityModeIfPossible.add(uri); - } else { - insecureEndpointsForSecurityModeIfPossible.add(uri); - } - } - discoveredRemoteConnectionEndpoints = secureEndpointsForSecurityModeIfPossible; - discoveredRemoteConnectionEndpoints.addAll(insecureEndpointsForSecurityModeIfPossible); - break; - case required: - case disabled: - /** - * If, SecurityMode equals to required, accept wss endpoints (secure endpoints) only or, - * if SecurityMode equals to disabled, accept ws endpoints (unsecure endpoints) only. - */ - for (WebSocketRemoteConnectionEndpoint uri : rceList) { - if ((securityMode.equals(SecurityMode.disabled) && !uri.isSecureEndpoint()) - || (securityMode.equals(SecurityMode.required) && uri.isSecureEndpoint())) { - discoveredRemoteConnectionEndpoints.add(uri); - } - } - break; - default: - } - return new Result(discoveredRemoteConnectionEndpoints, lookupFailures); + return new Result(discoveredSecureEndpoints, discoveredInsecureEndpoints, lookupFailures); } public static final class Result { - public final List discoveredRemoteConnectionEndpoints; + public final List discoveredSecureEndpoints; + public final List discoveredInsecureEndpoints; public final List lookupFailures; - public Result(List discoveredRemoteConnectionEndpoints, + public Result() { + this(Collections.emptyList()); + } + + public Result(List lookupFailures) { + // The list of endpoints needs to be mutable, because maybe a user supplied endpoint will be added to it. + // Hence we do not use Collections.emptyList() as argument for the discovered endpoints. + this(new ArrayList<>(1), new ArrayList<>(1), lookupFailures); + } + + public Result(List discoveredSecureEndpoints, + List discoveredInsecureEndpoints, List lookupFailures) { - this.discoveredRemoteConnectionEndpoints = discoveredRemoteConnectionEndpoints; + this.discoveredSecureEndpoints = discoveredSecureEndpoints; + this.discoveredInsecureEndpoints = discoveredInsecureEndpoints; this.lookupFailures = lookupFailures; } - public List getDiscoveredRemoteConnectionEndpoints() { - return discoveredRemoteConnectionEndpoints; + public boolean isEmpty() { + return discoveredSecureEndpoints.isEmpty() && discoveredInsecureEndpoints.isEmpty(); + } + + public int discoveredEndpointCount() { + return discoveredSecureEndpoints.size() + discoveredInsecureEndpoints.size(); + } + + // TODO: Remove the following methods since the fields are already public? Or make the fields private and use + // the methods? I tend to remove the methods, as their method name is pretty long. But OTOH the fields reference + // mutable datastructes, which is uncommon to be public. + public List getDiscoveredSecureRemoteConnectionEndpoints() { + return discoveredSecureEndpoints; + } + + public List getDiscoveredInsecureRemoteConnectionEndpoints() { + return discoveredInsecureEndpoints; } public List getLookupFailures() { diff --git a/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/XmppWebSocketTransportModuleTest.java b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/XmppWebSocketTransportModuleTest.java index c21a93497..bc0ee1cd0 100644 --- a/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/XmppWebSocketTransportModuleTest.java +++ b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/XmppWebSocketTransportModuleTest.java @@ -17,24 +17,15 @@ package org.jivesoftware.smack.websocket; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; -import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure; -import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure.HttpLookupFailure; -import org.jivesoftware.smack.websocket.XmppWebSocketTransportModule.XmppWebSocketTransport.DiscoveredWebSocketEndpoints; -import org.jivesoftware.smack.websocket.XmppWebSocketTransportModule.XmppWebSocketTransport.WebSocketEndpointsDiscoveryFailed; -import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint; -import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpointLookup.Result; import org.junit.jupiter.api.Test; import org.jxmpp.stringprep.XmppStringprepException; @@ -64,42 +55,6 @@ public class XmppWebSocketTransportModuleTest { assertNotNull(websocketTransportModuleDescriptor); } - @Test - public void websocketEndpointDiscoveryTest() throws URISyntaxException { - XmppWebSocketTransportModuleDescriptor websocketTransportModuleDescriptor = getWebSocketDescriptor(); - ModularXmppClientToServerConnectionInternal connectionInternal = mock(ModularXmppClientToServerConnectionInternal.class); - - XmppWebSocketTransportModule transportModule - = new XmppWebSocketTransportModule(websocketTransportModuleDescriptor, connectionInternal); - - XmppWebSocketTransportModule.XmppWebSocketTransport transport = transportModule.getTransport(); - - assertThrows(AssertionError.class, () -> transport.new DiscoveredWebSocketEndpoints(null)); - assertThrows(AssertionError.class, () -> transport.new WebSocketEndpointsDiscoveryFailed(null)); - - WebSocketRemoteConnectionEndpoint endpoint = new WebSocketRemoteConnectionEndpoint("wss://localhost.org:7443/ws/"); - - List discoveredRemoteConnectionEndpoints = new ArrayList<>(); - discoveredRemoteConnectionEndpoints.add(endpoint); - - HttpLookupFailure httpLookupFailure = new RemoteConnectionEndpointLookupFailure.HttpLookupFailure(null, null); - List failureList = new ArrayList<>(); - failureList.add(httpLookupFailure); - Result result = new Result(discoveredRemoteConnectionEndpoints, failureList); - - DiscoveredWebSocketEndpoints discoveredWebSocketEndpoints = transport.new DiscoveredWebSocketEndpoints(result); - assertNotNull(discoveredWebSocketEndpoints.getResult()); - - WebSocketEndpointsDiscoveryFailed endpointsDiscoveryFailed = transport.new WebSocketEndpointsDiscoveryFailed(result); - assertNotNull(endpointsDiscoveryFailed.toString()); - } - - @Test - public void websocketConnectedResultTest() throws URISyntaxException { - WebSocketRemoteConnectionEndpoint connectedEndpoint = new WebSocketRemoteConnectionEndpoint("wss://localhost.org:7443/ws/"); - assertNotNull(new XmppWebSocketTransportModule.WebSocketConnectedResult(connectedEndpoint)); - } - @Test public void lookupConnectionEndpointsTest() throws URISyntaxException { XmppWebSocketTransportModuleDescriptor websocketTransportModuleDescriptor = getWebSocketDescriptor(); diff --git a/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/rce/WebSocketRemoteConnectionEndpointTest.java b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/rce/WebSocketRemoteConnectionEndpointTest.java index 075abc7c4..b189f0bf7 100644 --- a/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/rce/WebSocketRemoteConnectionEndpointTest.java +++ b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/rce/WebSocketRemoteConnectionEndpointTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Aditya Borikar + * Copyright 2020 Aditya Borikar, 2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,20 +26,22 @@ import org.jivesoftware.smack.datatypes.UInt16; import org.junit.jupiter.api.Test; public class WebSocketRemoteConnectionEndpointTest { + @Test public void endpointTest() throws URISyntaxException { String endpointString = "ws://fooDomain.org:7070/ws/"; - WebSocketRemoteConnectionEndpoint endpoint = new WebSocketRemoteConnectionEndpoint(endpointString); + WebSocketRemoteConnectionEndpoint endpoint = WebSocketRemoteConnectionEndpoint.from(endpointString); assertEquals("fooDomain.org", endpoint.getHost()); assertEquals(UInt16.from(7070), endpoint.getPort()); - assertEquals(endpointString, endpoint.getWebSocketEndpoint().toString()); + assertEquals(endpointString, endpoint.getUri().toString()); } @Test public void faultyEndpointTest() { String faultyProtocolString = "wst://fooDomain.org:7070/ws/"; assertThrows(IllegalArgumentException.class, () -> { - new WebSocketRemoteConnectionEndpoint(faultyProtocolString); + WebSocketRemoteConnectionEndpoint.from(faultyProtocolString); }); } + } diff --git a/smack-websocket/src/testFixtures/java/org/jivesoftware/smack/websocket/test/WebSocketFactoryServiceTestUtil.java b/smack-websocket/src/testFixtures/java/org/jivesoftware/smack/websocket/test/WebSocketFactoryServiceTestUtil.java index ddafb5bf2..cb84133da 100644 --- a/smack-websocket/src/testFixtures/java/org/jivesoftware/smack/websocket/test/WebSocketFactoryServiceTestUtil.java +++ b/smack-websocket/src/testFixtures/java/org/jivesoftware/smack/websocket/test/WebSocketFactoryServiceTestUtil.java @@ -1,6 +1,6 @@ /** * - * Copyright 2020 Florian Schmaus. + * Copyright 2020-2021 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,16 +20,21 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; +import java.net.URISyntaxException; + import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; import org.jivesoftware.smack.websocket.impl.AbstractWebSocket; import org.jivesoftware.smack.websocket.impl.WebSocketFactoryService; +import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint; public class WebSocketFactoryServiceTestUtil { - public static void createWebSocketTest(Class expected) { + public static void createWebSocketTest(Class expected) throws URISyntaxException { + WebSocketRemoteConnectionEndpoint endpoint = WebSocketRemoteConnectionEndpoint.from("wss://example.org"); + ModularXmppClientToServerConnectionInternal connectionInternal = mock(ModularXmppClientToServerConnectionInternal.class); - AbstractWebSocket websocket = WebSocketFactoryService.createWebSocket(connectionInternal); + AbstractWebSocket websocket = WebSocketFactoryService.createWebSocket(endpoint, connectionInternal); assertEquals(expected, websocket.getClass()); }