diff --git a/build.gradle b/build.gradle index 9e4d6651f..49aa67092 100644 --- a/build.gradle +++ b/build.gradle @@ -122,8 +122,8 @@ allprojects { // See also: // - https://issues.apache.org/jira/browse/MNG-6232 // - https://issues.igniterealtime.org/browse/SMACK-858 - jxmppVersion = '1.0.0' - miniDnsVersion = '1.0.0' + jxmppVersion = '[1.0.0, 1.0.999]' + miniDnsVersion = '[1.0.0, 1.0.999]' smackMinAndroidSdk = 19 junitVersion = '5.6.2' commonsIoVersion = '2.6' @@ -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' @@ -305,6 +308,13 @@ configure (junit4Projects) { } } +// We need to evaluate the child projects first because +// - javadocAll needs the smack-core child to have already resolved +// the jXMPP/MiniDNS dependencies, so that we can the resovled +// version to link to those project's javadoc. +// - We use the child's project description as description for the +// Maven POM. +evaluationDependsOnChildren() task javadocAll(type: Javadoc) { source javadocAllProjects.collect {project -> project.sourceSets.main.allJava.findAll { @@ -319,13 +329,15 @@ task javadocAll(type: Javadoc) { classpath = files(subprojects.collect {project -> project.sourceSets.main.compileClasspath}) classpath += files(androidBootClasspath) + def staticJxmppVersion = getResolvedVersion('org.jxmpp:jxmpp-core') + def staticMiniDnsVersion = getResolvedVersion('org.minidns:minidns-core') options { linkSource = true use = true links = [ "https://docs.oracle.com/javase/${javaMajor}/docs/api/", - "https://jxmpp.org/releases/$jxmppVersion/javadoc/", - "https://minidns.org/releases/$miniDnsVersion/javadoc/", + "https://jxmpp.org/releases/${staticJxmppVersion}/javadoc/", + "https://minidns.org/releases/${staticMiniDnsVersion}/javadoc/", ] as String[] overview = "$projectDir/resources/javadoc-overview.html" } @@ -408,7 +420,6 @@ description = """\ Smack ${version} ${oneLineDesc}.""" -evaluationDependsOnChildren() subprojects { apply plugin: 'maven-publish' apply plugin: 'signing' @@ -678,19 +689,19 @@ task sinttestAll { } def getGitCommit() { - def dotGit = new File("$projectDir/.git") + def projectDirFile = new File("$projectDir") + def dotGit = new File(projectDirFile, ".git") if (!dotGit.isDirectory()) return 'non-git build' - def projectDir = dotGit.getParentFile() def cmd = 'git describe --always --tags --dirty=+' - def proc = cmd.execute(null, projectDir) + def proc = cmd.execute(null, projectDirFile) proc.waitForOrKill(10 * 1000) def gitCommit = proc.text.trim() assert !gitCommit.isEmpty() def srCmd = 'git symbolic-ref --short HEAD' - def srProc = srCmd.execute(null, projectDir) + def srProc = srCmd.execute(null, projectDirFile) srProc.waitForOrKill(10 * 1000) if (srProc.exitValue() == 0) { // Only add the information if the git command was @@ -740,3 +751,24 @@ def readVersionFile() { } versionFile.text.trim() } + +def getResolvedVersion(queriedProject = 'smack-core', component) { + def configuration = project(queriedProject) + .configurations + .compileClasspath + + def artifact = configuration + .resolvedConfiguration + .resolvedArtifacts + .findAll { + // 'it' is of type ResolvedArtifact, 'id' of + // Component*Artifcat*Identifier, and we check the + // ComponentIdentifier. + it.id.getComponentIdentifier() instanceof org.gradle.api.artifacts.component.ModuleComponentIdentifier + } + .find { + it.id.getComponentIdentifier().toString().startsWith(component + ':') + } + + artifact.getModuleVersion().getId().getVersion() +} diff --git a/settings.gradle b/settings.gradle index 37bbbb004..460e5e403 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,6 +16,7 @@ include 'smack-core', 'smack-resolver-javax', 'smack-sasl-javax', 'smack-sasl-provided', + 'smack-streammanagement', 'smack-legacy', 'smack-jingle-old', 'smack-bosh', @@ -29,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/AbstractXMPPConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java index bd3d47554..a421d6cd1 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -70,6 +70,7 @@ import org.jivesoftware.smack.filter.StanzaFilter; import org.jivesoftware.smack.filter.StanzaIdFilter; import org.jivesoftware.smack.internal.SmackTlsContext; import org.jivesoftware.smack.iqrequest.IQRequestHandler; +import org.jivesoftware.smack.packet.AbstractStreamOpen; import org.jivesoftware.smack.packet.Bind; import org.jivesoftware.smack.packet.ErrorIQ; import org.jivesoftware.smack.packet.ExtensionElement; @@ -522,6 +523,9 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { closingStreamReceived = false; streamId = null; + // The connection should not be connected nor marked as such prior calling connectInternal(). + assert !connected; + try { // Perform the actual connection to the XMPP service connectInternal(); @@ -537,8 +541,9 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { throw e; } - // Make note of the fact that we're now connected. - connected = true; + // If connectInternal() did not throw, then this connection must now be marked as connected. + assert connected; + callConnectionConnectedListener(); return this; @@ -2224,7 +2229,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { } } - protected void sendStreamOpen() throws NotConnectedException, InterruptedException { + protected final void sendStreamOpen() throws NotConnectedException, InterruptedException { // If possible, provide the receiving entity of the stream open tag, i.e. the server, as much information as // possible. The 'to' attribute is *always* available. The 'from' attribute if set by the user and no external // mechanism is used to determine the local entity (user). And the 'id' attribute is available after the first @@ -2236,10 +2241,18 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { from = XmppStringUtils.completeJidFrom(localpart, to); } String id = getStreamId(); + String lang = config.getXmlLang(); - StreamOpen streamOpen = new StreamOpen(to, from, id, config.getXmlLang(), StreamOpen.StreamContentNamespace.client); + AbstractStreamOpen streamOpen = getStreamOpen(to, from, id, lang); sendNonza(streamOpen); + updateOutgoingStreamXmlEnvironmentOnStreamOpen(streamOpen); + } + protected AbstractStreamOpen getStreamOpen(CharSequence to, CharSequence from, String id, String lang) { + return new StreamOpen(to, from, id, lang); + } + + protected void updateOutgoingStreamXmlEnvironmentOnStreamOpen(AbstractStreamOpen streamOpen) { XmlEnvironment.Builder xmlEnvironmentBuilder = XmlEnvironment.builder(); xmlEnvironmentBuilder.with(streamOpen); outgoingStreamXmlEnvironment = xmlEnvironmentBuilder.build(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java index 3d2c0b851..ea34d243b 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java @@ -364,7 +364,7 @@ public abstract class SmackException extends Exception { if (lookupFailures.isEmpty()) { sb.append("No endpoint lookup finished within the timeout"); } else { - sb.append("Not endpoints could be discovered due the following lookup failures: "); + sb.append("No endpoints could be discovered due the following lookup failures: "); StringUtils.appendTo(lookupFailures, sb); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/altconnections/HttpLookupMethod.java b/smack-core/src/main/java/org/jivesoftware/smack/altconnections/HttpLookupMethod.java index 75a4cf263..e30530be7 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/altconnections/HttpLookupMethod.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/altconnections/HttpLookupMethod.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.List; import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smack.util.ParserUtils; import org.jivesoftware.smack.xml.XmlPullParser; import org.jivesoftware.smack.xml.XmlPullParserException; @@ -132,6 +133,7 @@ public final class HttpLookupMethod { * @throws URISyntaxException exception to indicate that a string could not be parsed as a URI reference */ public static List parseXrdLinkReferencesFor(XmlPullParser parser, String relation) throws IOException, XmlPullParserException, URISyntaxException { + ParserUtils.forwardToStartElement(parser); List uriList = new ArrayList<>(); int initialDepth = parser.getDepth(); 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 f62caf8ee..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,12 +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; @@ -81,7 +82,9 @@ import org.jivesoftware.smack.util.Supplier; import org.jivesoftware.smack.xml.XmlPullParser; import org.jivesoftware.smack.xml.XmlPullParserException; +import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.parts.Resourcepart; +import org.jxmpp.util.XmppStringUtils; public final class ModularXmppClientToServerConnection extends AbstractXMPPConnection { @@ -135,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); @@ -176,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 { @@ -199,7 +212,7 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne } @Override - public void waitForCondition(Supplier condition, String waitFor) + public void waitForConditionOrThrowConnectionException(Supplier condition, String waitFor) throws InterruptedException, SmackException, XMPPException { ModularXmppClientToServerConnection.this.waitForConditionOrThrowConnectionException(condition, waitFor); } @@ -217,6 +230,7 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne @Override public void setTransport(XmppClientToServerTransport xmppTransport) { ModularXmppClientToServerConnection.this.activeTransport = xmppTransport; + ModularXmppClientToServerConnection.this.connected = true; } }; @@ -556,13 +570,35 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne waitForConditionOrThrowConnectionException(() -> featuresReceived, waitFor); } + @Override + protected AbstractStreamOpen getStreamOpen(CharSequence to, CharSequence from, String id, String lang) { + StreamOpenAndCloseFactory streamOpenAndCloseFactory = activeTransport.getStreamOpenAndCloseFactory(); + return streamOpenAndCloseFactory.createStreamOpen(to, from, id, lang); + } + protected void newStreamOpenWaitForFeaturesSequence(String waitFor) throws InterruptedException, SmackException, XMPPException { prepareToWaitForFeaturesReceived(); - sendStreamOpen(); + + // Create StreamOpen from StreamOpenAndCloseFactory via underlying transport. + StreamOpenAndCloseFactory streamOpenAndCloseFactory = activeTransport.getStreamOpenAndCloseFactory(); + CharSequence from = null; + CharSequence localpart = connectionInternal.connection.getConfiguration().getUsername(); + DomainBareJid xmppServiceDomain = getXMPPServiceDomain(); + if (localpart != null) { + from = XmppStringUtils.completeJidFrom(localpart, xmppServiceDomain); + } + AbstractStreamOpen streamOpen = streamOpenAndCloseFactory.createStreamOpen(xmppServiceDomain, from, getStreamId(), getConfiguration().getXmlLang()); + sendStreamOpen(streamOpen); + waitForFeaturesReceived(waitFor); } + private void sendStreamOpen(AbstractStreamOpen streamOpen) throws NotConnectedException, InterruptedException { + sendNonza(streamOpen); + updateOutgoingStreamXmlEnvironmentOnStreamOpen(streamOpen); + } + public static class DisconnectedStateDescriptor extends StateDescriptor { protected DisconnectedStateDescriptor() { super(DisconnectedState.class, StateDescriptor.Property.finalState); @@ -904,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/ModularXmppClientToServerConnectionConfiguration.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionConfiguration.java index a3bf16911..f67ab5a11 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionConfiguration.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionConfiguration.java @@ -96,7 +96,7 @@ public final class ModularXmppClientToServerConnectionConfiguration extends Conn return new ModularXmppClientToServerConnectionConfiguration(this); } - void addModule(ModularXmppClientToServerConnectionModuleDescriptor connectionModule) { + public void addModule(ModularXmppClientToServerConnectionModuleDescriptor connectionModule) { Class moduleDescriptorClass = connectionModule.getClass(); if (modulesDescriptors.containsKey(moduleDescriptorClass)) { throw new IllegalArgumentException("A connection module for " + moduleDescriptorClass + " is already configured"); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/StreamOpenAndCloseFactory.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/StreamOpenAndCloseFactory.java new file mode 100644 index 000000000..4a15467d2 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/StreamOpenAndCloseFactory.java @@ -0,0 +1,26 @@ +/** + * + * 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.c2s; + +import org.jivesoftware.smack.packet.AbstractStreamClose; +import org.jivesoftware.smack.packet.AbstractStreamOpen; + +public interface StreamOpenAndCloseFactory { + AbstractStreamOpen createStreamOpen(CharSequence to, CharSequence from, String id, String lang); + + AbstractStreamClose createStreamClose(); +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/XmppClientToServerTransport.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/XmppClientToServerTransport.java index d68440fc3..4bedb51b6 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/c2s/XmppClientToServerTransport.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/XmppClientToServerTransport.java @@ -58,6 +58,8 @@ public abstract class XmppClientToServerTransport { return getSslSession() != null; } + public abstract StreamOpenAndCloseFactory getStreamOpenAndCloseFactory(); + public abstract Stats getStats(); public abstract static class Stats { 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 81a485771..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; @@ -110,11 +114,17 @@ public abstract class ModularXmppClientToServerConnectionInternal { public abstract void asyncGo(Runnable runnable); - public abstract void waitForCondition(Supplier condition, String waitFor) throws InterruptedException, SmackException, XMPPException; + public abstract void waitForConditionOrThrowConnectionException(Supplier condition, String waitFor) throws InterruptedException, SmackException, XMPPException; public abstract void notifyWaitingThreads(); public abstract void setCompressionEnabled(boolean compressionEnabled); + /** + * Set the active transport (TCP, BOSH, WebSocket, …) to be used for the XMPP connection. Also marks the connection + * as connected. + * + * @param xmppTransport the active transport. + */ public abstract void setTransport(XmppClientToServerTransport xmppTransport); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java index aec14b18e..8f399480e 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java @@ -22,12 +22,16 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; public abstract class StateDescriptor { + private static final Logger LOGGER = Logger.getLogger(StateDescriptor.class.getName()); + public enum Property { multiVisitState, finalState, @@ -133,10 +137,35 @@ public abstract class StateDescriptor { addAndCheckNonExistent(precedenceOver, subordinate); } - protected void declareInferiortyTo(Class superior) { + protected void declarePrecedenceOver(String subordinate) { + addAndCheckNonExistent(precedenceOver, subordinate); + } + + protected void declareInferiorityTo(Class superior) { addAndCheckNonExistent(inferiorTo, superior); } + protected void declareInferiorityTo(String superior) { + addAndCheckNonExistent(inferiorTo, superior); + } + + private static void addAndCheckNonExistent(Set> set, String clazzName) { + Class clazz; + try { + clazz = Class.forName(clazzName); + } catch (ClassNotFoundException e) { + // The state descriptor class is not in classpath, which probably means that the smack module is not loaded + // into the classpath. Hence we can silently ignore that. + LOGGER.log(Level.FINEST, "Ignoring unknown state descriptor '" + clazzName + "'", e); + return; + } + if (!StateDescriptor.class.isAssignableFrom(clazz)) { + throw new IllegalArgumentException(clazz + " is no state descriptor class"); + } + Class stateDescriptorClass = clazz.asSubclass(StateDescriptor.class); + addAndCheckNonExistent(set, stateDescriptorClass); + } + private static void addAndCheckNonExistent(Set set, E e) { boolean newElement = set.add(e); if (!newElement) { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractStreamClose.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractStreamClose.java new file mode 100644 index 000000000..c938c7e9e --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractStreamClose.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. + */ +package org.jivesoftware.smack.packet; + +public abstract class AbstractStreamClose implements Nonza { +} 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 new file mode 100644 index 000000000..698e7888d --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/AbstractStreamOpen.java @@ -0,0 +1,89 @@ +/** + * + * Copyright 2020 Florian Schmaus, 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.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 + * implement {@link Nonza} here. This is because, {@link XMPPConnection} doesn't + * yet support sending {@link TopLevelStreamElement} directly and the same can only + * be achieved through {@link XMPPConnection#sendNonza(Nonza)}. + */ +public abstract class AbstractStreamOpen implements Nonza { + public static final String CLIENT_NAMESPACE = "jabber:client"; + public static final String SERVER_NAMESPACE = "jabber:server"; + + /** + * RFC 6120 § 4.7.5. + */ + public static final String VERSION = "1.0"; + + /** + * RFC 6120 § 4.7.1. + */ + protected final String from; + + /** + * RFC 6120 § 4.7.2. + */ + protected final String to; + + /** + * RFC 6120 § 4.7.3. + */ + protected final String id; + + /** + * RFC 6120 § 4.7.4. + */ + protected final String lang; + + /** + * RFC 6120 § 4.8.2. + */ + protected final String contentNamespace; + + public AbstractStreamOpen(CharSequence to, CharSequence from, String id, String lang) { + this(to, from, id, lang, StreamContentNamespace.client); + } + + public AbstractStreamOpen(CharSequence to, CharSequence from, String id, String lang, StreamContentNamespace ns) { + this.to = StringUtils.maybeToString(to); + this.from = StringUtils.maybeToString(from); + this.id = id; + this.lang = lang; + switch (ns) { + case client: + this.contentNamespace = CLIENT_NAMESPACE; + break; + case server: + this.contentNamespace = SERVER_NAMESPACE; + break; + default: + 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/packet/StanzaView.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaView.java index 7745ecc2a..003ddbeef 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaView.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/StanzaView.java @@ -90,11 +90,12 @@ public interface StanzaView extends XmlLangElement { default E getExtension(Class extensionElementClass) { QName qname = XmppElementUtil.getQNameFor(extensionElementClass); ExtensionElement extensionElement = getExtension(qname); - if (!extensionElementClass.isInstance(extensionElement)) { + + if (extensionElement == null) { return null; } - return extensionElementClass.cast(extensionElement); + return XmppElementUtil.castOrThrow(extensionElement, extensionElementClass); } /** diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamClose.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamClose.java index 377e6c616..fd284a2e5 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamClose.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamClose.java @@ -16,7 +16,7 @@ */ package org.jivesoftware.smack.packet; -public final class StreamClose implements Nonza { +public final class StreamClose extends AbstractStreamClose { public static final StreamClose INSTANCE = new StreamClose(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamOpen.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamOpen.java index 6ef368b63..959adc1fa 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamOpen.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/StreamOpen.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014-2019 Florian Schmaus + * Copyright © 2014-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,49 +17,14 @@ package org.jivesoftware.smack.packet; -import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.XmlStringBuilder; /** * The stream open tag. */ -public class StreamOpen implements Nonza { - +public final class StreamOpen extends AbstractStreamOpen { public static final String ELEMENT = "stream:stream"; - public static final String CLIENT_NAMESPACE = "jabber:client"; - public static final String SERVER_NAMESPACE = "jabber:server"; - - /** - * RFC 6120 § 4.7.5. - */ - public static final String VERSION = "1.0"; - - /** - * RFC 6120 § 4.7.1. - */ - private final String from; - - /** - * RFC 6120 § 4.7.2. - */ - private final String to; - - /** - * RFC 6120 § 4.7.3. - */ - private final String id; - - /** - * RFC 6120 § 4.7.4. - */ - private final String lang; - - /** - * RFC 6120 § 4.8.2. - */ - private final String contentNamespace; - public StreamOpen(CharSequence to) { this(to, null, null, null, StreamContentNamespace.client); } @@ -68,21 +33,12 @@ public class StreamOpen implements Nonza { this(to, from, id, "en", StreamContentNamespace.client); } + public StreamOpen(CharSequence to, CharSequence from, String id, String lang) { + super(to, from, id, lang); + } + public StreamOpen(CharSequence to, CharSequence from, String id, String lang, StreamContentNamespace ns) { - this.to = StringUtils.maybeToString(to); - this.from = StringUtils.maybeToString(from); - this.id = id; - this.lang = lang; - switch (ns) { - case client: - this.contentNamespace = CLIENT_NAMESPACE; - break; - case server: - this.contentNamespace = SERVER_NAMESPACE; - break; - default: - throw new IllegalStateException(); - } + super(to, from, id, lang, ns); } @Override diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/XmlEnvironment.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/XmlEnvironment.java index defc09868..2fdc21333 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/XmlEnvironment.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/XmlEnvironment.java @@ -71,7 +71,7 @@ public class XmlEnvironment { } public String getEffectiveNamespaceOrUse(String namespace) { - String effectiveNamespace = getEffectiveLanguage(); + String effectiveNamespace = getEffectiveNamespace(); if (StringUtils.isNullOrEmpty(effectiveNamespace)) { return namespace; } @@ -162,7 +162,7 @@ public class XmlEnvironment { return this; } - public Builder with(StreamOpen streamOpen) { + public Builder with(AbstractStreamOpen streamOpen) { withNamespace(streamOpen.getNamespace()); withLanguage(streamOpen.getLanguage()); return this; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderManager.java b/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderManager.java index 7527b330f..f9e2bb560 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderManager.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/provider/ProviderManager.java @@ -243,7 +243,11 @@ public final class ProviderManager { */ public static ExtensionElementProvider getExtensionProvider(String elementName, String namespace) { QName key = getQName(elementName, namespace); - return extensionProviders.get(key); + return getExtensionProvider(key); + } + + public static ExtensionElementProvider getExtensionProvider(QName qname) { + return extensionProviders.get(qname); } /** diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java index 70861812b..67249b559 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java @@ -82,14 +82,7 @@ public class PacketParserUtils { public static XmlPullParser getParserFor(Reader reader) throws XmlPullParserException, IOException { XmlPullParser parser = SmackXmlParser.newXmlParser(reader); - // Wind the parser forward to the first start tag - XmlPullParser.Event event = parser.getEventType(); - while (event != XmlPullParser.Event.START_ELEMENT) { - if (event == XmlPullParser.Event.END_DOCUMENT) { - throw new IllegalArgumentException("Document contains no start tag"); - } - event = parser.next(); - } + ParserUtils.forwardToStartElement(parser); return parser; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/ParserUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/ParserUtils.java index 3486b372d..4b038cf8f 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/ParserUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/ParserUtils.java @@ -63,6 +63,17 @@ public class ParserUtils { assert parser.getEventType() == XmlPullParser.Event.END_ELEMENT; } + public static void forwardToStartElement(XmlPullParser parser) throws XmlPullParserException, IOException { + // Wind the parser forward to the first start tag + XmlPullParser.Event event = parser.getEventType(); + while (event != XmlPullParser.Event.START_ELEMENT) { + if (event == XmlPullParser.Event.END_DOCUMENT) { + throw new IllegalArgumentException("Document contains no start tag"); + } + event = parser.next(); + } + } + public static void forwardToEndTagOfDepth(XmlPullParser parser, int depth) throws XmlPullParserException, IOException { XmlPullParser.Event event = parser.getEventType(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java b/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java index c483db913..50e27e072 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/XmlStringBuilder.java @@ -1,6 +1,6 @@ /** * - * Copyright 2014-2019 Florian Schmaus + * Copyright 2014-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import java.util.Date; import java.util.List; import org.jivesoftware.smack.packet.Element; -import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.FullyQualifiedElement; import org.jivesoftware.smack.packet.NamedElement; import org.jivesoftware.smack.packet.XmlEnvironment; @@ -43,7 +42,7 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element { effectiveXmlEnvironment = null; } - public XmlStringBuilder(ExtensionElement pe) { + public XmlStringBuilder(FullyQualifiedElement pe) { this(pe, null); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/XmppElementUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/XmppElementUtil.java index b78f62ab9..1e4eabbc3 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/XmppElementUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/XmppElementUtil.java @@ -23,17 +23,31 @@ import java.util.logging.Logger; import javax.xml.namespace.QName; +import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.FullyQualifiedElement; +import org.jivesoftware.smack.packet.StandardExtensionElement; +import org.jivesoftware.smack.provider.ProviderManager; + +import org.jxmpp.util.cache.LruCache; public class XmppElementUtil { + private static final LruCache, QName> CLASS_TO_QNAME_CACHE = new LruCache<>(512); + public static final Logger LOGGER = Logger.getLogger(XmppElementUtil.class.getName()); public static QName getQNameFor(Class fullyQualifiedElement) { + QName qname = CLASS_TO_QNAME_CACHE.get(fullyQualifiedElement); + if (qname != null) { + return qname; + } + try { Object qnameObject = fullyQualifiedElement.getField("QNAME").get(null); if (QName.class.isAssignableFrom(qnameObject.getClass())) { - return (QName) qnameObject; + qname = (QName) qnameObject; + CLASS_TO_QNAME_CACHE.put(fullyQualifiedElement, qname); + return qname; } LOGGER.warning("The QNAME field of " + fullyQualifiedElement + " is not of type QNAME."); } catch (NoSuchFieldException e) { @@ -52,24 +66,52 @@ public class XmppElementUtil { throw new IllegalArgumentException("The " + fullyQualifiedElement + " has no ELEMENT, NAMESPACE or QNAME member. Consider adding QNAME", e); } - return new QName(namespace, element); + qname = new QName(namespace, element); + CLASS_TO_QNAME_CACHE.put(fullyQualifiedElement, qname); + return qname; } - public static List getElementsFrom( - MultiMap elementMap, Class extensionElementClass) { + public static List getElementsFrom( + MultiMap elementMap, Class extensionElementClass) { QName qname = XmppElementUtil.getQNameFor(extensionElementClass); - List extensionElements = elementMap.getAll(qname); + List extensionElements = elementMap.getAll(qname); if (extensionElements.isEmpty()) { return Collections.emptyList(); } - List res = new ArrayList<>(extensionElements.size()); - for (E extensionElement : extensionElements) { - R e = extensionElementClass.cast(extensionElement); + List res = new ArrayList<>(extensionElements.size()); + for (ExtensionElement extensionElement : extensionElements) { + E e = castOrThrow(extensionElement, extensionElementClass); res.add(e); } return res; } + + public static E castOrThrow(ExtensionElement extensionElement, Class extensionElementClass) { + if (!extensionElementClass.isInstance(extensionElement)) { + final QName qname = getQNameFor(extensionElementClass); + + final String detailMessage; + if (extensionElement instanceof StandardExtensionElement) { + detailMessage = "because there is no according extension element provider registered with ProviderManager for " + + qname + + ". WARNING: This indicates a serious problem with your Smack setup, probably causing Smack not being able to properly initialize itself."; + } else { + Object provider = ProviderManager.getExtensionProvider(qname); + detailMessage = "because there is an inconsistency with the provider registered with ProviderManager: the active provider for " + + qname + " '" + provider.getClass() + + "' does not return instances of type " + extensionElementClass + + ", but instead returns instances of type " + extensionElement.getClass() + "."; + } + + String message = "Extension element is not of expected class '" + extensionElementClass.getName() + "', " + + detailMessage; + throw new IllegalStateException(message); + } + + return extensionElementClass.cast(extensionElement); + } + } 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-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamElements.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamElements.java index 9fcff7b9c..2423b0c1e 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamElements.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/element/MamElements.java @@ -18,9 +18,11 @@ package org.jivesoftware.smackx.mam.element; import java.util.List; +import javax.xml.namespace.QName; + import org.jivesoftware.smack.packet.Element; import org.jivesoftware.smack.packet.ExtensionElement; -import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.MessageView; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.XmlStringBuilder; @@ -54,6 +56,11 @@ public class MamElements { */ public static final String ELEMENT = "result"; + /** + * The qualified name of the MAM result extension element. + */ + public static final QName QNAME = new QName(NAMESPACE, ELEMENT); + /** * id of the result. */ @@ -139,8 +146,8 @@ public class MamElements { return xml; } - public static MamResultExtension from(Message message) { - return (MamResultExtension) message.getExtensionElement(ELEMENT, NAMESPACE); + public static MamResultExtension from(MessageView message) { + return message.getExtension(MamResultExtension.class); } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/element/JingleReason.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/element/JingleReason.java index 91c909aba..a7f1364a4 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/element/JingleReason.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/jingle/element/JingleReason.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017-2019 Florian Schmaus + * Copyright 2017-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -151,7 +151,7 @@ public class JingleReason implements FullyQualifiedElement { @Override public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { - XmlStringBuilder xml = new XmlStringBuilder(this); + XmlStringBuilder xml = new XmlStringBuilder(this, enclosingNamespace); xml.rightAngleBracket(); xml.openElement(reason.asString); 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-streammanagement/build.gradle b/smack-streammanagement/build.gradle new file mode 100644 index 000000000..c06c423d9 --- /dev/null +++ b/smack-streammanagement/build.gradle @@ -0,0 +1,8 @@ +description = """\ +Smack support for XMPP Stream Management (XEP-0198).""" + +dependencies { + api project(':smack-core') + + testFixturesApi(testFixtures(project(":smack-core"))) +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/SMUtils.java b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/SMUtils.java similarity index 100% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/SMUtils.java rename to smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/SMUtils.java diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementException.java b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/StreamManagementException.java similarity index 100% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementException.java rename to smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/StreamManagementException.java diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementModule.java b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/StreamManagementModule.java similarity index 98% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementModule.java rename to smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/StreamManagementModule.java index 58be31a34..1a6c78a93 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementModule.java +++ b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/StreamManagementModule.java @@ -91,7 +91,7 @@ public class StreamManagementModule extends ModularXmppClientToServerConnectionM addPredeccessor(AuthenticatedButUnboundStateDescriptor.class); addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); declarePrecedenceOver(ResourceBindingStateDescriptor.class); - declareInferiortyTo(CompressionStateDescriptor.class); + declareInferiorityTo(CompressionStateDescriptor.class); } @Override diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementModuleDescriptor.java b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/StreamManagementModuleDescriptor.java similarity index 100% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementModuleDescriptor.java rename to smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/StreamManagementModuleDescriptor.java diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/package-info.java b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/package-info.java similarity index 100% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/package-info.java rename to smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/package-info.java diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/packet/StreamManagement.java b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/packet/StreamManagement.java similarity index 100% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/packet/StreamManagement.java rename to smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/packet/StreamManagement.java diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/packet/package-info.java b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/packet/package-info.java similarity index 100% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/packet/package-info.java rename to smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/packet/package-info.java diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/AfterXStanzas.java b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/predicates/AfterXStanzas.java similarity index 100% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/AfterXStanzas.java rename to smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/predicates/AfterXStanzas.java diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/ForEveryMessage.java b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/predicates/ForEveryMessage.java similarity index 100% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/ForEveryMessage.java rename to smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/predicates/ForEveryMessage.java diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/ForEveryStanza.java b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/predicates/ForEveryStanza.java similarity index 100% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/ForEveryStanza.java rename to smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/predicates/ForEveryStanza.java diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/ForMatchingPredicateOrAfterXStanzas.java b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/predicates/ForMatchingPredicateOrAfterXStanzas.java similarity index 100% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/ForMatchingPredicateOrAfterXStanzas.java rename to smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/predicates/ForMatchingPredicateOrAfterXStanzas.java diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/Predicate.java b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/predicates/Predicate.java similarity index 100% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/Predicate.java rename to smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/predicates/Predicate.java diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/ShortcutPredicates.java b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/predicates/ShortcutPredicates.java similarity index 100% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/ShortcutPredicates.java rename to smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/predicates/ShortcutPredicates.java diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/package-info.java b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/predicates/package-info.java similarity index 100% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/package-info.java rename to smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/predicates/package-info.java diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/provider/ParseStreamManagement.java b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/provider/ParseStreamManagement.java similarity index 100% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/provider/ParseStreamManagement.java rename to smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/provider/ParseStreamManagement.java diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/provider/StreamManagementStreamFeatureProvider.java b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/provider/StreamManagementStreamFeatureProvider.java similarity index 100% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/provider/StreamManagementStreamFeatureProvider.java rename to smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/provider/StreamManagementStreamFeatureProvider.java diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/provider/package-info.java b/smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/provider/package-info.java similarity index 100% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/provider/package-info.java rename to smack-streammanagement/src/main/java/org/jivesoftware/smack/sm/provider/package-info.java diff --git a/smack-tcp/src/test/java/org/jivesoftware/smack/sm/provider/ParseStreamManagementTest.java b/smack-streammanagement/src/test/java/org/jivesoftware/smack/sm/provider/ParseStreamManagementTest.java similarity index 100% rename from smack-tcp/src/test/java/org/jivesoftware/smack/sm/provider/ParseStreamManagementTest.java rename to smack-streammanagement/src/test/java/org/jivesoftware/smack/sm/provider/ParseStreamManagementTest.java diff --git a/smack-tcp/build.gradle b/smack-tcp/build.gradle index 74ec76eb8..60b4b2691 100644 --- a/smack-tcp/build.gradle +++ b/smack-tcp/build.gradle @@ -3,6 +3,7 @@ Smack for standard XMPP connections over TCP.""" dependencies { compile project(':smack-core') + api project(':smack-streammanagement') testFixturesApi(testFixtures(project(":smack-core"))) } diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/OnceForThisStanza.java b/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/tcp/OnceForThisStanza.java similarity index 95% rename from smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/OnceForThisStanza.java rename to smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/tcp/OnceForThisStanza.java index 94c9e6863..5c6ab7fe8 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/OnceForThisStanza.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/tcp/OnceForThisStanza.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2014 Florian Schmaus + * Copyright © 2014-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jivesoftware.smack.sm.predicates; +package org.jivesoftware.smack.sm.predicates.tcp; import org.jivesoftware.smack.filter.StanzaFilter; import org.jivesoftware.smack.packet.Stanza; diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/tcp/package-info.java b/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/tcp/package-info.java new file mode 100644 index 000000000..15e022848 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/sm/predicates/tcp/package-info.java @@ -0,0 +1,21 @@ +/** + * + * Copyright 2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * XMPPTCPConnection Stream Managment Predicates. + */ +package org.jivesoftware.smack.sm.predicates.tcp; diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java index 2dbb44a04..188e63186 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java @@ -538,15 +538,24 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } // If we are able to resume the stream, then don't set - // connected/authenticated/usingTLS to false since we like behave like we are still + // connected/authenticated/usingTLS to false since we like to behave like we are still // connected (e.g. sendStanza should not throw a NotConnectedException). - if (isSmResumptionPossible() && instant) { - disconnectedButResumeable = true; + if (instant) { + disconnectedButResumeable = isSmResumptionPossible(); + if (!disconnectedButResumeable) { + // Reset the stream management session id to null, since the stream is no longer resumable. Note that we + // keep the unacknowledgedStanzas queue, because we want to resend them when we are reconnected. + smSessionId = null; + } } else { disconnectedButResumeable = false; - // Reset the stream management session id to null, since if the stream is cleanly closed, i.e. sending a closing - // stream tag, there is no longer a stream to resume. - smSessionId = null; + + // Drop the stream management state if this is not an instant shutdown. We send + // a close tag and now the stream management state is no longer valid. + // This also prevents that we will potentially (re-)send any unavailable presence we + // may have send, because it got put into the unacknowledged queue and was not acknowledged before the + // connection terminated. + dropSmState(); // Note that we deliberately do not reset authenticatedConnectionInitiallyEstablishedTimestamp here, so that the // information is available in the connectionClosedOnError() listeners. } @@ -836,6 +845,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { // there is an error establishing the connection connectUsingConfiguration(); + connected = true; + // We connected successfully to the servers TCP port initConnection(); diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java index 60ba9fd41..ca003ca8d 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java @@ -58,6 +58,7 @@ import org.jivesoftware.smack.XmppInputOutputFilter; import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.ConnectedButUnauthenticatedStateDescriptor; import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.LookupRemoteConnectionEndpointsStateDescriptor; 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; @@ -68,6 +69,7 @@ import org.jivesoftware.smack.fsm.StateTransitionResult; import org.jivesoftware.smack.internal.SmackTlsContext; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.StartTls; +import org.jivesoftware.smack.packet.StreamClose; import org.jivesoftware.smack.packet.StreamOpen; import org.jivesoftware.smack.packet.TlsFailure; import org.jivesoftware.smack.packet.TlsProceed; @@ -580,6 +582,22 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM super(connectionInternal); } + @Override + public StreamOpenAndCloseFactory getStreamOpenAndCloseFactory() { + return new StreamOpenAndCloseFactory() { + @Override + public StreamOpen createStreamOpen(CharSequence to, CharSequence from, String id, String lang) { + String xmlLang = connectionInternal.connection.getConfiguration().getXmlLang(); + StreamOpen streamOpen = new StreamOpen(to, from, id, xmlLang, StreamOpen.StreamContentNamespace.client); + return streamOpen; + } + @Override + public StreamClose createStreamClose() { + return StreamClose.INSTANCE; + } + }; + } + @Override protected void resetDiscoveredConnectionEndpoints() { discoveredTcpEndpoints = null; @@ -1165,7 +1183,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM } private void waitForHandshakeFinished() throws InterruptedException, CertificateException, SSLException, SmackException, XMPPException { - connectionInternal.waitForCondition(() -> isHandshakeFinished(), "TLS handshake to finish"); + connectionInternal.waitForConditionOrThrowConnectionException(() -> isHandshakeFinished(), "TLS handshake to finish"); if (handshakeStatus == TlsHandshakeStatus.failed) { throw handshakeException; 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 diff --git a/version b/version index c6de5a7d9..07b8ab2a4 100644 --- a/version +++ b/version @@ -1 +1 @@ -4.4.0-alpha6-SNAPSHOT +4.5.0-alpha1-SNAPSHOT