diff --git a/build.gradle b/build.gradle index ab804a0d1..49aa67092 100644 --- a/build.gradle +++ b/build.gradle @@ -288,6 +288,9 @@ tasks.withType(Javadoc) { // fixtures, and we want to have mockito also available in // test, so we use API here. testFixturesApi "org.mockito:mockito-core:3.3.3" + + // To mock final classes + testImplementation 'org.mockito:mockito-inline:3.3.3' testImplementation 'com.jamesmurty.utils:java-xmlbuilder:1.2' errorprone 'com.google.errorprone:error_prone_core:2.3.4' diff --git a/settings.gradle b/settings.gradle index 875a6e88c..460e5e403 100644 --- a/settings.gradle +++ b/settings.gradle @@ -30,6 +30,7 @@ include 'smack-core', 'smack-omemo-signal-integration-test', 'smack-repl', 'smack-openpgp', + 'smack-websocket', 'smack-xmlparser', 'smack-xmlparser-stax', 'smack-xmlparser-xpp3' 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 191dd32e4..7f677871e 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 @@ -61,13 +61,13 @@ import org.jivesoftware.smack.fsm.StateTransitionResult; import org.jivesoftware.smack.fsm.StateTransitionResult.AttemptResult; import org.jivesoftware.smack.internal.AbstractStats; import org.jivesoftware.smack.internal.SmackTlsContext; +import org.jivesoftware.smack.packet.AbstractStreamClose; import org.jivesoftware.smack.packet.AbstractStreamOpen; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Nonza; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.Stanza; -import org.jivesoftware.smack.packet.StreamClose; import org.jivesoftware.smack.packet.StreamError; import org.jivesoftware.smack.packet.TopLevelStreamElement; import org.jivesoftware.smack.packet.XmlEnvironment; @@ -138,6 +138,11 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne ModularXmppClientToServerConnection.this.notifyConnectionError(e); } + @Override + public void setCurrentConnectionExceptionAndNotify(Exception exception) { + ModularXmppClientToServerConnection.this.setCurrentConnectionExceptionAndNotify(exception); + } + @Override public void onStreamOpen(XmlPullParser parser) { ModularXmppClientToServerConnection.this.onStreamOpen(parser); @@ -179,6 +184,11 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne return inputOutputFilters.listIterator(inputOutputFilters.size()); } + @Override + public void waitForFeaturesReceived(String waitFor) throws InterruptedException, SmackException, XMPPException { + ModularXmppClientToServerConnection.this.waitForFeaturesReceived(waitFor); + } + @Override public void newStreamOpenWaitForFeaturesSequence(String waitFor) throws InterruptedException, SmackException, XMPPException { @@ -930,7 +940,9 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) { closingStreamReceived = false; - boolean streamCloseIssued = outgoingElementsQueue.offerAndShutdown(StreamClose.INSTANCE); + StreamOpenAndCloseFactory openAndCloseFactory = activeTransport.getStreamOpenAndCloseFactory(); + AbstractStreamClose closeStreamElement = openAndCloseFactory.createStreamClose(); + boolean streamCloseIssued = outgoingElementsQueue.offerAndShutdown(closeStreamElement); if (streamCloseIssued) { activeTransport.notifyAboutNewOutgoingElements(); 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 0462307c1..139f1194f 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 @@ -85,6 +85,8 @@ public abstract class ModularXmppClientToServerConnectionInternal { public abstract void notifyConnectionError(Exception e); + public abstract void setCurrentConnectionExceptionAndNotify(Exception exception); + public abstract void onStreamOpen(XmlPullParser parser); public abstract void onStreamClosed(); @@ -99,6 +101,8 @@ public abstract class ModularXmppClientToServerConnectionInternal { public abstract ListIterator getXmppInputOutputFilterEndIterator(); + public abstract void waitForFeaturesReceived(String waitFor) throws InterruptedException, SmackException, XMPPException; + public abstract void newStreamOpenWaitForFeaturesSequence(String waitFor) throws InterruptedException, NoResponseException, NotConnectedException, SmackException, XMPPException; 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 1b6a3d1e0..698e7888d 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 @@ -19,6 +19,7 @@ package org.jivesoftware.smack.packet; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.packet.StreamOpen.StreamContentNamespace; import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.util.XmlStringBuilder; /** * AbstractStreamOpen is actually a {@link TopLevelStreamElement}, however we @@ -80,4 +81,9 @@ public abstract class AbstractStreamOpen implements Nonza { throw new IllegalStateException(); } } + + protected final void addCommonAttributes(XmlStringBuilder xml) { + xml.optAttribute("to", to); + xml.optAttribute("version", VERSION); + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/rce/RemoteConnectionEndpointLookupFailure.java b/smack-core/src/main/java/org/jivesoftware/smack/util/rce/RemoteConnectionEndpointLookupFailure.java index 73ce8a4b0..7e92b13dc 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/rce/RemoteConnectionEndpointLookupFailure.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/rce/RemoteConnectionEndpointLookupFailure.java @@ -18,6 +18,7 @@ package org.jivesoftware.smack.util.rce; import org.jivesoftware.smack.util.ToStringUtil; +import org.jxmpp.jid.DomainBareJid; import org.minidns.dnsname.DnsName; public abstract class RemoteConnectionEndpointLookupFailure { @@ -67,4 +68,17 @@ public abstract class RemoteConnectionEndpointLookupFailure { return dnsName; } } + + public static class HttpLookupFailure extends RemoteConnectionEndpointLookupFailure { + private final DomainBareJid host; + + public HttpLookupFailure(DomainBareJid host, Exception exception) { + super("Http lookup exception for " + host, exception); + this.host = host; + } + + public DomainBareJid getHost() { + return host; + } + } } diff --git a/smack-core/src/main/resources/org.jivesoftware.smack/smack-config.xml b/smack-core/src/main/resources/org.jivesoftware.smack/smack-config.xml index e1be746e0..b80048942 100644 --- a/smack-core/src/main/resources/org.jivesoftware.smack/smack-config.xml +++ b/smack-core/src/main/resources/org.jivesoftware.smack/smack-config.xml @@ -20,6 +20,7 @@ org.jivesoftware.smack.android.AndroidSmackInitializer org.jivesoftware.smack.java7.Java7SmackInitializer org.jivesoftware.smack.im.SmackImInitializer + org.jivesoftware.smack.websocket.WebsocketInitializer org.jivesoftware.smackx.omemo.OmemoInitializer org.jivesoftware.smackx.ox.util.OpenPgpInitializer diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java index 204562fd4..4f5e3d6c1 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java @@ -44,6 +44,7 @@ import org.jivesoftware.smack.tcp.XMPPTCPConnection; import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; import org.jivesoftware.smack.util.MultiMap; import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.websocket.XmppWebsocketTransportModuleDescriptor; import org.jivesoftware.smackx.admin.ServiceAdministrationManager; import org.jivesoftware.smackx.iqregister.AccountManager; @@ -86,6 +87,15 @@ public class XmppConnectionManager { .applyExtraConfguration(cb -> cb.removeModule(CompressionModuleDescriptor.class)) .build() ); + addConnectionDescriptor( + XmppConnectionDescriptor.buildWith(ModularXmppClientToServerConnection.class, ModularXmppClientToServerConnectionConfiguration.class, ModularXmppClientToServerConnectionConfiguration.Builder.class) + .withNickname("modular-websocket") + .applyExtraConfguration(cb -> { + cb.removeAllModules(); + cb.addModule(XmppWebsocketTransportModuleDescriptor.class); + }) + .build() + ); } catch (NoSuchMethodException | SecurityException e) { throw new AssertionError(e); } diff --git a/smack-java8-full/build.gradle b/smack-java8-full/build.gradle index 8586a0005..5fbf1ddc8 100644 --- a/smack-java8-full/build.gradle +++ b/smack-java8-full/build.gradle @@ -12,6 +12,7 @@ dependencies { api project(':smack-openpgp') api project(':smack-resolver-minidns') api project(':smack-resolver-minidns-dox') + api project(':smack-websocket') api project(':smack-tcp') testImplementation 'com.google.guava:guava:28.2-jre' diff --git a/smack-java8-full/src/test/resources/state-graph.dot b/smack-java8-full/src/test/resources/state-graph.dot index 650f3bff6..8c41183a5 100644 --- a/smack-java8-full/src/test/resources/state-graph.dot +++ b/smack-java8-full/src/test/resources/state-graph.dot @@ -1,6 +1,6 @@ digraph { "Disconnected" -> "LookupRemoteConnectionEndpoints"; - "LookupRemoteConnectionEndpoints" -> "EstablishingTcpConnection"; + "LookupRemoteConnectionEndpoints" -> "EstablishingTcpConnection" [xlabel="1"]; "EstablishingTcpConnection" -> "EstablishTls (RFC 6120 § 5)" [xlabel="1"]; "EstablishTls (RFC 6120 § 5)" -> "ConnectedButUnauthenticated"; "ConnectedButUnauthenticated" -> "Bind2 (XEP-0386)" [xlabel="1"]; @@ -32,5 +32,7 @@ digraph { "ConnectedButUnauthenticated" -> "InstantShutdown" [xlabel="5"]; "ConnectedButUnauthenticated" [ style=filled ] "EstablishingTcpConnection" -> "ConnectedButUnauthenticated" [xlabel="2"]; + "LookupRemoteConnectionEndpoints" -> "EstablishingWebsocketConnection" [xlabel="2"]; + "EstablishingWebsocketConnection" -> "ConnectedButUnauthenticated"; "Disconnected" [ style=filled ] } 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 new file mode 100644 index 000000000..035276fab --- /dev/null +++ b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/WebsocketConnection.java @@ -0,0 +1,53 @@ +/** + * + * Copyright 2020 Aditya Borikar + * + * This file is part of smack-repl. + * + * smack-repl is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.igniterealtime.smack.smackrepl; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +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.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]); + + // 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); + builder.addModule(websocketBuilder.build()); + + ModularXmppClientToServerConnectionConfiguration config = builder.build(); + ModularXmppClientToServerConnection connection = new ModularXmppClientToServerConnection(config); + + connection.connect(); + connection.login(); + connection.disconnect(); + } +} diff --git a/smack-websocket/build.gradle b/smack-websocket/build.gradle new file mode 100644 index 000000000..43d205f36 --- /dev/null +++ b/smack-websocket/build.gradle @@ -0,0 +1,10 @@ +description = """\ +Smack for standard XMPP connections over Websockets.""" + +dependencies { + compile project(':smack-core') + + testFixturesApi(testFixtures(project(":smack-core"))) + + implementation("com.squareup.okhttp3:okhttp:4.6.0") +} 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 new file mode 100644 index 000000000..ba683fc75 --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/WebsocketConnectionAttemptState.java @@ -0,0 +1,101 @@ +/** + * + * Copyright 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. + * 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; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.websocket.XmppWebsocketTransportModule.EstablishingWebsocketConnectionState; +import org.jivesoftware.smack.websocket.implementations.AbstractWebsocket; +import org.jivesoftware.smack.websocket.implementations.WebsocketImplProvider; +import org.jivesoftware.smack.websocket.implementations.okhttp.OkHttpWebsocket; +import org.jivesoftware.smack.websocket.rce.WebsocketRemoteConnectionEndpoint; + +public final class WebsocketConnectionAttemptState { + private final ModularXmppClientToServerConnectionInternal connectionInternal; + private final XmppWebsocketTransportModule.XmppWebsocketTransport.DiscoveredWebsocketEndpoints discoveredEndpoints; + + private WebsocketRemoteConnectionEndpoint connectedEndpoint; + + WebsocketConnectionAttemptState(ModularXmppClientToServerConnectionInternal connectionInternal, + XmppWebsocketTransportModule.XmppWebsocketTransport.DiscoveredWebsocketEndpoints discoveredWebsocketEndpoints, + EstablishingWebsocketConnectionState establishingWebsocketConnectionState) { + assert discoveredWebsocketEndpoints != null; + this.connectionInternal = connectionInternal; + this.discoveredEndpoints = discoveredWebsocketEndpoints; + } + + /** + * Establish a websocket connection with one of the discoveredRemoteConnectionEndpoints.
+ * + * @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; + + if (endpoints.isEmpty()) { + throw new WebsocketException(new Throwable("No Endpoints discovered to establish connection")); + } + + List connectionFailureList = new ArrayList<>(); + AbstractWebsocket websocket; + + try { + // Obtain desired websocket implementation by using WebsocketImplProvider + websocket = WebsocketImplProvider.getWebsocketImpl(OkHttpWebsocket.class, connectionInternal, discoveredEndpoints); + } catch (NoSuchMethodException | SecurityException | InstantiationException | + IllegalAccessException | IllegalArgumentException | InvocationTargetException exception) { + throw new WebsocketException(exception); + } + + // 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); + } + } + } + + assert connectedEndpoint != null; + + // Return connected websocket when no failure occurs. + return websocket; + } + + /** + * Returns the connected websocket endpoint. + * + * @return connected websocket endpoint + */ + public WebsocketRemoteConnectionEndpoint getConnectedEndpoint() { + return connectedEndpoint; + } +} 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 new file mode 100644 index 000000000..ba508001c --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/WebsocketException.java @@ -0,0 +1,38 @@ +/** + * + * Copyright 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. + * 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; + +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); + } + + public List getThrowableList() { + return throwableList; + } +} diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/WebsocketInitializer.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/WebsocketInitializer.java new file mode 100644 index 000000000..18b3b17b1 --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/WebsocketInitializer.java @@ -0,0 +1,28 @@ +/** + * + * Copyright 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. + * 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; + +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.initializer.UrlInitializer; + +public final class WebsocketInitializer extends UrlInitializer { + + static { + SmackConfiguration.addModule(XmppWebsocketTransportModuleDescriptor.class); + } + +} 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 new file mode 100644 index 000000000..77f16f27c --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/XmppWebsocketTransportModule.java @@ -0,0 +1,325 @@ +/** + * + * Copyright 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. + * 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; + +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.SmackFuture; +import org.jivesoftware.smack.SmackFuture.InternalSmackFuture; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.ConnectedButUnauthenticatedStateDescriptor; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.LookupRemoteConnectionEndpointsStateDescriptor; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModule; +import org.jivesoftware.smack.c2s.StreamOpenAndCloseFactory; +import org.jivesoftware.smack.c2s.XmppClientToServerTransport; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.c2s.internal.WalkStateGraphContext; +import org.jivesoftware.smack.fsm.State; +import org.jivesoftware.smack.fsm.StateDescriptor; +import org.jivesoftware.smack.fsm.StateTransitionResult; +import org.jivesoftware.smack.fsm.StateTransitionResult.AttemptResult; +import org.jivesoftware.smack.packet.AbstractStreamClose; +import org.jivesoftware.smack.packet.AbstractStreamOpen; +import org.jivesoftware.smack.packet.TopLevelStreamElement; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure; +import org.jivesoftware.smack.websocket.XmppWebsocketTransportModule.XmppWebsocketTransport.DiscoveredWebsocketEndpoints; +import org.jivesoftware.smack.websocket.elements.WebsocketCloseElement; +import org.jivesoftware.smack.websocket.elements.WebsocketOpenElement; +import org.jivesoftware.smack.websocket.implementations.AbstractWebsocket; +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. + */ +public final class XmppWebsocketTransportModule + extends ModularXmppClientToServerConnectionModule { + private final XmppWebsocketTransport websocketTransport; + + private AbstractWebsocket websocket; + + protected XmppWebsocketTransportModule(XmppWebsocketTransportModuleDescriptor moduleDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + super(moduleDescriptor, connectionInternal); + + websocketTransport = new XmppWebsocketTransport(connectionInternal); + } + + @Override + protected XmppWebsocketTransport getTransport() { + return websocketTransport; + } + + static final class EstablishingWebsocketConnectionStateDescriptor extends StateDescriptor { + private EstablishingWebsocketConnectionStateDescriptor() { + super(XmppWebsocketTransportModule.EstablishingWebsocketConnectionState.class); + addPredeccessor(LookupRemoteConnectionEndpointsStateDescriptor.class); + addSuccessor(ConnectedButUnauthenticatedStateDescriptor.class); + + // This states preference to TCP transports over this Websocket transport implementation. + declareInferiorityTo("org.jivesoftware.smack.tcp.XmppTcpTransportModule$EstablishingTcpConnectionStateDescriptor"); + } + + @Override + protected State constructState(ModularXmppClientToServerConnectionInternal connectionInternal) { + XmppWebsocketTransportModule websocketTransportModule = connectionInternal.connection.getConnectionModuleFor( + XmppWebsocketTransportModuleDescriptor.class); + return websocketTransportModule.constructEstablishingWebsocketConnectionState(this, connectionInternal); + } + } + + final class EstablishingWebsocketConnectionState extends State { + protected EstablishingWebsocketConnectionState(StateDescriptor stateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + super(stateDescriptor, connectionInternal); + } + + @Override + public AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) + throws IOException, SmackException, InterruptedException, XMPPException { + WebsocketConnectionAttemptState connectionAttemptState = new WebsocketConnectionAttemptState( + connectionInternal, discoveredWebsocketEndpoints, this); + + try { + websocket = connectionAttemptState.establishWebsocketConnection(); + } catch (InterruptedException | WebsocketException e) { + StateTransitionResult.Failure failure = new StateTransitionResult.FailureCausedByException(e); + return failure; + } + + connectionInternal.setTransport(websocketTransport); + + WebsocketRemoteConnectionEndpoint connectedEndpoint = connectionAttemptState.getConnectedEndpoint(); + + // Construct a WebsocketConnectedResult using the connected endpoint. + return new WebsocketConnectedResult(connectedEndpoint); + } + } + + public EstablishingWebsocketConnectionState constructEstablishingWebsocketConnectionState( + EstablishingWebsocketConnectionStateDescriptor establishingWebsocketConnectionStateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + return new EstablishingWebsocketConnectionState(establishingWebsocketConnectionStateDescriptor, + connectionInternal); + } + + public static final class WebsocketConnectedResult extends StateTransitionResult.Success { + final WebsocketRemoteConnectionEndpoint connectedEndpoint; + + public WebsocketConnectedResult(WebsocketRemoteConnectionEndpoint connectedEndpoint) { + super("Websocket connection establised with endpoint: " + connectedEndpoint.getWebsocketEndpoint()); + this.connectedEndpoint = connectedEndpoint; + } + } + + private DiscoveredWebsocketEndpoints discoveredWebsocketEndpoints; + + /** + * Transport class for {@link ModularXmppClientToServerConnectionModule}'s websocket implementation. + */ + public final class XmppWebsocketTransport extends XmppClientToServerTransport { + + AsyncButOrdered> asyncButOrderedOutgoingElementsQueue; + + protected XmppWebsocketTransport(ModularXmppClientToServerConnectionInternal connectionInternal) { + super(connectionInternal); + asyncButOrderedOutgoingElementsQueue = new AsyncButOrdered>(); + } + + @Override + protected void resetDiscoveredConnectionEndpoints() { + discoveredWebsocketEndpoints = null; + } + + @Override + protected List> lookupConnectionEndpoints() { + // Assert that there are no stale discovered endpoints prior performing the lookup. + assert discoveredWebsocketEndpoints == null; + + InternalSmackFuture websocketEndpointsLookupFuture = new InternalSmackFuture<>(); + + connectionInternal.asyncGo(() -> { + + WebsocketRemoteConnectionEndpoint providedEndpoint = null; + + // Check if there is a websocket endpoint already configured. + URI uri = moduleDescriptor.getExplicitlyProvidedUri(); + if (uri != null) { + providedEndpoint = new WebsocketRemoteConnectionEndpoint(uri); + } + + 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); + } + + // 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); + } + }); + + return Collections.singletonList(websocketEndpointsLookupFuture); + } + + @Override + protected void loadConnectionEndpoints(LookupConnectionEndpointsSuccess lookupConnectionEndpointsSuccess) { + discoveredWebsocketEndpoints = (DiscoveredWebsocketEndpoints) lookupConnectionEndpointsSuccess; + } + + @Override + protected void afterFiltersClosed() { + } + + @Override + protected void disconnect() { + websocket.disconnect(1000, "Websocket closed normally"); + } + + @Override + protected void notifyAboutNewOutgoingElements() { + 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); + }); + } + + @Override + public SSLSession getSslSession() { + return websocket.getSSLSession(); + } + + @Override + public boolean isTransportSecured() { + return websocket.isConnectionSecure(); + } + + @Override + public boolean isConnected() { + return websocket.isConnected(); + } + + @Override + public Stats getStats() { + return null; + } + + @Override + public StreamOpenAndCloseFactory getStreamOpenAndCloseFactory() { + 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; + } + } + @Override + public AbstractStreamClose createStreamClose() { + return new WebsocketCloseElement(); + } + }; + } + + /** + * Contains {@link Result} for successfully discovered endpoints. + */ + public final class DiscoveredWebsocketEndpoints implements LookupConnectionEndpointsSuccess { + final WebsocketRemoteConnectionEndpointLookup.Result result; + + DiscoveredWebsocketEndpoints(Result result) { + assert result != null; + this.result = result; + } + + public WebsocketRemoteConnectionEndpointLookup.Result getResult() { + return result; + } + } + + /** + * Contains list of {@link RemoteConnectionEndpointLookupFailure} when no endpoint + * could be found during http lookup. + */ + final class WebsocketEndpointsDiscoveryFailed implements LookupConnectionEndpointsFailed { + final List lookupFailures; + + WebsocketEndpointsDiscoveryFailed( + WebsocketRemoteConnectionEndpointLookup.Result result) { + assert result != null; + lookupFailures = Collections.unmodifiableList(result.lookupFailures); + } + + @Override + public String toString() { + StringBuilder str = new StringBuilder(); + StringUtils.appendTo(lookupFailures, str); + return str.toString(); + } + } + } +} 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 new file mode 100644 index 000000000..07a3e2e84 --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/XmppWebsocketTransportModuleDescriptor.java @@ -0,0 +1,136 @@ +/** + * + * Copyright 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. + * 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; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashSet; +import java.util.Set; + +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModule; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModuleDescriptor; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.fsm.StateDescriptor; +import org.jivesoftware.smack.util.Objects; +import org.jivesoftware.smack.websocket.XmppWebsocketTransportModule.EstablishingWebsocketConnectionStateDescriptor; + +/** + * The descriptor class for {@link XmppWebsocketTransportModule}. + *
+ * To add {@link XmppWebsocketTransportModule} to {@link ModularXmppClientToServerConnection}, + * use {@link ModularXmppClientToServerConnectionConfiguration.Builder#addModule(ModularXmppClientToServerConnectionModuleDescriptor)}. + */ +public final class XmppWebsocketTransportModuleDescriptor extends ModularXmppClientToServerConnectionModuleDescriptor { + private boolean performWebsocketEndpointDiscovery; + private URI uri; + + public XmppWebsocketTransportModuleDescriptor(Builder builder) { + this.performWebsocketEndpointDiscovery = builder.performWebsocketEndpointDiscovery; + this.uri = builder.uri; + } + + /** + * Returns true if websocket endpoint discovery is true, returns false otherwise. + * @return boolean + */ + public boolean isWebsocketEndpointDiscoveryEnabled() { + return performWebsocketEndpointDiscovery; + } + + /** + * Returns explicitly configured websocket endpoint uri. + * @return uri + */ + public URI getExplicitlyProvidedUri() { + return uri; + } + + @Override + protected Set> getStateDescriptors() { + Set> res = new HashSet<>(); + res.add(EstablishingWebsocketConnectionStateDescriptor.class); + return res; + } + + @Override + protected ModularXmppClientToServerConnectionModule constructXmppConnectionModule( + ModularXmppClientToServerConnectionInternal connectionInternal) { + return new XmppWebsocketTransportModule(this, connectionInternal); + } + + /** + * Returns a new instance of {@link Builder}. + *
+ * @return Builder + * @param connectionConfigurationBuilder {@link ModularXmppClientToServerConnectionConfiguration.Builder}. + */ + public static Builder getBuilder( + ModularXmppClientToServerConnectionConfiguration.Builder connectionConfigurationBuilder) { + return new Builder(connectionConfigurationBuilder); + } + + /** + * Builder class for {@link XmppWebsocketTransportModuleDescriptor}. + *
+ * To obtain an instance of {@link XmppWebsocketTransportModuleDescriptor.Builder}, use {@link XmppWebsocketTransportModuleDescriptor#getBuilder(ModularXmppClientToServerConnectionConfiguration.Builder)} method. + *
+ * Use {@link Builder#explicitlySetWebsocketEndpoint(URI)} to configure the URI of an endpoint as a backup in case connection couldn't be established with endpoints through http lookup. + *
+ * Use {@link Builder#explicitlySetWebsocketEndpointAndDiscovery(URI, boolean)} to configure endpoint and disallow websocket endpoint discovery through http lookup. + * By default, {@link Builder#performWebsocketEndpointDiscovery} is set to true. + *
+ * Use {@link Builder#build()} to obtain {@link XmppWebsocketTransportModuleDescriptor}. + */ + public static final class Builder extends ModularXmppClientToServerConnectionModuleDescriptor.Builder { + private boolean performWebsocketEndpointDiscovery = true; + private URI uri; + + private Builder( + ModularXmppClientToServerConnectionConfiguration.Builder connectionConfigurationBuilder) { + super(connectionConfigurationBuilder); + } + + public Builder explicitlySetWebsocketEndpoint(URI endpoint) { + return explicitlySetWebsocketEndpointAndDiscovery(endpoint, true); + } + + public Builder explicitlySetWebsocketEndpointAndDiscovery(URI endpoint, boolean performWebsocketEndpointDiscovery) { + Objects.requireNonNull(endpoint, "Provided endpoint URI must not be null"); + this.uri = endpoint; + this.performWebsocketEndpointDiscovery = performWebsocketEndpointDiscovery; + return this; + } + + public Builder explicitlySetWebsocketEndpoint(CharSequence endpoint) throws URISyntaxException { + URI endpointUri = new URI(endpoint.toString()); + return explicitlySetWebsocketEndpointAndDiscovery(endpointUri, true); + } + + public Builder explicitlySetWebsocketEndpoint(CharSequence endpoint, boolean performWebsocketEndpointDiscovery) + throws URISyntaxException { + URI endpointUri = new URI(endpoint.toString()); + return explicitlySetWebsocketEndpointAndDiscovery(endpointUri, performWebsocketEndpointDiscovery); + } + + @Override + public ModularXmppClientToServerConnectionModuleDescriptor build() { + return new XmppWebsocketTransportModuleDescriptor(this); + } + } +} diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/elements/AbstractWebsocketNonza.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/elements/AbstractWebsocketNonza.java new file mode 100644 index 000000000..3a95124ae --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/elements/AbstractWebsocketNonza.java @@ -0,0 +1,47 @@ +/** + * + * Copyright 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. + * 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.elements; + +import org.jivesoftware.smack.packet.Nonza; +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.util.XmlStringBuilder; + +import org.jxmpp.jid.DomainBareJid; + +public abstract class AbstractWebsocketNonza implements Nonza { + public static final String NAMESPACE = "urn:ietf:params:xml:ns:xmpp-framing"; + private static final String VERSION = "1.0"; + private final DomainBareJid to; + + public AbstractWebsocketNonza(DomainBareJid jid) { + this.to = jid; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { + XmlStringBuilder xml = new XmlStringBuilder(this, xmlEnvironment); + xml.attribute("to", to.toString()); + xml.attribute("version", VERSION); + xml.closeEmptyElement(); + return xml; + } +} diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/elements/WebsocketCloseElement.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/elements/WebsocketCloseElement.java new file mode 100644 index 000000000..d455336c0 --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/elements/WebsocketCloseElement.java @@ -0,0 +1,49 @@ +/** + * + * Copyright 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. + * 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.elements; + +import javax.xml.namespace.QName; + +import org.jivesoftware.smack.packet.AbstractStreamClose; +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.util.XmlStringBuilder; + +public final class WebsocketCloseElement extends AbstractStreamClose { + public static final String ELEMENT = "close"; + public static final String NAMESPACE = "urn:ietf:params:xml:ns:xmpp-framing"; + public static final QName QNAME = new QName(NAMESPACE, ELEMENT); + + public WebsocketCloseElement() { + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public CharSequence toXML(XmlEnvironment xmlEnvironment) { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.closeEmptyElement(); + return xml; + } +} diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/elements/WebsocketOpenElement.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/elements/WebsocketOpenElement.java new file mode 100644 index 000000000..5660018cf --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/elements/WebsocketOpenElement.java @@ -0,0 +1,54 @@ +/** + * + * Copyright 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. + * 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.elements; + +import javax.xml.namespace.QName; + +import org.jivesoftware.smack.packet.AbstractStreamOpen; +import org.jivesoftware.smack.packet.StreamOpen.StreamContentNamespace; +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.util.XmlStringBuilder; + +import org.jxmpp.jid.DomainBareJid; + +public final class WebsocketOpenElement extends AbstractStreamOpen { + public static final String ELEMENT = "open"; + public static final String NAMESPACE = "urn:ietf:params:xml:ns:xmpp-framing"; + public static final QName QNAME = new QName(NAMESPACE, ELEMENT); + + public WebsocketOpenElement(DomainBareJid to) { + super(to, null, null, null, StreamContentNamespace.client); + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public CharSequence toXML(XmlEnvironment xmlEnvironment) { + XmlStringBuilder xml = new XmlStringBuilder(this); + addCommonAttributes(xml); + xml.closeEmptyElement(); + return xml; + } +} diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/elements/package-info.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/elements/package-info.java new file mode 100644 index 000000000..4b992c9c4 --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/elements/package-info.java @@ -0,0 +1,20 @@ +/** + * + * Copyright 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. + * 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. + */ +/** + * This package contains Stanzas required to open and close stream. + */ +package org.jivesoftware.smack.websocket.elements; diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/implementations/AbstractWebsocket.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/implementations/AbstractWebsocket.java new file mode 100644 index 000000000..8341c2f1b --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/implementations/AbstractWebsocket.java @@ -0,0 +1,63 @@ +/** + * + * Copyright 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. + * 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.implementations; + +import javax.net.ssl.SSLSession; + +import org.jivesoftware.smack.packet.TopLevelStreamElement; +import org.jivesoftware.smack.websocket.rce.WebsocketRemoteConnectionEndpoint; + +public abstract class AbstractWebsocket { + + protected enum WebsocketConnectionPhase { + openFrameSent, + exchangingTopLevelStreamElements + } + + protected static String getStreamFromOpenElement(String openElement) { + String streamElement = openElement.replaceFirst("\\A\\s*\\z", ">"); + return streamElement; + } + + protected static boolean isOpenElement(String text) { + if (text.startsWith("")) { + return true; + } + return false; + } + + public abstract void connect(WebsocketRemoteConnectionEndpoint endpoint) throws Throwable; + + public abstract void send(TopLevelStreamElement element); + + public abstract void disconnect(int code, String message); + + public abstract boolean isConnectionSecure(); + + public abstract SSLSession getSSLSession(); + + public abstract boolean isConnected(); +} diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/implementations/WebsocketImplProvider.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/implementations/WebsocketImplProvider.java new file mode 100644 index 000000000..b67ac236d --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/implementations/WebsocketImplProvider.java @@ -0,0 +1,35 @@ +/** + * + * Copyright 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. + * 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.implementations; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.util.Objects; +import org.jivesoftware.smack.websocket.XmppWebsocketTransportModule.XmppWebsocketTransport.DiscoveredWebsocketEndpoints; + +public final class WebsocketImplProvider { + + public static AbstractWebsocket getWebsocketImpl(Class websocketImpl, ModularXmppClientToServerConnectionInternal connectionInternal, DiscoveredWebsocketEndpoints discoveredWebsocketEndpoints) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + Objects.requireNonNull(connectionInternal, "ConnectionInternal cannot be null"); + + // Creates an instance of the constructor for the desired websocket implementation. + Constructor constructor = websocketImpl.getConstructor(ModularXmppClientToServerConnectionInternal.class, DiscoveredWebsocketEndpoints.class); + return (AbstractWebsocket) constructor.newInstance(connectionInternal, discoveredWebsocketEndpoints); + } +} diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/implementations/okhttp/LoggingInterceptor.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/implementations/okhttp/LoggingInterceptor.java new file mode 100644 index 000000000..c76684b98 --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/implementations/okhttp/LoggingInterceptor.java @@ -0,0 +1,90 @@ +/** + * + * Copyright 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. + * 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.implementations.okhttp; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Iterator; +import java.util.logging.Level; +import java.util.logging.Logger; + +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; + + public 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); + + 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); + } + + // 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) { + Headers headers = response.headers(); + Iterator iterator = headers.iterator(); + StringBuilder sb = new StringBuilder(); + sb.append("Received headers:"); + while (iterator.hasNext()) { + sb.append("\n\t" + iterator.next()); + } + debugger.incomingStreamSink(sb); + } + + public void interceptReceivedText(String text) { + try { + incomingTextSplitter.write(text.getBytes(Charset.defaultCharset())); + } 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) { + try { + outgoingTextSplitter.write(text.getBytes(Charset.defaultCharset())); + } 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/src/main/java/org/jivesoftware/smack/websocket/implementations/okhttp/OkHttpWebsocket.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/implementations/okhttp/OkHttpWebsocket.java new file mode 100644 index 000000000..0d898d849 --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/implementations/okhttp/OkHttpWebsocket.java @@ -0,0 +1,179 @@ +/** + * + * Copyright 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. + * 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.implementations.okhttp; + +import java.io.IOException; +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.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.XmppWebsocketTransportModule.XmppWebsocketTransport.DiscoveredWebsocketEndpoints; +import org.jivesoftware.smack.websocket.elements.WebsocketOpenElement; +import org.jivesoftware.smack.websocket.implementations.AbstractWebsocket; +import org.jivesoftware.smack.websocket.rce.WebsocketRemoteConnectionEndpoint; +import org.jivesoftware.smack.xml.XmlPullParserException; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; + +public final class OkHttpWebsocket extends AbstractWebsocket { + + private static final Logger LOGGER = Logger.getLogger(OkHttpWebsocket.class.getName()); + + private static OkHttpClient okHttpClient = null; + + private final ModularXmppClientToServerConnectionInternal connectionInternal; + private final LoggingInterceptor interceptor; + + private String openStreamHeader; + private WebSocket currentWebsocket; + private WebsocketConnectionPhase phase; + private WebsocketRemoteConnectionEndpoint connectedEndpoint; + + public OkHttpWebsocket(ModularXmppClientToServerConnectionInternal connectionInternal, + DiscoveredWebsocketEndpoints discoveredWebsocketEndpoints) { + this.connectionInternal = 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(); + Request request = new Request.Builder() + .url(currentUri) + .header("Sec-WebSocket-Protocol", "xmpp") + .build(); + + WebSocketListener listener = new WebSocketListener() { + + @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())); + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + if (interceptor != null) { + interceptor.interceptReceivedText(text); + } + if (isCloseElement(text)) { + connectionInternal.onStreamClosed(); + return; + } + + 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 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); + } + } + }; + + // Creates an instance of websocket through okHttpClient. + currentWebsocket = okHttpClient.newWebSocket(request, listener); + + // Open a new stream and wait until features are received. + connectionInternal.waitForFeaturesReceived("Waiting to receive features"); + + connectedEndpoint = endpoint; + } + + @Override + public void send(TopLevelStreamElement element) { + String textToBeSent = element.toXML().toString(); + if (interceptor != null) { + interceptor.interceptSentText(textToBeSent); + } + currentWebsocket.send(textToBeSent); + } + + @Override + public void disconnect(int code, String message) { + currentWebsocket.close(code, message); + LOGGER.log(Level.INFO, "Websocket has been closed with message: " + message); + } + + @Override + public boolean isConnectionSecure() { + return connectedEndpoint.isSecureEndpoint(); + } + + @Override + public boolean isConnected() { + return connectedEndpoint == null ? false : true; + } + + @Override + public SSLSession getSSLSession() { + return null; + } +} diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/implementations/okhttp/package-info.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/implementations/okhttp/package-info.java new file mode 100644 index 000000000..c077b1214 --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/implementations/okhttp/package-info.java @@ -0,0 +1,17 @@ +/** + * + * Copyright 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. + * 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.implementations.okhttp; diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/implementations/package-info.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/implementations/package-info.java new file mode 100644 index 000000000..4260faaaf --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/implementations/package-info.java @@ -0,0 +1,20 @@ +/** + * + * Copyright 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. + * 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. + */ +/** + * This package contains websocket implementations to be plugged inside websocket transport. + */ +package org.jivesoftware.smack.websocket.implementations; diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/package-info.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/package-info.java new file mode 100644 index 000000000..1f3049bde --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/package-info.java @@ -0,0 +1,20 @@ +/** + * + * Copyright 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. + * 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. + */ +/** + * Websocket related classes for Smack. + */ +package org.jivesoftware.smack.websocket; 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 new file mode 100644 index 000000000..d8bb0dca3 --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/WebsocketRemoteConnectionEndpoint.java @@ -0,0 +1,85 @@ +/** + * + * Copyright 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. + * 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.InetAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.Collections; +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 { + + private static final Logger LOGGER = Logger.getAnonymousLogger(); + + private final URI uri; + + public WebsocketRemoteConnectionEndpoint(String uri) throws URISyntaxException { + this(new URI(uri)); + } + + public 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"); + } + } + + public URI getWebsocketEndpoint() { + return uri; + } + + public boolean isSecureEndpoint() { + if (uri.getScheme().equals("wss")) { + return true; + } + return false; + } + + @Override + public CharSequence getHost() { + return uri.getHost(); + } + + @Override + public UInt16 getPort() { + return UInt16.from(uri.getPort()); + } + + @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; + } + + @Override + public String getDescription() { + return null; + } +} 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 new file mode 100644 index 000000000..16ffc7f83 --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/WebsocketRemoteConnectionEndpointLookup.java @@ -0,0 +1,115 @@ +/** + * + * Copyright 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. + * 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.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Iterator; +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; +import org.jivesoftware.smack.xml.XmlPullParserException; + +import org.jxmpp.jid.DomainBareJid; + +public final class WebsocketRemoteConnectionEndpointLookup { + + public static Result lookup(DomainBareJid domainBareJid, SecurityMode securityMode) { + List lookupFailures = new ArrayList<>(1); + List discoveredRemoteConnectionEndpoints = new ArrayList<>(); + + List rcUriList = null; + try { + // Look for remote connection endpoints by making use of http lookup method described inside XEP-0156. + rcUriList = HttpLookupMethod.lookup(domainBareJid, + LinkRelation.WEBSOCKET); + } catch (IOException | XmlPullParserException | URISyntaxException e) { + lookupFailures.add(new RemoteConnectionEndpointLookupFailure.HttpLookupFailure( + domainBareJid, e)); + return new Result(discoveredRemoteConnectionEndpoints, lookupFailures); + } + + if (rcUriList.isEmpty()) { + throw new IllegalStateException("No endpoints were found inside host-meta"); + } + + // 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); + } + + public static final class Result { + public final List discoveredRemoteConnectionEndpoints; + public final List lookupFailures; + + public Result(List discoveredRemoteConnectionEndpoints, + List lookupFailures) { + this.discoveredRemoteConnectionEndpoints = discoveredRemoteConnectionEndpoints; + this.lookupFailures = lookupFailures; + } + + public List getDiscoveredRemoteConnectionEndpoints() { + return discoveredRemoteConnectionEndpoints; + } + + public List getLookupFailures() { + return lookupFailures; + } + } +} diff --git a/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/package-info.java b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/package-info.java new file mode 100644 index 000000000..f7b0eb2e2 --- /dev/null +++ b/smack-websocket/src/main/java/org/jivesoftware/smack/websocket/rce/package-info.java @@ -0,0 +1,20 @@ +/** + * + * Copyright 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. + * 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. + */ +/** + * This package contains websocket endpoint classes needed by the websocket transport. + */ +package org.jivesoftware.smack.websocket.rce; diff --git a/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/WebsocketConnectionAttemptStateTest.java b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/WebsocketConnectionAttemptStateTest.java new file mode 100644 index 000000000..59cd0adc8 --- /dev/null +++ b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/WebsocketConnectionAttemptStateTest.java @@ -0,0 +1,28 @@ +/** + * + * Copyright 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. + * 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; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class WebsocketConnectionAttemptStateTest { + @Test + public void constructorTest() { + assertThrows(AssertionError.class, () -> new WebsocketConnectionAttemptState(null, null, null)); + } +} diff --git a/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/WebsocketInitializerTest.java b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/WebsocketInitializerTest.java new file mode 100644 index 000000000..ce41c594e --- /dev/null +++ b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/WebsocketInitializerTest.java @@ -0,0 +1,32 @@ +/** + * + * Copyright 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. + * 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; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +public class WebsocketInitializerTest { + @Test + public void testExtensionInitializer() { + WebsocketInitializer initializer = new WebsocketInitializer(); + List exceptions = initializer.initialize(); + assertTrue(exceptions.size() == 0); + } +} 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 new file mode 100644 index 000000000..ec745aa70 --- /dev/null +++ b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/XmppWebsocketTransportModuleTest.java @@ -0,0 +1,124 @@ +/** + * + * Copyright 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. + * 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; + +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; + +public class XmppWebsocketTransportModuleTest { + @Test + public void createWebsocketModuleConnectionInstanceTest() throws URISyntaxException, XmppStringprepException { + ModularXmppClientToServerConnectionConfiguration.Builder builder = ModularXmppClientToServerConnectionConfiguration + .builder(); + + builder.removeAllModules(); + builder.addModule(XmppWebsocketTransportModuleDescriptor.class); + builder.setXmppAddressAndPassword("user5@localhost.org", "user5"); + builder.setHost("localhost.org"); + + XmppWebsocketTransportModuleDescriptor.Builder websocketBuilder = XmppWebsocketTransportModuleDescriptor.getBuilder(builder); + websocketBuilder.explicitlySetWebsocketEndpointAndDiscovery(new URI("wss://localhost.org:7443/ws/"), false); + + ModularXmppClientToServerConnectionConfiguration config = builder.build(); + ModularXmppClientToServerConnection connection = new ModularXmppClientToServerConnection(config); + assertNotNull(connection); + } + + @Test + public void createDescriptorTest() throws URISyntaxException, XmppStringprepException { + XmppWebsocketTransportModuleDescriptor websocketTransportModuleDescriptor = getWebsocketDescriptor(); + 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(); + ModularXmppClientToServerConnectionInternal connectionInternal = mock(ModularXmppClientToServerConnectionInternal.class); + + XmppWebsocketTransportModule transportModule + = new XmppWebsocketTransportModule(websocketTransportModuleDescriptor, connectionInternal); + + XmppWebsocketTransportModule.XmppWebsocketTransport transport = transportModule.getTransport(); + assertNotNull(transport.lookupConnectionEndpoints()); + + } + + private static XmppWebsocketTransportModuleDescriptor getWebsocketDescriptor() throws URISyntaxException { + ModularXmppClientToServerConnectionConfiguration.Builder builder = ModularXmppClientToServerConnectionConfiguration + .builder(); + + XmppWebsocketTransportModuleDescriptor.Builder websocketBuilder = XmppWebsocketTransportModuleDescriptor.getBuilder(builder); + websocketBuilder.explicitlySetWebsocketEndpointAndDiscovery(new URI("wss://localhost.org:7443/ws/"), false); + return (XmppWebsocketTransportModuleDescriptor) websocketBuilder.build(); + } +} diff --git a/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/elements/WebsocketElementTest.java b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/elements/WebsocketElementTest.java new file mode 100644 index 000000000..7c00bbeac --- /dev/null +++ b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/elements/WebsocketElementTest.java @@ -0,0 +1,43 @@ +/** + * + * Copyright 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. + * 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.elements; + +import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlNotSimilar; +import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; + +import org.junit.jupiter.api.Test; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.stringprep.XmppStringprepException; + +public class WebsocketElementTest { + private static final String OPEN_ELEMENT = ""; + private static final String CLOSE_ELEMENT = ""; + + @Test + public void websocketOpenElementTest() throws XmppStringprepException { + String openElementXml = new WebsocketOpenElement(JidCreate.domainBareFrom("foodomain.foo")).toXML().toString(); + assertXmlSimilar(OPEN_ELEMENT, openElementXml); + assertXmlNotSimilar(CLOSE_ELEMENT, new WebsocketOpenElement(JidCreate.domainBareFrom("foodomain.foo")).toXML()); + } + + @Test + public void websocketCloseElementTest() throws XmppStringprepException { + String closeElementXml = new WebsocketCloseElement().toXML().toString(); + assertXmlSimilar(CLOSE_ELEMENT, closeElementXml); + assertXmlNotSimilar(OPEN_ELEMENT, closeElementXml); + } +} diff --git a/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/implementations/AbstractWebsocketTest.java b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/implementations/AbstractWebsocketTest.java new file mode 100644 index 000000000..3c71dadfa --- /dev/null +++ b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/implementations/AbstractWebsocketTest.java @@ -0,0 +1,47 @@ +/** + * + * Copyright 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. + * 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.implementations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public final class AbstractWebsocketTest { + private static final String OPEN_ELEMENT = ""; + private static final String OPEN_STREAM = ""; + private static final String CLOSE_ELEMENT = ""; + + @Test + public void getStreamFromOpenElementTest() { + String generatedOpenStream = AbstractWebsocket.getStreamFromOpenElement(OPEN_ELEMENT); + assertEquals(generatedOpenStream, OPEN_STREAM); + } + + @Test + public void isOpenElementTest() { + assertTrue(AbstractWebsocket.isOpenElement(OPEN_ELEMENT)); + assertFalse(AbstractWebsocket.isOpenElement(OPEN_STREAM)); + } + + @Test + public void isCloseElementTest() { + assertTrue(AbstractWebsocket.isCloseElement(CLOSE_ELEMENT)); + assertFalse(AbstractWebsocket.isCloseElement(OPEN_STREAM)); + } +} diff --git a/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/implementations/ProviderTest.java b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/implementations/ProviderTest.java new file mode 100644 index 000000000..842a3fe0e --- /dev/null +++ b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/implementations/ProviderTest.java @@ -0,0 +1,61 @@ +/** + * + * Copyright 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. + * 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.implementations; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.reflect.InvocationTargetException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.websocket.XmppWebsocketTransportModule.XmppWebsocketTransport.DiscoveredWebsocketEndpoints; + +import org.jivesoftware.smack.websocket.implementations.okhttp.OkHttpWebsocket; +import org.jivesoftware.smack.websocket.rce.WebsocketRemoteConnectionEndpoint; +import org.jivesoftware.smack.websocket.rce.WebsocketRemoteConnectionEndpointLookup.Result; + +import org.junit.jupiter.api.Test; + +public class ProviderTest { + @Test + public void providerTest() { + assertThrows(IllegalArgumentException.class, () -> WebsocketImplProvider.getWebsocketImpl(OkHttpWebsocket.class, null, null)); + } + + @Test + public void getImplTest() throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, URISyntaxException { + WebsocketRemoteConnectionEndpoint endpoint = new WebsocketRemoteConnectionEndpoint("wss://localhost.org:7443/ws/"); + + List discoveredRemoteConnectionEndpoints = new ArrayList<>(); + discoveredRemoteConnectionEndpoints.add(endpoint); + + Result result = new Result(discoveredRemoteConnectionEndpoints, null); + + DiscoveredWebsocketEndpoints discoveredWebsocketEndpoints = mock(DiscoveredWebsocketEndpoints.class); + when(discoveredWebsocketEndpoints.getResult()).thenReturn(result); + + ModularXmppClientToServerConnectionInternal connectionInternal = mock(ModularXmppClientToServerConnectionInternal.class); + + assertNotNull(WebsocketImplProvider.getWebsocketImpl(OkHttpWebsocket.class, connectionInternal, discoveredWebsocketEndpoints)); + } +} 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 new file mode 100644 index 000000000..534f99de2 --- /dev/null +++ b/smack-websocket/src/test/java/org/jivesoftware/smack/websocket/rce/WebsocketRemoteConnectionEndpointTest.java @@ -0,0 +1,45 @@ +/** + * + * Copyright 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. + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.net.URISyntaxException; + +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); + assertEquals("fooDomain.org", endpoint.getHost()); + assertEquals(UInt16.from(7070), endpoint.getPort()); + assertEquals(endpointString, endpoint.getWebsocketEndpoint().toString()); + } + + @Test + public void faultyEndpointTest() { + String faultyProtocolString = "wst://fooDomain.org:7070/ws/"; + assertThrows(IllegalArgumentException.class, () -> { + new WebsocketRemoteConnectionEndpoint(faultyProtocolString); + }); + } +} diff --git a/smack-websocket/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/smack-websocket/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..ca6ee9cea --- /dev/null +++ b/smack-websocket/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file