1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2024-11-25 21:42:07 +01:00

Compare commits

...

25 commits

Author SHA1 Message Date
Aditya Borikar
1df0763f92 Remove AbstractWebsocketNonza since it is no longer needed 2021-01-25 20:31:51 +01:00
Florian Schmaus
c5a546554b Rework WebSocket code
Related to SMACK-835.
2021-01-25 19:56:54 +01:00
Florian Schmaus
0c013e4f29 Merge branch '4.4' 2021-01-25 19:48:16 +01:00
Florian Schmaus
9ab50c374d [extensions] Use StanzaView as parameter type in BoBDataExtension.from()
BoBDataExtensions can also appear in IQs (Registration IQ), and
potentially also Presence stanzas.

Fixes SMACK-901.
2021-01-25 14:29:21 +01:00
Florian Schmaus
84d73e9623 [core] Fix XmlEnvironment namespace: Use default namespace (not element's)
Assume the element
<foo:bar xmlns='namespace' xmlns:foo='foo-namespace'/>

then the <bar/> element's namespace is 'foo-namespace', but the
default namespace is 'namespace'. And this is the namespace that
scopes into inner elements.
2021-01-25 14:24:55 +01:00
Florian Schmaus
ec80ab4a20
Merge pull request #456 from Flowdalic/coveralls
[gradle] Bump coveralls-gradle-plugin to 2.10.2
2021-01-12 16:04:22 +01:00
Florian Schmaus
9fb4a2bf4f [gradle] Bump coveralls-gradle-plugin to 2.10.2
This should make it work with Github Actions.
2021-01-12 13:13:16 +01:00
Florian Schmaus
ec2ef1facc
Merge pull request #455 from Flowdalic/fix-data-form-npe
[xdata] Fix NPE in DataForm.Builder.addItem()
2021-01-12 12:31:17 +01:00
Florian Schmaus
d64a9d9029 [xdata] Fix NPE in DataForm.Builder.addItem()
Fixes SMACK-900.
2021-01-12 09:41:46 +01:00
Florian Schmaus
1bfb9f34f1 Merge branch '4.4' 2021-01-12 09:22:42 +01:00
Florian Schmaus
9d734c4a3e
Merge pull request #454 from Flowdalic/readme-build-status-github-actions
[README] Switch "build status" badge from Travis to Github Actions
2021-01-12 09:21:54 +01:00
Florian Schmaus
3a661d71b9
Merge pull request #453 from Flowdalic/fix-caps-npe
[caps] Check for null in EntityCapsManager.addCapsExtension()
2021-01-12 09:12:16 +01:00
Florian Schmaus
8cc4ba0a42 [README] Switch "build status" badge from Travis to Github Actions
This also renames the CI workflow from "Smack CI" to just "CI".
2021-01-12 09:08:34 +01:00
Florian Schmaus
df3ca4100b [caps] Check for null in EntityCapsManager.addCapsExtension()
To avoid a NPE, we check for null in addCapsExtension.

Fixes SMACK-899.
2021-01-12 09:01:19 +01:00
Florian Schmaus
c431d84154 Merge branch '4.4' 2021-01-10 20:45:32 +01:00
Florian Schmaus
938a4271f3
Merge pull request #452 from Flowdalic/abstract-provider-element-type
[core] AbstractProvider should also consider TypeVariable
2021-01-10 20:45:12 +01:00
Florian Schmaus
6eda93228f [core] AbstractProvider should also consider TypeVariable
aTalk shows the following exception:

2020-12-14 12:11:13.704 7370-30976/org.atalk.android E/AndroidRuntime: FATAL EXCEPTION: AccountManager.loadStoredAccounts
    Process: org.atalk.android, PID: 7370
    java.lang.AssertionError: Element type 'EE' is neither of type Class or ParameterizedType
        at org.jivesoftware.smack.provider.AbstractProvider.<init>(AbstractProvider.java:46)
        at org.jivesoftware.smack.provider.Provider.<init>(Provider.java:40)
        at org.jivesoftware.smack.provider.ExtensionElementProvider.<init>(ExtensionElementProvider.java:29)
        at org.xmpp.extensions.DefaultExtensionElementProvider.<init>(DefaultExtensionElementProvider.java:43)
        at org.xmpp.extensions.coin.CoinIQProvider.<init>(CoinIQProvider.java:46)
        at net.java.sip.communicator.impl.protocol.jabber.ProtocolProviderServiceJabberImpl.initialize(ProtocolProviderServiceJabberImpl.java:2091)
        at net.java.sip.communicator.impl.protocol.jabber.ProtocolProviderFactoryJabberImpl.createService(ProtocolProviderFactoryJabberImpl.java:121)
        at net.java.sip.communicator.service.protocol.ProtocolProviderFactory.loadAccount(ProtocolProviderFactory.java:934)
        at net.java.sip.communicator.service.protocol.AccountManager.doLoadStoredAccounts(AccountManager.java:139)
        at net.java.sip.communicator.service.protocol.AccountManager.loadStoredAccounts(AccountManager.java:294)
        at net.java.sip.communicator.service.protocol.AccountManager.runInLoadStoredAccountsThread(AccountManager.java:394)
        at net.java.sip.communicator.service.protocol.AccountManager.access$000(AccountManager.java:36)
        at
		net.java.sip.communicator.service.protocol.AccountManager$1.run(AccountManager.java:329)

where CoinIQProvider line 46-47 [1] reads

    private final DefaultExtensionElementProvider<URIsExtension> urisProvider
            = new
			DefaultExtensionElementProvider<>(URIsExtension.class);

This fixes SMACK-898.

1: f61f264312/aTalk/src/main/java/org/xmpp/extensions/coin/CoinIQProvider.java (L47)
2021-01-10 20:37:01 +01:00
Florian Schmaus
1d5949de4d
Merge pull request #451 from Flowdalic/github-actions
Add GitHub CI actions
2021-01-10 20:36:29 +01:00
Dan Caseley
5b73f2c061 Add Github Actions CI workflow, drop Travis CI
Modified-by: Florian Schmaus <flo@geekplace.eu>
2021-01-10 20:28:34 +01:00
Florian Schmaus
35a71f0131 Merge branch '4.4' 2021-01-06 13:51:38 +01:00
Florian Schmaus
65d7c1c1b8 [im] Update DirectoryRosterStore TODO note regarding Android API 26. 2021-01-06 13:48:49 +01:00
Florian Schmaus
40aa9e87b7 [im] DirectoryRosterStore.readEntry() should also catch IllegalArgumentException
Fixes SMACK-897.
2021-01-06 13:48:03 +01:00
Florian Schmaus
d64ee785bd [extensions] Add BoBDataExtension getBobData() and getContentId()
Those two methods where missing after the BoB API redesign.

Fixes SMACK-896.
2021-01-06 13:47:09 +01:00
Florian Schmaus
fcc372754e Fix NPE in BoBIQ by adding XmlStringBuilder.optIntAttribute(String, Integer)
The method was missing and hence BoBIQ used optIntAttribute(String,
int) instead, which resulted in an NPE if the Integer was null.

Fixes SMACK-895.
2021-01-06 13:45:13 +01:00
Florian Schmaus
ca3679add9 Smack 4.4.1-SNAPSHOT 2020-12-06 12:15:19 +01:00
52 changed files with 1149 additions and 615 deletions

97
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,97 @@
name: CI
env:
GRADLE_VERSION: 6.7.1
on: [push, pull_request]
jobs:
build:
name: Build Smack
runs-on: ubuntu-20.04
strategy:
matrix:
java: [ 1.8, 11 ]
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v1
with:
java-version: ${{ matrix.java }}
# Caches
- name: Cache Maven
uses: actions/cache@v2
with:
path: ~/.m2/repository
key: maven-${{ hashFiles('**/build.gradle') }}
restore-keys: |
maven-
- name: Cache Gradle
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: gradle-caches-${{ hashFiles('**/build.gradle') }}
restore-keys:
gradle-caches
- name: Cache Gradle Binary
uses: actions/cache@v2
with:
path: |
~/gradle-bin-${GRADLE_VERSION}/
key: gradle-bin-${GRADLE_VERSION}
- name: Cache Android SDK
uses: actions/cache@v2
with:
path: |
~/.android/sdk
key: android-${{ hashFiles('build.gradle') }}
restore-keys: |
android-
# Pre-reqs
- name: Grab gradle wrapper
run: |
wget https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-all.zip
unzip gradle-${GRADLE_VERSION}-all.zip
echo "PATH_TO_GRADLE=./gradle-${GRADLE_VERSION}/bin/gradle" >> $GITHUB_ENV
- name: Install GraphViz
run: sudo apt update && sudo apt install graphviz
- name: Install Android SDK Manager
uses: android-actions/setup-android@v2
- name: Install Android SDK
run: |
sdkmanager "platforms;android-19"
# Testing
- name: Gradle Check
run: ${PATH_TO_GRADLE} check --stacktrace
# Test local publish
- name: Gradle publish
run: ${PATH_TO_GRADLE} publishToMavenLocal --stacktrace
# Javadoc
- name: Javadoc
if: ${{ matrix.java == 11 }}
run: ${PATH_TO_GRADLE} javadocAll --stacktrace
# Test Coverage Report
- name: Jacoco Test Coverage
if: ${{ matrix.java == 1.8 }}
run: ${PATH_TO_GRADLE} jacocoRootReport coveralls
env:
COVERALLS_REPO_TOKEN: S2ecSJja2cKJa9yv45C8ZFPohXuRrTXKd
# Upload build artifacts
- name: Upload build artifacts
uses: actions/upload-artifact@v2
with:
name: smack-java-${{ matrix.java }}
path: |
smack-*/build/libs/*.jar
!**/*-test-fixtures.jar
!**/*-tests.jar

View file

@ -1,52 +0,0 @@
language: android
dist: trusty
android:
components:
- android-19
jdk:
- openjdk8
- openjdk11
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.android/build-cache
- $HOME/.m2
before_install:
- export GRADLE_VERSION=6.2
- wget https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-all.zip
- unzip -q gradle-${GRADLE_VERSION}-all.zip
- export PATH="$(pwd)/gradle-${GRADLE_VERSION}/bin:$PATH"
addons:
apt:
update: true
packages:
- graphviz
install: gradle assemble --stacktrace
# Run the test suite and also install the artifacts in the local maven
# archive to additionaly test if artifact creation is
# functional. Which hasn't always be the case in the past, see
# 90cbcaebc7a89f4f771f733a33ac9f389df85be2
# Also run javadocAll to ensure it works.
script:
- |
JAVAC_MAJOR_VERSION=$(javac -version | sed -E 's/javac ([[:digit:]]+).*/\1/')
GRADLE_TASKS=()
GRADLE_TASKS+=(check)
GRADLE_TASKS+=(publishToMavenLocal)
if [[ ${JAVAC_MAJOR_VERSION} -ge 11 ]]; then
GRADLE_TASKS+=(javadocAll)
fi
gradle ${GRADLE_TASKS[@]} --stacktrace
after_success:
- JAVAC_VERSION=$((javac -version) 2>&1)
# Only run jacocoRootReport in the Java 8 build
- if [[ "$JAVAC_VERSION" = javac\ 1.8.* ]]; then gradle jacocoRootReport coveralls; fi

View file

@ -1,7 +1,7 @@
Smack
=====
[![Build Status](https://api.travis-ci.com/igniterealtime/Smack.svg?branch=master)](https://travis-ci.com/github/igniterealtime/Smack) [![Coverage Status](https://coveralls.io/repos/igniterealtime/Smack/badge.svg)](https://coveralls.io/r/igniterealtime/Smack) [![Project Stats](https://www.openhub.net/p/smack/widgets/project_thin_badge.gif)](https://www.openhub.net/p/smack) [![Link to XMPP chat smack@conference.igniterealtime.org](https://inverse.chat/badge.svg?room=smack@conference.igniterealtime.org)](https://inverse.chat/#converse/room?jid=smack@conference.igniterealtime.org)
[![Build Status](https://github.com/igniterealtime/Smack/workflows/CI/badge.svg)](https://github.com/igniterealtime/Smack/actions?query=workflow%3A%22CI%22) [![Coverage Status](https://coveralls.io/repos/igniterealtime/Smack/badge.svg)](https://coveralls.io/r/igniterealtime/Smack) [![Project Stats](https://www.openhub.net/p/smack/widgets/project_thin_badge.gif)](https://www.openhub.net/p/smack) [![Link to XMPP chat smack@conference.igniterealtime.org](https://inverse.chat/badge.svg?room=smack@conference.igniterealtime.org)](https://inverse.chat/#converse/room?jid=smack@conference.igniterealtime.org)
About
-----

View file

@ -7,7 +7,6 @@ buildscript {
dependencies {
classpath 'org.kordamp:markdown-gradle-plugin:1.0.0'
classpath 'org.kordamp.gradle:clirr-gradle-plugin:0.2.2'
classpath "org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.3.1"
}
}
@ -16,6 +15,7 @@ plugins {
id 'net.ltgt.errorprone' version '1.1.1'
// Use e.g. "gradle <task> taskTree" to show its dependency tree.
id 'com.dorongold.task-tree' version '1.5'
id 'com.github.kt3k.coveralls' version '2.10.2'
}
apply plugin: 'org.kordamp.gradle.markdown'

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2009 Jive Software, 2018-2020 Florian Schmaus.
* Copyright 2009 Jive Software, 2018-2021 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -2201,18 +2201,29 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
return SMACK_REACTOR.schedule(runnable, delay, unit, ScheduledAction.Kind.NonBlocking);
}
protected void onStreamOpen(XmlPullParser parser) {
// We found an opening stream.
if ("jabber:client".equals(parser.getNamespace(null))) {
streamId = parser.getAttributeValue("", "id");
incomingStreamXmlEnvironment = XmlEnvironment.from(parser);
/**
* Must be called when a XMPP stream open tag is encountered. Sets values like the stream ID and the incoming stream
* XML environment.
* <p>
* This method also returns a matching stream close tag. For example if the stream open is {@code <stream >}, then
* {@code </stream>} is returned. But if it is {@code <stream:stream>}, then {@code </stream:stream>} is returned.
* Or if it is {@code <foo:stream>}, then {@code </foo:stream>} is returned.
* </p>
*
* @param parser an XML parser that is positioned at the start of the stream open.
* @return a String representing the corresponding stream end tag.
*/
protected String onStreamOpen(XmlPullParser parser) {
assert StreamOpen.ETHERX_JABBER_STREAMS_NAMESPACE.equals(parser.getNamespace());
assert StreamOpen.UNPREFIXED_ELEMENT.equals(parser.getName());
String reportedServerDomainString = parser.getAttributeValue("", "from");
if (reportedServerDomainString == null) {
// RFC 6120 § 4.7.1. makes no explicit statement whether or not 'from' in the stream open from the server
// in c2s connections is required or not.
return;
}
streamId = parser.getAttributeValue("id");
incomingStreamXmlEnvironment = XmlEnvironment.from(parser);
String reportedServerDomainString = parser.getAttributeValue("from");
// RFC 6120 § 4.7.1. makes no explicit statement whether or not 'from' in the stream open from the server
// in c2s connections is required or not.
if (reportedServerDomainString != null) {
DomainBareJid reportedServerDomain;
try {
reportedServerDomain = JidCreate.domainBareFrom(reportedServerDomainString);
@ -2226,6 +2237,12 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
+ "' as reported by server could not be transformed to a valid JID", e);
}
}
String prefix = parser.getPrefix();
if (StringUtils.isNotEmpty(prefix)) {
return "</" + prefix + ":stream>";
}
return "</stream>";
}
protected final void sendStreamOpen() throws NotConnectedException, InterruptedException {
@ -2233,7 +2250,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
// possible. The 'to' attribute is *always* available. The 'from' attribute if set by the user and no external
// mechanism is used to determine the local entity (user). And the 'id' attribute is available after the first
// response from the server (see e.g. RFC 6120 § 9.1.1 Step 2.)
CharSequence to = getXMPPServiceDomain();
DomainBareJid to = getXMPPServiceDomain();
CharSequence from = null;
CharSequence localpart = config.getUsername();
if (localpart != null) {
@ -2247,7 +2264,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
updateOutgoingStreamXmlEnvironmentOnStreamOpen(streamOpen);
}
protected AbstractStreamOpen getStreamOpen(CharSequence to, CharSequence from, String id, String lang) {
protected AbstractStreamOpen getStreamOpen(DomainBareJid to, CharSequence from, String id, String lang) {
return new StreamOpen(to, from, id, lang);
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2017-2020 Florian Schmaus
* Copyright 2017-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -75,6 +75,10 @@ public abstract class SmackFuture<V, E extends Exception> implements Future<V>,
@Override
public final synchronized boolean isDone() {
return result != null || exception != null || cancelled;
}
public final synchronized boolean wasSuccessful() {
return result != null;
}
@ -162,6 +166,10 @@ public abstract class SmackFuture<V, E extends Exception> implements Future<V>,
return result;
}
public E getExceptionIfAvailable() {
return exception;
}
protected final synchronized void maybeInvokeCallbacks() {
if (cancelled) {
return;
@ -326,6 +334,11 @@ public abstract class SmackFuture<V, E extends Exception> implements Future<V>,
return future;
}
public static boolean await(Collection<? extends SmackFuture<?, ?>> futures, long timeout)
throws InterruptedException {
return await(futures, timeout, TimeUnit.MILLISECONDS);
}
public static boolean await(Collection<? extends SmackFuture<?, ?>> futures, long timeout, TimeUnit unit) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(futures.size());
for (SmackFuture<?, ?> future : futures) {

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2018-2020 Florian Schmaus
* Copyright 2018-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -139,13 +139,8 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne
}
@Override
public void setCurrentConnectionExceptionAndNotify(Exception exception) {
ModularXmppClientToServerConnection.this.setCurrentConnectionExceptionAndNotify(exception);
}
@Override
public void onStreamOpen(XmlPullParser parser) {
ModularXmppClientToServerConnection.this.onStreamOpen(parser);
public String onStreamOpen(XmlPullParser parser) {
return ModularXmppClientToServerConnection.this.onStreamOpen(parser);
}
@Override
@ -571,7 +566,7 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne
}
@Override
protected AbstractStreamOpen getStreamOpen(CharSequence to, CharSequence from, String id, String lang) {
protected AbstractStreamOpen getStreamOpen(DomainBareJid to, CharSequence from, String id, String lang) {
StreamOpenAndCloseFactory streamOpenAndCloseFactory = activeTransport.getStreamOpenAndCloseFactory();
return streamOpenAndCloseFactory.createStreamOpen(to, from, id, lang);
}
@ -720,6 +715,11 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne
throw SmackException.NoEndpointsDiscoveredException.from(lookupFailures);
}
if (!lookupFailures.isEmpty()) {
// TODO: Put those non-fatal lookup failures into a sink of the connection so that the user is able to
// be aware of them.
}
// Even though the outgoing elements queue is unrelated to the lookup remote connection endpoints state, we
// do start the queue at this point. The transports will need it available, and we use the state's reset()
// function to close the queue again on failure.
@ -1110,7 +1110,12 @@ public final class ModularXmppClientToServerConnection extends AbstractXMPPConne
XmppClientToServerTransport.Stats stats = entry.getValue();
StringUtils.appendHeading(appendable, transportClass.getName());
appendable.append(stats.toString()).append('\n');
if (stats != null) {
appendable.append(stats.toString());
} else {
appendable.append("No stats available.");
}
appendable.append('\n');
}
for (Map.Entry<String, Object> entry : filtersStats.entrySet()) {

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2019-2020 Florian Schmaus
* Copyright 2019-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -62,6 +62,10 @@ public final class ModularXmppClientToServerConnectionConfiguration extends Conn
// configuration, e.g. there is no edge from disconnected to connected.
throw new IllegalStateException(e);
}
for (ModularXmppClientToServerConnectionModuleDescriptor moduleDescriptor : moduleDescriptors) {
moduleDescriptor.validateConfiguration(this);
}
}
public void printStateGraphInDotFormat(PrintWriter pw, boolean breakStateName) {

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2019-2020 Florian Schmaus
* Copyright 2019-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -28,6 +28,9 @@ public abstract class ModularXmppClientToServerConnectionModuleDescriptor {
protected abstract ModularXmppClientToServerConnectionModule<? extends ModularXmppClientToServerConnectionModuleDescriptor> constructXmppConnectionModule(
ModularXmppClientToServerConnectionInternal connectionInternal);
protected void validateConfiguration(ModularXmppClientToServerConnectionConfiguration configuration) {
}
public abstract static class Builder {
private final ModularXmppClientToServerConnectionConfiguration.Builder connectionConfigurationBuilder;

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Aditya Borikar.
* Copyright 2020 Aditya Borikar, 2021 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,8 +19,12 @@ package org.jivesoftware.smack.c2s;
import org.jivesoftware.smack.packet.AbstractStreamClose;
import org.jivesoftware.smack.packet.AbstractStreamOpen;
import org.jxmpp.jid.DomainBareJid;
public interface StreamOpenAndCloseFactory {
AbstractStreamOpen createStreamOpen(CharSequence to, CharSequence from, String id, String lang);
AbstractStreamOpen createStreamOpen(DomainBareJid to, CharSequence from, String id, String lang);
AbstractStreamClose createStreamClose();
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2019-2020 Florian Schmaus
* Copyright 2019-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -37,6 +37,8 @@ public abstract class XmppClientToServerTransport {
protected abstract void loadConnectionEndpoints(LookupConnectionEndpointsSuccess lookupConnectionEndpointsSuccess);
public abstract boolean hasUseableConnectionEndpoints();
/**
* Notify the transport that new outgoing data is available. Usually this method does not need to be called
* explicitly, only if the filters are modified so that they potentially produced new data.

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Florian Schmaus
* Copyright 2020-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
*/
package org.jivesoftware.smack.c2s.internal;
import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
@ -39,8 +40,10 @@ import org.jivesoftware.smack.packet.Nonza;
import org.jivesoftware.smack.packet.TopLevelStreamElement;
import org.jivesoftware.smack.packet.XmlEnvironment;
import org.jivesoftware.smack.util.Consumer;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smack.util.Supplier;
import org.jivesoftware.smack.xml.XmlPullParser;
import org.jivesoftware.smack.xml.XmlPullParserException;
public abstract class ModularXmppClientToServerConnectionInternal {
@ -85,9 +88,19 @@ public abstract class ModularXmppClientToServerConnectionInternal {
public abstract void notifyConnectionError(Exception e);
public abstract void setCurrentConnectionExceptionAndNotify(Exception exception);
public final String onStreamOpen(String streamOpen) {
XmlPullParser streamOpenParser;
try {
streamOpenParser = PacketParserUtils.getParserFor(streamOpen);
} catch (XmlPullParserException | IOException e) {
// Should never happen.
throw new AssertionError(e);
}
String streamClose = onStreamOpen(streamOpenParser);
return streamClose;
}
public abstract void onStreamOpen(XmlPullParser parser);
public abstract String onStreamOpen(XmlPullParser parser);
public abstract void onStreamClosed();

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2018-2020 Florian Schmaus
* Copyright 2018-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,6 +20,7 @@ import java.io.IOException;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.c2s.XmppClientToServerTransport;
import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal;
import org.jivesoftware.smack.c2s.internal.WalkStateGraphContext;
@ -75,4 +76,24 @@ public abstract class State {
}
}
public abstract static class AbstractTransport extends State {
private final XmppClientToServerTransport transport;
protected AbstractTransport(XmppClientToServerTransport transport, StateDescriptor stateDescriptor,
ModularXmppClientToServerConnectionInternal connectionInternal) {
super(stateDescriptor, connectionInternal);
this.transport = transport;
}
@Override
public StateTransitionResult.TransitionImpossible isTransitionToPossible(WalkStateGraphContext walkStateGraphContext)
throws SmackException {
if (!transport.hasUseableConnectionEndpoints()) {
return new StateTransitionResult.TransitionImpossibleBecauseNoEndpointsDiscovered(transport);
}
return super.isTransitionToPossible(walkStateGraphContext);
}
}
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2018-2020 Florian Schmaus
* Copyright 2018-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,6 +16,8 @@
*/
package org.jivesoftware.smack.fsm;
import org.jivesoftware.smack.c2s.XmppClientToServerTransport;
public abstract class StateTransitionResult {
private final String message;
@ -92,4 +94,10 @@ public abstract class StateTransitionResult {
super(stateDescriptor.getFullStateName(false) + " is not implemented (yet)");
}
}
public static class TransitionImpossibleBecauseNoEndpointsDiscovered extends TransitionImpossibleReason {
public TransitionImpossibleBecauseNoEndpointsDiscovered(XmppClientToServerTransport transport) {
super("The transport " + transport + " did not discover any endpoints");
}
}
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Florian Schmaus, Aditya Borikar
* Copyright 2020-2021 Florian Schmaus, 2020 Aditya Borikar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -28,6 +28,7 @@ import org.jivesoftware.smack.util.XmlStringBuilder;
* be achieved through {@link XMPPConnection#sendNonza(Nonza)}.
*/
public abstract class AbstractStreamOpen implements Nonza {
public static final String ETHERX_JABBER_STREAMS_NAMESPACE = "http://etherx.jabber.org/streams";
public static final String CLIENT_NAMESPACE = "jabber:client";
public static final String SERVER_NAMESPACE = "jabber:server";

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2018 Florian Schmaus
* Copyright 2018-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,6 +20,8 @@ public final class StreamClose extends AbstractStreamClose {
public static final StreamClose INSTANCE = new StreamClose();
public static final String STRING = "</" + StreamOpen.ELEMENT + ">";
private StreamClose() {
}
@ -39,4 +41,8 @@ public final class StreamClose extends AbstractStreamClose {
return StreamOpen.ELEMENT;
}
@Override
public String toString() {
return STRING;
}
}

View file

@ -23,7 +23,9 @@ import org.jivesoftware.smack.util.XmlStringBuilder;
* The stream open <b>tag</b>.
*/
public final class StreamOpen extends AbstractStreamOpen {
public static final String ELEMENT = "stream:stream";
public static final String UNPREFIXED_ELEMENT = "stream";
public static final String ELEMENT = "stream:" + UNPREFIXED_ELEMENT;
public StreamOpen(CharSequence to) {
this(to, null, null, null, StreamContentNamespace.client);

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2019 Florian Schmaus
* Copyright 2019-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -133,7 +133,7 @@ public class XmlEnvironment {
}
public static XmlEnvironment from(XmlPullParser parser, XmlEnvironment outerXmlEnvironment) {
String namespace = parser.getNamespace();
String namespace = parser.getDefaultNamespace();
String xmlLang = ParserUtils.getXmlLang(parser);
return new XmlEnvironment(namespace, xmlLang, outerXmlEnvironment);
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2019-2020 Florian Schmaus
* Copyright 2019-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,6 +18,7 @@ package org.jivesoftware.smack.provider;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import org.jivesoftware.smack.packet.Element;
@ -42,9 +43,12 @@ public class AbstractProvider<E extends Element> {
} else if (elementType instanceof ParameterizedType) {
ParameterizedType parameteriezedElementType = (ParameterizedType) elementType;
elementClass = (Class<E>) parameteriezedElementType.getRawType();
} else if (elementType instanceof TypeVariable) {
TypeVariable<?> typeVariableElementType = (TypeVariable<?>) elementType;
elementClass = (Class<E>) typeVariableElementType.getClass();
} else {
throw new AssertionError(
"Element type '" + elementType + "' is neither of type Class or ParameterizedType");
throw new AssertionError("Element type '" + elementType + "' (" + elementType.getClass()
+ ") is neither of type Class, ParameterizedType or TypeVariable");
}
}

View file

@ -389,6 +389,22 @@ public class XmlStringBuilder implements Appendable, CharSequence, Element {
return this;
}
/**
* If the provided Integer argument is not null, then add a new XML attribute with the given name and the Integer as
* value.
*
* @param name the XML attribute name.
* @param value the optional integer to use as the attribute's value.
* @return a reference to this object.
* @since 4.4.1
*/
public XmlStringBuilder optIntAttribute(String name, Integer value) {
if (value != null) {
attribute(name, value.toString());
}
return this;
}
/**
* Add the given attribute if value not null and {@code value => 0}.
*

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2019 Florian Schmaus
* Copyright 2019-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -23,6 +23,9 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
import javax.xml.XMLConstants;
@ -207,6 +210,34 @@ public class XmlPullParserTest {
}
}
@ParameterizedTest
@EnumSource(SmackTestUtil.XmlPullParserKind.class)
public void testGetNamespace(SmackTestUtil.XmlPullParserKind parserKind)
throws XmlPullParserException, IOException {
String xml = "<foo:bar xmlns='namespace' xmlns:foo='foo-namespace'/>";
XmlPullParser parser = SmackTestUtil.getParserFor(xml, parserKind);
String parsedName = parser.getName();
assertEquals("bar", parsedName);
String parsedPrefix = parser.getPrefix();
assertEquals("foo", parsedPrefix);
String parsedPrefixNamespace = parser.getNamespace(parsedPrefix);
assertEquals("foo-namespace", parsedPrefixNamespace);
String parsedNamespace = parser.getNamespace();
assertEquals("foo-namespace", parsedNamespace);
List<Function<XmlPullParser, String>> defaultNamespaceRetrievers = new ArrayList<>();
defaultNamespaceRetrievers.add(p -> p.getNamespace(null));
defaultNamespaceRetrievers.add(p -> p.getDefaultNamespace());
for (Function<XmlPullParser, String> defaultNamespaceRetriever : defaultNamespaceRetrievers) {
String defaultNamespace = defaultNamespaceRetriever.apply(parser);
assertEquals("namespace", defaultNamespace);
}
}
private static void assertAttributeHolds(XmlPullParser parser, int attributeIndex, String expectedLocalpart,
String expectedPrefix, String expectedNamespace) {
QName qname = parser.getAttributeQName(attributeIndex);

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Florian Schmaus
* Copyright 2020-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -17,7 +17,7 @@
package org.jivesoftware.smackx.bob.element;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.StanzaView;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smackx.bob.BoBData;
@ -60,6 +60,26 @@ public class BoBDataExtension implements ExtensionElement {
return NAMESPACE;
}
/**
* Get the content ID.
*
* @return the content ID.
* @since 4.4.1
*/
public final ContentId getContentId() {
return cid;
}
/**
* Get the Bits of Binary (BOB) data.
*
* @return the BoB data.
* @since 4.4.1
*/
public final BoBData getBobData() {
return bobData;
}
@Override
public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) {
XmlStringBuilder xml = new XmlStringBuilder(this);
@ -74,8 +94,8 @@ public class BoBDataExtension implements ExtensionElement {
return xml;
}
public static BoBDataExtension from(Message message) {
return message.getExtension(BoBDataExtension.class);
public static BoBDataExtension from(StanzaView stanza) {
return stanza.getExtension(BoBDataExtension.class);
}
}

View file

@ -312,7 +312,10 @@ public final class EntityCapsManager extends Manager {
// XEP-0115 specifies that a client SHOULD include entity capabilities
// with every presence notification it sends.
private void addCapsExtension(PresenceBuilder presenceBuilder) {
CapsVersionAndHash capsVersionAndHash = getCapsVersionAndHash();
final CapsVersionAndHash capsVersionAndHash = getCapsVersionAndHash();
if (capsVersionAndHash == null) {
return;
}
CapsExtension caps = new CapsExtension(entityNode, capsVersionAndHash.version, capsVersionAndHash.hash);
presenceBuilder.overrideExtension(caps);
}
@ -434,10 +437,10 @@ public final class EntityCapsManager extends Manager {
}
/**
* Get our own caps version. The version depends on the enabled features.
* Get our own caps version or {@code null} if none is yet set. The version depends on the enabled features.
* A caps version looks like '66/0NaeaBKkwk85efJTGmU47vXI='
*
* @return our own caps version
* @return our own caps version or {@code null}.
*/
public CapsVersionAndHash getCapsVersionAndHash() {
return currentCapsVersion;

View file

@ -494,6 +494,9 @@ public final class DataForm implements ExtensionElement {
* @return a reference to this builder.
*/
public Builder addItem(Item item) {
if (items == null) {
items = new ArrayList<>();
}
items.add(item);
return this;
}

View file

@ -183,7 +183,7 @@ public final class DirectoryRosterStore implements RosterStore {
private static Item readEntry(File file) {
Reader reader;
try {
// TODO: Should use Files.newBufferedReader() but it is not available on Android.
// TODO: Use Files.newBufferedReader() once Smack's minimum Android API level is 26 or higher.
reader = new FileReader(file);
} catch (FileNotFoundException e) {
LOGGER.log(Level.FINE, "Roster entry file not found", e);
@ -195,7 +195,7 @@ public final class DirectoryRosterStore implements RosterStore {
Item item = RosterPacketProvider.parseItem(parser);
reader.close();
return item;
} catch (XmlPullParserException | IOException e) {
} catch (XmlPullParserException | IOException | IllegalArgumentException e) {
boolean deleted = file.delete();
String message = "Exception while parsing roster entry.";
if (deleted) {

View file

@ -12,7 +12,8 @@ dependencies {
api project(':smack-openpgp')
api project(':smack-resolver-minidns')
api project(':smack-resolver-minidns-dox')
api project(':smack-websocket')
// TODO: Change this to smack-websocket-java11 once it arrives.
api project(':smack-websocket-okhttp')
api project(':smack-tcp')
testImplementation(testFixtures(project(":smack-core")))

View file

@ -0,0 +1,110 @@
/**
*
* Copyright 2021 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.full;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Date;
import java.util.logging.Logger;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection;
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration;
import org.jivesoftware.smack.debugger.ConsoleDebugger;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.websocket.XmppWebSocketTransportModuleDescriptor;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jxmpp.util.XmppDateTime;
public class WebSocketConnectionTest {
static {
SmackConfiguration.DEBUG = true;
}
public static void main(String[] args)
throws URISyntaxException, SmackException, IOException, XMPPException, InterruptedException {
String jid, password, websocketEndpoint, messageTo = null;
if (args.length < 3 || args.length > 4) {
throw new IllegalArgumentException();
}
jid = args[0];
password = args[1];
websocketEndpoint = args[2];
if (args.length >= 4) {
messageTo = args[3];
}
testWebSocketConnection(jid, password, websocketEndpoint, messageTo);
}
public static void testWebSocketConnection(String jid, String password, String websocketEndpoint)
throws URISyntaxException, SmackException, IOException, XMPPException, InterruptedException {
testWebSocketConnection(jid, password, websocketEndpoint, null);
}
public static void testWebSocketConnection(String jid, String password, String websocketEndpoint, String messageTo)
throws URISyntaxException, SmackException, IOException, XMPPException, InterruptedException {
ModularXmppClientToServerConnectionConfiguration.Builder builder = ModularXmppClientToServerConnectionConfiguration.builder();
builder.removeAllModules()
.setXmppAddressAndPassword(jid, password)
.setDebuggerFactory(ConsoleDebugger.Factory.INSTANCE)
;
XmppWebSocketTransportModuleDescriptor.Builder websocketBuilder = XmppWebSocketTransportModuleDescriptor.getBuilder(builder);
websocketBuilder.explicitlySetWebSocketEndpointAndDiscovery(websocketEndpoint, false);
builder.addModule(websocketBuilder.build());
ModularXmppClientToServerConnectionConfiguration config = builder.build();
ModularXmppClientToServerConnection connection = new ModularXmppClientToServerConnection(config);
connection.setReplyTimeout(5 * 60 * 1000);
connection.addConnectionStateMachineListener((event, c) -> {
Logger.getAnonymousLogger().info("Connection event: " + event);
});
connection.connect();
connection.login();
if (messageTo != null) {
Message message = connection.getStanzaFactory().buildMessageStanza()
.to(messageTo)
.setBody("It is alive! " + XmppDateTime.formatXEP0082Date(new Date()))
.build()
;
connection.sendStanza(message);
}
Thread.sleep(1000);
connection.disconnect();
ModularXmppClientToServerConnection.Stats connectionStats = connection.getStats();
ServiceDiscoveryManager.Stats serviceDiscoveryManagerStats = ServiceDiscoveryManager.getInstanceFor(connection).getStats();
// CHECKSTYLE:OFF
System.out.println("WebSocket successfully finished, yeah!\n" + connectionStats + '\n' + serviceDiscoveryManagerStats);
// CHECKSTYLE:ON
}
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2018-2020 Florian Schmaus
* Copyright 2018-2021 Florian Schmaus
*
* This file is part of smack-repl.
*
@ -25,8 +25,6 @@ import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.logging.Logger;
import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.SmackException;
@ -38,18 +36,11 @@ import org.jivesoftware.smack.compression.XMPPInputOutputStream;
import org.jivesoftware.smack.compression.XMPPInputOutputStream.FlushMethod;
import org.jivesoftware.smack.debugger.ConsoleDebugger;
import org.jivesoftware.smack.debugger.SmackDebuggerFactory;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.sm.StreamManagementModuleDescriptor;
import org.jivesoftware.smack.tcp.XmppTcpTransportModuleDescriptor;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jxmpp.util.XmppDateTime;
public class Nio {
private static final Logger LOGGER = Logger.getLogger(Nio.class.getName());
public static void main(String[] args) throws SmackException, IOException, XMPPException, InterruptedException {
doNio(args[0], args[1], args[2]);
}
@ -111,30 +102,7 @@ public class Nio {
connection.setReplyTimeout(5 * 60 * 1000);
connection.addConnectionStateMachineListener((event, c) -> {
LOGGER.info("Connection event: " + event);
});
connection.connect();
connection.login();
Message message = connection.getStanzaFactory().buildMessageStanza()
.to("flo@geekplace.eu")
.setBody("It is alive! " + XmppDateTime.formatXEP0082Date(new Date()))
.build();
connection.sendStanza(message);
Thread.sleep(1000);
connection.disconnect();
ModularXmppClientToServerConnection.Stats connectionStats = connection.getStats();
ServiceDiscoveryManager.Stats serviceDiscoveryManagerStats = ServiceDiscoveryManager.getInstanceFor(connection).getStats();
// CHECKSTYLE:OFF
System.out.println("NIO successfully finished, yeah!\n" + connectionStats + '\n' + serviceDiscoveryManagerStats);
// CHECKSTYLE:ON
XmppTools.modularConnectionTest(connection, "flo@geekplace.eu");
}
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Aditya Borikar
* Copyright 2021 Florian Schmaus
*
* This file is part of smack-repl.
*
@ -21,7 +21,6 @@
package org.igniterealtime.smack.smackrepl;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import org.jivesoftware.smack.SmackException;
@ -29,25 +28,51 @@ import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection;
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration;
import org.jivesoftware.smack.util.TLSUtils;
import org.jivesoftware.smack.websocket.XmppWebSocketTransportModuleDescriptor;
public class WebSocketConnection {
public static void main(String[] args) throws SmackException, IOException, XMPPException, InterruptedException, URISyntaxException {
ModularXmppClientToServerConnectionConfiguration.Builder builder = ModularXmppClientToServerConnectionConfiguration.builder();
builder.removeAllModules();
builder.setXmppAddressAndPassword(args[0], args[1]);
public static void main(String[] args)
throws URISyntaxException, SmackException, IOException, XMPPException, InterruptedException {
String jid, password, websocketEndpoint, messageTo = null;
if (args.length < 3 || args.length > 4) {
throw new IllegalArgumentException();
}
jid = args[0];
password = args[1];
websocketEndpoint = args[2];
if (args.length >= 4) {
messageTo = args[3];
}
TLSUtils.setDefaultTrustStoreTypeToJksIfRequired();
testWebSocketConnection(jid, password, websocketEndpoint, messageTo);
}
public static void testWebSocketConnection(String jid, String password, String websocketEndpoint)
throws URISyntaxException, SmackException, IOException, XMPPException, InterruptedException {
testWebSocketConnection(jid, password, websocketEndpoint, null);
}
public static void testWebSocketConnection(String jid, String password, String websocketEndpoint, String messageTo)
throws URISyntaxException, SmackException, IOException, XMPPException, InterruptedException {
ModularXmppClientToServerConnectionConfiguration.Builder builder = ModularXmppClientToServerConnectionConfiguration.builder();
builder.removeAllModules()
.setXmppAddressAndPassword(jid, password)
;
// Set a fallback uri into websocket transport descriptor and add this descriptor into connection builder.
XmppWebSocketTransportModuleDescriptor.Builder websocketBuilder = XmppWebSocketTransportModuleDescriptor.getBuilder(builder);
websocketBuilder.explicitlySetWebSocketEndpointAndDiscovery(new URI(args[2]), false);
websocketBuilder.explicitlySetWebSocketEndpointAndDiscovery(websocketEndpoint, false);
builder.addModule(websocketBuilder.build());
ModularXmppClientToServerConnectionConfiguration config = builder.build();
ModularXmppClientToServerConnection connection = new ModularXmppClientToServerConnection(config);
connection.connect();
connection.login();
connection.disconnect();
connection.setReplyTimeout(5 * 60 * 1000);
XmppTools.modularConnectionTest(connection, messageTo);
}
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2016 Florian Schmaus
* Copyright 2016-2021 Florian Schmaus
*
* This file is part of smack-repl.
*
@ -23,6 +23,8 @@ package org.igniterealtime.smack.smackrepl;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.logging.Logger;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.SmackException.NoResponseException;
@ -30,15 +32,19 @@ import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jivesoftware.smack.util.TLSUtils;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.iqregister.AccountManager;
import org.jxmpp.jid.DomainBareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.jid.parts.Localpart;
import org.jxmpp.stringprep.XmppStringprepException;
import org.jxmpp.util.XmppDateTime;
public class XmppTools {
@ -106,4 +112,40 @@ public class XmppTools {
connection.disconnect();
}
}
public static void modularConnectionTest(ModularXmppClientToServerConnection connection, String messageTo) throws XMPPException, SmackException, IOException, InterruptedException {
connection.addConnectionStateMachineListener((event, c) -> {
Logger.getAnonymousLogger().info("Connection event: " + event);
});
connection.connect();
connection.login();
XmppTools.sendItsAlive(messageTo, connection);
Thread.sleep(1000);
connection.disconnect();
ModularXmppClientToServerConnection.Stats connectionStats = connection.getStats();
ServiceDiscoveryManager.Stats serviceDiscoveryManagerStats = ServiceDiscoveryManager.getInstanceFor(connection).getStats();
// CHECKSTYLE:OFF
System.out.println("NIO successfully finished, yeah!\n" + connectionStats + '\n' + serviceDiscoveryManagerStats);
// CHECKSTYLE:ON
}
public static void sendItsAlive(String to, XMPPConnection connection)
throws XmppStringprepException, NotConnectedException, InterruptedException {
if (to == null) {
return;
}
Message message = connection.getStanzaFactory().buildMessageStanza()
.to(to)
.setBody("It is alive! " + XmppDateTime.formatXEP0082Date(new Date()))
.build();
connection.sendStanza(message);
}
}

View file

@ -83,6 +83,7 @@ import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.StartTls;
import org.jivesoftware.smack.packet.StreamError;
import org.jivesoftware.smack.packet.StreamOpen;
import org.jivesoftware.smack.proxy.ProxyInfo;
import org.jivesoftware.smack.sasl.packet.SaslNonza;
import org.jivesoftware.smack.sm.SMUtils;
@ -961,6 +962,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
switch (eventType) {
case START_ELEMENT:
final String name = parser.getName();
final String namespace = parser.getNamespace();
switch (name) {
case Message.ELEMENT:
case IQ.IQ_ELEMENT:
@ -972,7 +975,9 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
}
break;
case "stream":
onStreamOpen(parser);
if (StreamOpen.ETHERX_JABBER_STREAMS_NAMESPACE.equals(namespace)) {
onStreamOpen(parser);
}
break;
case "error":
StreamError streamError = PacketParserUtils.parseStreamError(parser);
@ -989,7 +994,6 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
openStreamAndResetParser();
break;
case "failure":
String namespace = parser.getNamespace(null);
switch (namespace) {
case "urn:ietf:params:xml:ns:xmpp-tls":
// TLS negotiation has failed. The server will close the connection

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2019-2020 Florian Schmaus
* Copyright 2019-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -80,14 +80,12 @@ import org.jivesoftware.smack.tcp.rce.RemoteXmppTcpConnectionEndpoints;
import org.jivesoftware.smack.tcp.rce.RemoteXmppTcpConnectionEndpoints.Result;
import org.jivesoftware.smack.tcp.rce.Rfc6120TcpRemoteConnectionEndpoint;
import org.jivesoftware.smack.util.CollectionUtil;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.UTF8;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure;
import org.jivesoftware.smack.xml.XmlPullParser;
import org.jivesoftware.smack.xml.XmlPullParserException;
import org.jxmpp.jid.DomainBareJid;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.util.JidUtil;
import org.jxmpp.xml.splitter.Utf8ByteXmppXmlSplitter;
@ -213,6 +211,8 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM
}
final String prefixXmlns = "xmlns:" + prefix;
// TODO: Use the return value of onStreamOpen(), which now returns the
// corresponding stream close tag, instead of creating it here.
final StringBuilder streamClose = new StringBuilder(32);
final StringBuilder streamOpen = new StringBuilder(256);
@ -253,14 +253,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM
this.streamOpen = streamOpen.toString();
this.streamClose = streamClose.toString();
XmlPullParser streamOpenParser;
try {
streamOpenParser = PacketParserUtils.getParserFor(this.streamOpen);
} catch (XmlPullParserException | IOException e) {
// Should never happen.
throw new AssertionError(e);
}
connectionInternal.onStreamOpen(streamOpenParser);
connectionInternal.onStreamOpen(this.streamOpen);
}
@Override
@ -586,7 +579,7 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM
public StreamOpenAndCloseFactory getStreamOpenAndCloseFactory() {
return new StreamOpenAndCloseFactory() {
@Override
public StreamOpen createStreamOpen(CharSequence to, CharSequence from, String id, String lang) {
public StreamOpen createStreamOpen(DomainBareJid to, CharSequence from, String id, String lang) {
String xmlLang = connectionInternal.connection.getConfiguration().getXmlLang();
StreamOpen streamOpen = new StreamOpen(to, from, id, xmlLang, StreamOpen.StreamContentNamespace.client);
return streamOpen;
@ -603,6 +596,11 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM
discoveredTcpEndpoints = null;
}
@Override
public boolean hasUseableConnectionEndpoints() {
return discoveredTcpEndpoints != null;
}
@Override
protected List<SmackFuture<LookupConnectionEndpointsResult, Exception>> lookupConnectionEndpoints() {
// Assert that there are no stale discovered endpoints prior performing the lookup.
@ -750,10 +748,10 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM
return new EstablishingTcpConnectionState(stateDescriptor, connectionInternal);
}
final class EstablishingTcpConnectionState extends State {
final class EstablishingTcpConnectionState extends State.AbstractTransport {
private EstablishingTcpConnectionState(EstablishingTcpConnectionStateDescriptor stateDescriptor,
ModularXmppClientToServerConnectionInternal connectionInternal) {
super(stateDescriptor, connectionInternal);
super(tcpNioTransport, stateDescriptor, connectionInternal);
}
@Override
@ -777,6 +775,10 @@ public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionM
connectionInternal.setTransport(tcpNioTransport);
// TODO: It appears this should be done in a generic way. I'd assume we always
// have to wait for stream features after the connection was established. If this is true then consider
// moving this into State.AbstractTransport. But I am not yet 100% positive that this is the case for every
// transport. Hence keep it here for now.
connectionInternal.newStreamOpenWaitForFeaturesSequence("stream features after initial connection");
return new TcpSocketConnectedResult(remoteAddress);

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Aditya Borikar
* Copyright 2020 Aditya Borikar, 2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -17,7 +17,6 @@
package org.jivesoftware.smack.websocket.okhttp;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -27,39 +26,34 @@ import org.jivesoftware.smack.debugger.SmackDebugger;
import okhttp3.Headers;
import okhttp3.Response;
import org.jxmpp.xml.splitter.Utf8ByteXmppXmlSplitter;
import org.jxmpp.xml.splitter.XmlPrettyPrinter;
import org.jxmpp.xml.splitter.XmppXmlSplitter;
public final class LoggingInterceptor {
private static final Logger LOGGER = Logger.getAnonymousLogger();
private static final int MAX_ELEMENT_SIZE = 64 * 1024;
private final SmackDebugger debugger;
private final Utf8ByteXmppXmlSplitter incomingTextSplitter;
private final Utf8ByteXmppXmlSplitter outgoingTextSplitter;
private static final Logger LOGGER = Logger.getLogger(LoggingInterceptor.class.getName());
public LoggingInterceptor(SmackDebugger smackDebugger) {
private final SmackDebugger debugger;
private final XmppXmlSplitter incomingXmlSplitter;
private final XmppXmlSplitter outgoingXmlSplitter;
LoggingInterceptor(SmackDebugger smackDebugger) {
this.debugger = smackDebugger;
XmlPrettyPrinter incomingTextPrinter = XmlPrettyPrinter.builder()
.setPrettyWriter(sb -> debugger.incomingStreamSink(sb))
.setTabWidth(4)
.build();
XmppXmlSplitter incomingXmlSplitter = new XmppXmlSplitter(MAX_ELEMENT_SIZE, null,
incomingTextPrinter);
incomingTextSplitter = new Utf8ByteXmppXmlSplitter(incomingXmlSplitter);
incomingXmlSplitter = new XmppXmlSplitter(incomingTextPrinter);
XmlPrettyPrinter outgoingTextPrinter = XmlPrettyPrinter.builder()
.setPrettyWriter(sb -> debugger.outgoingStreamSink(sb))
.setTabWidth(4)
.build();
XmppXmlSplitter outgoingXmlSplitter = new XmppXmlSplitter(MAX_ELEMENT_SIZE, null,
outgoingTextPrinter);
outgoingTextSplitter = new Utf8ByteXmppXmlSplitter(outgoingXmlSplitter);
outgoingXmlSplitter = new XmppXmlSplitter(outgoingTextPrinter);
}
// Open response received here isn't in the form of an Xml an so, there isn't much to format.
public void interceptOpenResponse(Response response) {
void interceptOpenResponse(Response response) {
Headers headers = response.headers();
Iterator<?> iterator = headers.iterator();
StringBuilder sb = new StringBuilder();
@ -70,18 +64,18 @@ public final class LoggingInterceptor {
debugger.incomingStreamSink(sb);
}
public void interceptReceivedText(String text) {
void interceptReceivedText(String text) {
try {
incomingTextSplitter.write(text.getBytes(Charset.defaultCharset()));
incomingXmlSplitter.write(text);
} catch (IOException e) {
// Connections shouldn't be terminated due to exceptions encountered during debugging. hence only log them.
LOGGER.log(Level.WARNING, "IOException encountered while parsing received text: " + text, e);
}
}
public void interceptSentText(String text) {
void interceptSentText(String text) {
try {
outgoingTextSplitter.write(text.getBytes(Charset.defaultCharset()));
outgoingXmlSplitter.write(text);
} catch (IOException e) {
// Connections shouldn't be terminated due to exceptions encountered during debugging, hence only log them.
LOGGER.log(Level.WARNING, "IOException encountered while parsing outgoing text: " + text, e);

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Aditya Borikar
* Copyright 2020 Aditya Borikar, 2020-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,22 +16,17 @@
*/
package org.jivesoftware.smack.websocket.okhttp;
import java.io.IOException;
import java.net.URI;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLSession;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.SmackFuture;
import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal;
import org.jivesoftware.smack.packet.TopLevelStreamElement;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smack.websocket.WebSocketException;
import org.jivesoftware.smack.websocket.elements.WebSocketOpenElement;
import org.jivesoftware.smack.websocket.impl.AbstractWebSocket;
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint;
import org.jivesoftware.smack.xml.XmlPullParserException;
import okhttp3.OkHttpClient;
import okhttp3.Request;
@ -43,135 +38,119 @@ public final class OkHttpWebSocket extends AbstractWebSocket {
private static final Logger LOGGER = Logger.getLogger(OkHttpWebSocket.class.getName());
private static OkHttpClient okHttpClient = null;
private static final OkHttpClient okHttpClient = new OkHttpClient();
// This is a potential candidate to be placed into AbstractWebSocket, but I keep it here until smack-websocket-java11
// arrives.
private final SmackFuture.InternalSmackFuture<AbstractWebSocket, Exception> future = new SmackFuture.InternalSmackFuture<>();
private final ModularXmppClientToServerConnectionInternal connectionInternal;
private final LoggingInterceptor interceptor;
private String openStreamHeader;
private WebSocket currentWebSocket;
private WebSocketConnectionPhase phase;
private WebSocketRemoteConnectionEndpoint connectedEndpoint;
private final WebSocket okHttpWebSocket;
public OkHttpWebSocket(ModularXmppClientToServerConnectionInternal connectionInternal) {
this.connectionInternal = connectionInternal;
public OkHttpWebSocket(WebSocketRemoteConnectionEndpoint endpoint,
ModularXmppClientToServerConnectionInternal connectionInternal) {
super(endpoint, connectionInternal);
if (okHttpClient == null) {
// Creates an instance of okHttp client.
OkHttpClient.Builder builder = new OkHttpClient.Builder();
okHttpClient = builder.build();
}
// Add some mechanism to enable and disable this interceptor.
if (connectionInternal.smackDebugger != null) {
interceptor = new LoggingInterceptor(connectionInternal.smackDebugger);
} else {
interceptor = null;
}
}
@Override
public void connect(WebSocketRemoteConnectionEndpoint endpoint) throws InterruptedException, SmackException, XMPPException {
final String currentUri = endpoint.getWebSocketEndpoint().toString();
final URI uri = endpoint.getUri();
final String url = uri.toString();
Request request = new Request.Builder()
.url(currentUri)
.url(url)
.header("Sec-WebSocket-Protocol", "xmpp")
.build();
WebSocketListener listener = new WebSocketListener() {
okHttpWebSocket = okHttpClient.newWebSocket(request, listener);
}
@Override
public void onOpen(WebSocket webSocket, Response response) {
LOGGER.log(Level.FINER, "WebSocket is open");
phase = WebSocketConnectionPhase.openFrameSent;
if (interceptor != null) {
interceptor.interceptOpenResponse(response);
}
send(new WebSocketOpenElement(connectionInternal.connection.getXMPPServiceDomain()));
private final WebSocketListener listener = new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
LOGGER.log(Level.FINER, "OkHttp invoked onOpen() for {0}. Response: {1}",
new Object[] { webSocket, response });
if (interceptor != null) {
interceptor.interceptOpenResponse(response);
}
@Override
public void onMessage(WebSocket webSocket, String text) {
if (interceptor != null) {
interceptor.interceptReceivedText(text);
}
if (isCloseElement(text)) {
connectionInternal.onStreamClosed();
return;
}
future.setResult(OkHttpWebSocket.this);
}
String closingStream = "</stream>";
switch (phase) {
case openFrameSent:
if (isOpenElement(text)) {
// Converts the <open> element received into <stream> element.
openStreamHeader = getStreamFromOpenElement(text);
phase = WebSocketConnectionPhase.exchangingTopLevelStreamElements;
try {
connectionInternal.onStreamOpen(PacketParserUtils.getParserFor(openStreamHeader));
} catch (XmlPullParserException | IOException e) {
LOGGER.log(Level.WARNING, "Exception caught:", e);
}
} else {
LOGGER.log(Level.WARNING, "Unexpected Frame received", text);
}
break;
case exchangingTopLevelStreamElements:
connectionInternal.parseAndProcessElement(openStreamHeader + text + closingStream);
break;
default:
LOGGER.log(Level.INFO, "Default text: " + text);
}
@Override
public void onMessage(WebSocket webSocket, String text) {
if (interceptor != null) {
interceptor.interceptReceivedText(text);
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
LOGGER.log(Level.INFO, "Exception caught", t);
WebSocketException websocketException = new WebSocketException(t);
if (connectionInternal.connection.isConnected()) {
connectionInternal.notifyConnectionError(websocketException);
} else {
connectionInternal.setCurrentConnectionExceptionAndNotify(websocketException);
}
onIncomingWebSocketElement(text);
}
@Override
public void onFailure(WebSocket webSocket, Throwable throwable, Response response) {
LOGGER.log(Level.FINER, "OkHttp invoked onFailure() for " + webSocket + ". Response: " + response, throwable);
WebSocketException websocketException = new WebSocketException(throwable);
// If we are already connected, then we need to notify the connection that it got tear down. Otherwise we
// need to notify the thread calling connect() that the connection failed.
if (future.wasSuccessful()) {
connectionInternal.notifyConnectionError(websocketException);
} else {
future.setException(websocketException);
}
};
}
// Creates an instance of websocket through okHttpClient.
currentWebSocket = okHttpClient.newWebSocket(request, listener);
@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
LOGGER.log(Level.FINER, "OkHttp invoked onClosing() for " + webSocket + ". Code: " + code + ". Reason: " + reason);
}
// Open a new stream and wait until features are received.
connectionInternal.waitForFeaturesReceived("Waiting to receive features");
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
LOGGER.log(Level.FINER, "OkHttp invoked onClosed() for " + webSocket + ". Code: " + code + ". Reason: " + reason);
}
connectedEndpoint = endpoint;
};
@Override
public SmackFuture<AbstractWebSocket, Exception> getFuture() {
return future;
}
@Override
public void send(TopLevelStreamElement element) {
String textToBeSent = element.toXML().toString();
public void send(String element) {
if (interceptor != null) {
interceptor.interceptSentText(textToBeSent);
interceptor.interceptSentText(element);
}
currentWebSocket.send(textToBeSent);
okHttpWebSocket.send(element);
}
@Override
public void disconnect(int code, String message) {
currentWebSocket.close(code, message);
LOGGER.log(Level.INFO, "WebSocket has been closed with message: " + message);
LOGGER.log(Level.INFO, "WebSocket closing with code: " + code + " and message: " + message);
okHttpWebSocket.close(code, message);
}
@Override
public boolean isConnectionSecure() {
return connectedEndpoint.isSecureEndpoint();
return endpoint.isSecureEndpoint();
}
@Override
public boolean isConnected() {
return connectedEndpoint == null ? false : true;
// TODO: Do we need this method at all if we create an AbstractWebSocket object for every endpoint?
return true;
}
@Override
public SSLSession getSSLSession() {
// TODO: What shall we do about this method, as it appears that OkHttp does not provide access to the used SSLSession?
return null;
}
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Florian Schmaus.
* Copyright 2020-2021 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,12 +19,13 @@ package org.jivesoftware.smack.websocket.okhttp;
import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal;
import org.jivesoftware.smack.websocket.impl.AbstractWebSocket;
import org.jivesoftware.smack.websocket.impl.WebSocketFactory;
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint;
public class OkHttpWebSocketFactory implements WebSocketFactory {
@Override
public AbstractWebSocket create(ModularXmppClientToServerConnectionInternal connectionInternal) {
return new OkHttpWebSocket(connectionInternal);
public AbstractWebSocket create(WebSocketRemoteConnectionEndpoint endpoint, ModularXmppClientToServerConnectionInternal connectionInternal) {
return new OkHttpWebSocket(endpoint, connectionInternal);
}
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Florian Schmaus.
* Copyright 2020-2021 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,6 +16,8 @@
*/
package org.jivesoftware.smack.websocket.okhttp;
import java.net.URISyntaxException;
import org.jivesoftware.smack.websocket.test.WebSocketFactoryServiceTestUtil;
import org.junit.jupiter.api.Test;
@ -23,7 +25,7 @@ import org.junit.jupiter.api.Test;
public class OkHttpWebSocketFactoryServiceTest {
@Test
public void createWebSocketTest() {
public void createWebSocketTest() throws URISyntaxException {
WebSocketFactoryServiceTestUtil.createWebSocketTest(OkHttpWebSocket.class);
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Aditya Borikar, Florian Schmaus.
* Copyright 2020 Aditya Borikar, 2020-2021 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,22 +19,29 @@ package org.jivesoftware.smack.websocket;
import java.util.ArrayList;
import java.util.List;
import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.SmackFuture;
import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal;
import org.jivesoftware.smack.fsm.StateTransitionResult;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.websocket.XmppWebSocketTransportModule.EstablishingWebSocketConnectionState;
import org.jivesoftware.smack.websocket.impl.AbstractWebSocket;
import org.jivesoftware.smack.websocket.impl.WebSocketFactoryService;
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint;
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpointLookup;
public final class WebSocketConnectionAttemptState {
private final ModularXmppClientToServerConnectionInternal connectionInternal;
private final XmppWebSocketTransportModule.XmppWebSocketTransport.DiscoveredWebSocketEndpoints discoveredEndpoints;
private WebSocketRemoteConnectionEndpoint connectedEndpoint;
private AbstractWebSocket webSocket;
WebSocketConnectionAttemptState(ModularXmppClientToServerConnectionInternal connectionInternal,
XmppWebSocketTransportModule.XmppWebSocketTransport.DiscoveredWebSocketEndpoints discoveredWebSocketEndpoints,
EstablishingWebSocketConnectionState establishingWebSocketConnectionState) {
assert discoveredWebSocketEndpoints != null;
assert !discoveredWebSocketEndpoints.result.isEmpty();
this.connectionInternal = connectionInternal;
this.discoveredEndpoints = discoveredWebSocketEndpoints;
}
@ -44,48 +51,96 @@ public final class WebSocketConnectionAttemptState {
*
* @return {@link AbstractWebSocket} with which connection is establised
* @throws InterruptedException if the calling thread was interrupted
* @throws WebSocketException if encounters a websocket exception
*/
AbstractWebSocket establishWebSocketConnection() throws InterruptedException, WebSocketException {
List<WebSocketRemoteConnectionEndpoint> endpoints = discoveredEndpoints.result.discoveredRemoteConnectionEndpoints;
@SuppressWarnings({"incomplete-switch", "MissingCasesInEnumSwitch"})
StateTransitionResult.Failure establishWebSocketConnection() throws InterruptedException {
final WebSocketRemoteConnectionEndpointLookup.Result endpointLookupResult = discoveredEndpoints.result;
final List<Exception> failures = new ArrayList<>(endpointLookupResult.discoveredEndpointCount());
if (endpoints.isEmpty()) {
throw new WebSocketException(new Throwable("No Endpoints discovered to establish connection"));
}
webSocket = null;
List<Throwable> connectionFailureList = new ArrayList<>();
AbstractWebSocket websocket = WebSocketFactoryService.createWebSocket(connectionInternal);
// Keep iterating over available endpoints until a connection is establised or all endpoints are tried to create a connection with.
for (WebSocketRemoteConnectionEndpoint endpoint : endpoints) {
try {
websocket.connect(endpoint);
connectedEndpoint = endpoint;
break;
} catch (Throwable t) {
connectionFailureList.add(t);
// If the number of entries in connectionFailureList is equal to the number of endpoints,
// it means that all endpoints have been tried and have been unsuccessful.
if (connectionFailureList.size() == endpoints.size()) {
WebSocketException websocketException = new WebSocketException(connectionFailureList);
throw new WebSocketException(websocketException);
}
SecurityMode securityMode = connectionInternal.connection.getConfiguration().getSecurityMode();
switch (securityMode) {
case required:
case ifpossible:
establishWebSocketConnection(endpointLookupResult.discoveredSecureEndpoints, failures);
if (webSocket != null) {
return null;
}
}
assert connectedEndpoint != null;
establishWebSocketConnection(endpointLookupResult.discoveredInsecureEndpoints, failures);
if (webSocket != null) {
return null;
}
// Return connected websocket when no failure occurs.
return websocket;
StateTransitionResult.Failure failure = FailedToConnectToAnyWebSocketEndpoint.create(failures);
return failure;
}
/**
* Returns the connected websocket endpoint.
*
* @return connected websocket endpoint
*/
public WebSocketRemoteConnectionEndpoint getConnectedEndpoint() {
return connectedEndpoint;
private void establishWebSocketConnection(List<? extends WebSocketRemoteConnectionEndpoint> webSocketEndpoints,
List<Exception> failures) throws InterruptedException {
final int endpointCount = webSocketEndpoints.size();
List<SmackFuture<AbstractWebSocket, Exception>> futures = new ArrayList<>(endpointCount);
{
List<AbstractWebSocket> webSockets = new ArrayList<>(endpointCount);
// First only create the AbstractWebSocket instances, in case a constructor throws.
for (WebSocketRemoteConnectionEndpoint endpoint : webSocketEndpoints) {
AbstractWebSocket webSocket = WebSocketFactoryService.createWebSocket(endpoint, connectionInternal);
webSockets.add(webSocket);
}
for (AbstractWebSocket webSocket : webSockets) {
SmackFuture<AbstractWebSocket, Exception> future = webSocket.getFuture();
futures.add(future);
}
}
SmackFuture.await(futures, connectionInternal.connection.getReplyTimeout());
for (SmackFuture<AbstractWebSocket, Exception> future : futures) {
AbstractWebSocket connectedWebSocket = future.getIfAvailable();
if (connectedWebSocket == null) {
Exception exception = future.getExceptionIfAvailable();
assert exception != null;
failures.add(exception);
continue;
}
if (webSocket == null) {
webSocket = connectedWebSocket;
// Continue here since we still need to read out the failure exceptions from potential further remaining
// futures and close remaining successfully connected ones.
continue;
}
connectedWebSocket.disconnect(1000, "Using other connection endpoint at " + webSocket.getEndpoint());
}
}
public AbstractWebSocket getConnectedWebSocket() {
return webSocket;
}
public static final class FailedToConnectToAnyWebSocketEndpoint extends StateTransitionResult.Failure {
private final List<Exception> failures;
private FailedToConnectToAnyWebSocketEndpoint(String failureMessage, List<Exception> failures) {
super(failureMessage);
this.failures = failures;
}
public List<Exception> getFailures() {
return failures;
}
private static FailedToConnectToAnyWebSocketEndpoint create(List<Exception> failures) {
StringBuilder sb = new StringBuilder(256);
StringUtils.appendTo(failures, sb, e -> sb.append(e.getMessage()));
String message = sb.toString();
return new FailedToConnectToAnyWebSocketEndpoint(message, failures);
}
}
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Aditya Borikar
* Copyright 2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,23 +16,11 @@
*/
package org.jivesoftware.smack.websocket;
import java.util.Collections;
import java.util.List;
public final class WebSocketException extends Exception {
private static final long serialVersionUID = 1L;
private final List<Throwable> throwableList;
public WebSocketException(List<Throwable> throwableList) {
this.throwableList = throwableList;
}
public WebSocketException(Throwable throwable) {
this.throwableList = Collections.singletonList(throwable);
super("WebSocketException: " + throwable.getMessage(), throwable);
}
public List<Throwable> getThrowableList() {
return throwableList;
}
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Aditya Borikar
* Copyright 2020 Aditya Borikar, 2020-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,20 +16,16 @@
*/
package org.jivesoftware.smack.websocket;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLSession;
import org.jivesoftware.smack.AsyncButOrdered;
import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.SmackFuture;
import org.jivesoftware.smack.SmackFuture.InternalSmackFuture;
import org.jivesoftware.smack.XMPPException;
@ -54,13 +50,13 @@ import org.jivesoftware.smack.websocket.XmppWebSocketTransportModule.XmppWebSock
import org.jivesoftware.smack.websocket.elements.WebSocketCloseElement;
import org.jivesoftware.smack.websocket.elements.WebSocketOpenElement;
import org.jivesoftware.smack.websocket.impl.AbstractWebSocket;
import org.jivesoftware.smack.websocket.rce.InsecureWebSocketRemoteConnectionEndpoint;
import org.jivesoftware.smack.websocket.rce.SecureWebSocketRemoteConnectionEndpoint;
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint;
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpointLookup;
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpointLookup.Result;
import org.jxmpp.jid.DomainBareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
/**
* The websocket transport module that goes with Smack's modular architecture.
@ -101,31 +97,37 @@ public final class XmppWebSocketTransportModule
}
}
final class EstablishingWebSocketConnectionState extends State {
final class EstablishingWebSocketConnectionState extends State.AbstractTransport {
protected EstablishingWebSocketConnectionState(StateDescriptor stateDescriptor,
ModularXmppClientToServerConnectionInternal connectionInternal) {
super(stateDescriptor, connectionInternal);
super(websocketTransport, stateDescriptor, connectionInternal);
}
@Override
public AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext)
throws IOException, SmackException, InterruptedException, XMPPException {
public AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws InterruptedException,
NoResponseException, NotConnectedException, SmackException, XMPPException {
WebSocketConnectionAttemptState connectionAttemptState = new WebSocketConnectionAttemptState(
connectionInternal, discoveredWebSocketEndpoints, this);
try {
websocket = connectionAttemptState.establishWebSocketConnection();
} catch (InterruptedException | WebSocketException e) {
StateTransitionResult.Failure failure = new StateTransitionResult.FailureCausedByException<Exception>(e);
StateTransitionResult.Failure failure = connectionAttemptState.establishWebSocketConnection();
if (failure != null) {
return failure;
}
websocket = connectionAttemptState.getConnectedWebSocket();
connectionInternal.setTransport(websocketTransport);
WebSocketRemoteConnectionEndpoint connectedEndpoint = connectionAttemptState.getConnectedEndpoint();
// TODO: It appears this should be done in a generic way. I'd assume we always
// have to wait for stream features after the connection was established. But I
// am not yet 100% positive that this is the case for every transport. Hence keep it here for now(?).
// See also similar comment in XmppTcpTransportModule.
// Maybe move this into ConnectedButUnauthenticated state's transitionInto() method? That seems to be the
// right place.
connectionInternal.newStreamOpenWaitForFeaturesSequence("stream features after initial connection");
// Construct a WebSocketConnectedResult using the connected endpoint.
return new WebSocketConnectedResult(connectedEndpoint);
return new WebSocketConnectedResult(websocket.getEndpoint());
}
}
@ -140,7 +142,7 @@ public final class XmppWebSocketTransportModule
final WebSocketRemoteConnectionEndpoint connectedEndpoint;
public WebSocketConnectedResult(WebSocketRemoteConnectionEndpoint connectedEndpoint) {
super("WebSocket connection establised with endpoint: " + connectedEndpoint.getWebSocketEndpoint());
super("WebSocket connection establised with endpoint: " + connectedEndpoint);
this.connectedEndpoint = connectedEndpoint;
}
}
@ -164,6 +166,12 @@ public final class XmppWebSocketTransportModule
discoveredWebSocketEndpoints = null;
}
@Override
public boolean hasUseableConnectionEndpoints() {
return discoveredWebSocketEndpoints != null;
}
@SuppressWarnings("incomplete-switch")
@Override
protected List<SmackFuture<LookupConnectionEndpointsResult, Exception>> lookupConnectionEndpoints() {
// Assert that there are no stale discovered endpoints prior performing the lookup.
@ -172,51 +180,56 @@ public final class XmppWebSocketTransportModule
InternalSmackFuture<LookupConnectionEndpointsResult, Exception> websocketEndpointsLookupFuture = new InternalSmackFuture<>();
connectionInternal.asyncGo(() -> {
Result result = null;
WebSocketRemoteConnectionEndpoint providedEndpoint = null;
ModularXmppClientToServerConnectionConfiguration configuration = connectionInternal.connection.getConfiguration();
DomainBareJid host = configuration.getXMPPServiceDomain();
// Check if there is a websocket endpoint already configured.
URI uri = moduleDescriptor.getExplicitlyProvidedUri();
if (uri != null) {
providedEndpoint = new WebSocketRemoteConnectionEndpoint(uri);
if (moduleDescriptor.isWebSocketEndpointDiscoveryEnabled()) {
// Fetch remote endpoints.
result = WebSocketRemoteConnectionEndpointLookup.lookup(host);
}
if (!moduleDescriptor.isWebSocketEndpointDiscoveryEnabled()) {
// If discovery is disabled, assert that the provided endpoint isn't null.
assert providedEndpoint != null;
SecurityMode mode = connectionInternal.connection.getConfiguration().getSecurityMode();
if ((providedEndpoint.isSecureEndpoint() &&
mode.equals(SecurityMode.disabled))
|| (!providedEndpoint.isSecureEndpoint() &&
mode.equals(SecurityMode.required))) {
throw new IllegalStateException("Explicitly configured uri: " + providedEndpoint.getWebSocketEndpoint().toString()
+ " does not comply with the configured security mode: " + mode);
WebSocketRemoteConnectionEndpoint providedEndpoint = moduleDescriptor.getExplicitlyProvidedEndpoint();
if (providedEndpoint != null) {
// If there was not automatic lookup that produced a result, then create a result now.
if (result == null) {
result = new Result();
}
// Generate Result for explicitly configured endpoint.
Result manualResult = new Result(Arrays.asList(providedEndpoint), null);
LookupConnectionEndpointsResult endpointsResult = new DiscoveredWebSocketEndpoints(manualResult);
websocketEndpointsLookupFuture.setResult(endpointsResult);
} else {
DomainBareJid host = connectionInternal.connection.getXMPPServiceDomain();
ModularXmppClientToServerConnectionConfiguration configuration = connectionInternal.connection.getConfiguration();
SecurityMode mode = configuration.getSecurityMode();
// Fetch remote endpoints.
Result xep0156result = WebSocketRemoteConnectionEndpointLookup.lookup(host, mode);
List<WebSocketRemoteConnectionEndpoint> discoveredEndpoints = xep0156result.discoveredRemoteConnectionEndpoints;
// Generate result considering both manual and fetched endpoints.
Result finalResult = new Result(discoveredEndpoints, xep0156result.getLookupFailures());
LookupConnectionEndpointsResult endpointsResult = new DiscoveredWebSocketEndpoints(finalResult);
websocketEndpointsLookupFuture.setResult(endpointsResult);
// We insert the provided endpoint at the beginning of the list, so that it is used first.
final int INSERT_INDEX = 0;
if (providedEndpoint instanceof SecureWebSocketRemoteConnectionEndpoint) {
SecureWebSocketRemoteConnectionEndpoint secureEndpoint = (SecureWebSocketRemoteConnectionEndpoint) providedEndpoint;
result.discoveredSecureEndpoints.add(INSERT_INDEX, secureEndpoint);
} else if (providedEndpoint instanceof InsecureWebSocketRemoteConnectionEndpoint) {
InsecureWebSocketRemoteConnectionEndpoint insecureEndpoint = (InsecureWebSocketRemoteConnectionEndpoint) providedEndpoint;
result.discoveredInsecureEndpoints.add(INSERT_INDEX, insecureEndpoint);
} else {
throw new AssertionError();
}
}
if (moduleDescriptor.isImplicitWebSocketEndpointEnabled()) {
String urlWithoutScheme = "://" + host + ":5443/ws";
SecureWebSocketRemoteConnectionEndpoint implicitSecureEndpoint = SecureWebSocketRemoteConnectionEndpoint.from(
WebSocketRemoteConnectionEndpoint.SECURE_WEB_SOCKET_SCHEME + urlWithoutScheme);
result.discoveredSecureEndpoints.add(implicitSecureEndpoint);
InsecureWebSocketRemoteConnectionEndpoint implicitInsecureEndpoint = InsecureWebSocketRemoteConnectionEndpoint.from(
WebSocketRemoteConnectionEndpoint.INSECURE_WEB_SOCKET_SCHEME + urlWithoutScheme);
result.discoveredInsecureEndpoints.add(implicitInsecureEndpoint);
}
final LookupConnectionEndpointsResult endpointsResult;
if (result.isEmpty()) {
endpointsResult = new WebSocketEndpointsDiscoveryFailed(result.lookupFailures);
} else {
endpointsResult = new DiscoveredWebSocketEndpoints(result);
}
websocketEndpointsLookupFuture.setResult(endpointsResult);
});
return Collections.singletonList(websocketEndpointsLookupFuture);
@ -238,11 +251,11 @@ public final class XmppWebSocketTransportModule
@Override
protected void notifyAboutNewOutgoingElements() {
Queue<TopLevelStreamElement> outgoingElementsQueue = connectionInternal.outgoingElementsQueue;
final Queue<TopLevelStreamElement> outgoingElementsQueue = connectionInternal.outgoingElementsQueue;
asyncButOrderedOutgoingElementsQueue.performAsyncButOrdered(outgoingElementsQueue, () -> {
// Once new outgoingElement is notified, send the top level stream element obtained by polling.
TopLevelStreamElement topLevelStreamElement = outgoingElementsQueue.poll();
websocket.send(topLevelStreamElement);
for (TopLevelStreamElement topLevelStreamElement; (topLevelStreamElement = outgoingElementsQueue.poll()) != null;) {
websocket.send(topLevelStreamElement);
}
});
}
@ -268,15 +281,11 @@ public final class XmppWebSocketTransportModule
@Override
public StreamOpenAndCloseFactory getStreamOpenAndCloseFactory() {
// TODO: Create extra class for this?
return new StreamOpenAndCloseFactory() {
@Override
public AbstractStreamOpen createStreamOpen(CharSequence to, CharSequence from, String id, String lang) {
try {
return new WebSocketOpenElement(JidCreate.domainBareFrom(to));
} catch (XmppStringprepException e) {
Logger.getAnonymousLogger().log(Level.WARNING, "Couldn't create OpenElement", e);
return null;
}
public AbstractStreamOpen createStreamOpen(DomainBareJid to, CharSequence from, String id, String lang) {
return new WebSocketOpenElement(to);
}
@Override
public AbstractStreamClose createStreamClose() {
@ -295,10 +304,6 @@ public final class XmppWebSocketTransportModule
assert result != null;
this.result = result;
}
public WebSocketRemoteConnectionEndpointLookup.Result getResult() {
return result;
}
}
/**
@ -308,10 +313,13 @@ public final class XmppWebSocketTransportModule
final class WebSocketEndpointsDiscoveryFailed implements LookupConnectionEndpointsFailed {
final List<RemoteConnectionEndpointLookupFailure> lookupFailures;
WebSocketEndpointsDiscoveryFailed(
WebSocketRemoteConnectionEndpointLookup.Result result) {
assert result != null;
lookupFailures = Collections.unmodifiableList(result.lookupFailures);
WebSocketEndpointsDiscoveryFailed(RemoteConnectionEndpointLookupFailure lookupFailure) {
this(Collections.singletonList(lookupFailure));
}
WebSocketEndpointsDiscoveryFailed(List<RemoteConnectionEndpointLookupFailure> lookupFailures) {
assert lookupFailures != null;
this.lookupFailures = Collections.unmodifiableList(lookupFailures);
}
@Override

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Aditya Borikar
* Copyright 2020 Aditya Borikar, 2020-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,6 +21,7 @@ import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.Set;
import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection;
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration;
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModule;
@ -29,6 +30,7 @@ import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionIn
import org.jivesoftware.smack.fsm.StateDescriptor;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.websocket.XmppWebSocketTransportModule.EstablishingWebSocketConnectionStateDescriptor;
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint;
/**
* The descriptor class for {@link XmppWebSocketTransportModule}.
@ -37,12 +39,43 @@ import org.jivesoftware.smack.websocket.XmppWebSocketTransportModule.Establishin
* use {@link ModularXmppClientToServerConnectionConfiguration.Builder#addModule(ModularXmppClientToServerConnectionModuleDescriptor)}.
*/
public final class XmppWebSocketTransportModuleDescriptor extends ModularXmppClientToServerConnectionModuleDescriptor {
private boolean performWebSocketEndpointDiscovery;
private URI uri;
private final boolean performWebSocketEndpointDiscovery;
private final boolean implicitWebSocketEndpoint;
private final URI uri;
private final WebSocketRemoteConnectionEndpoint wsRce;
public XmppWebSocketTransportModuleDescriptor(Builder builder) {
this.performWebSocketEndpointDiscovery = builder.performWebSocketEndpointDiscovery;
this.implicitWebSocketEndpoint = builder.implicitWebSocketEndpoint;
this.uri = builder.uri;
if (uri != null) {
wsRce = WebSocketRemoteConnectionEndpoint.from(uri);
} else {
wsRce = null;
}
}
@Override
@SuppressWarnings({"incomplete-switch", "MissingCasesInEnumSwitch"})
protected void validateConfiguration(ModularXmppClientToServerConnectionConfiguration configuration) {
if (wsRce == null) {
return;
}
SecurityMode securityMode = configuration.getSecurityMode();
switch (securityMode) {
case required:
if (!wsRce.isSecureEndpoint()) {
throw new IllegalArgumentException("The provided WebSocket endpoint " + wsRce + " is not a secure endpoint, but the connection configuration requires secure endpoints");
}
break;
case disabled:
if (wsRce.isSecureEndpoint()) {
throw new IllegalArgumentException("The provided WebSocket endpoint " + wsRce + " is a secure endpoint, but the connection configuration has security disabled");
}
break;
}
}
/**
@ -53,6 +86,10 @@ public final class XmppWebSocketTransportModuleDescriptor extends ModularXmppCli
return performWebSocketEndpointDiscovery;
}
public boolean isImplicitWebSocketEndpointEnabled() {
return implicitWebSocketEndpoint;
}
/**
* Returns explicitly configured websocket endpoint uri.
* @return uri
@ -61,6 +98,10 @@ public final class XmppWebSocketTransportModuleDescriptor extends ModularXmppCli
return uri;
}
WebSocketRemoteConnectionEndpoint getExplicitlyProvidedEndpoint() {
return wsRce;
}
@Override
protected Set<Class<? extends StateDescriptor>> getStateDescriptors() {
Set<Class<? extends StateDescriptor>> res = new HashSet<>();
@ -99,6 +140,7 @@ public final class XmppWebSocketTransportModuleDescriptor extends ModularXmppCli
*/
public static final class Builder extends ModularXmppClientToServerConnectionModuleDescriptor.Builder {
private boolean performWebSocketEndpointDiscovery = true;
private boolean implicitWebSocketEndpoint = true;
private URI uri;
private Builder(
@ -119,15 +161,20 @@ public final class XmppWebSocketTransportModuleDescriptor extends ModularXmppCli
public Builder explicitlySetWebSocketEndpoint(CharSequence endpoint) throws URISyntaxException {
URI endpointUri = new URI(endpoint.toString());
return explicitlySetWebSocketEndpointAndDiscovery(endpointUri, true);
return explicitlySetWebSocketEndpoint(endpointUri);
}
public Builder explicitlySetWebSocketEndpoint(CharSequence endpoint, boolean performWebSocketEndpointDiscovery)
public Builder explicitlySetWebSocketEndpointAndDiscovery(CharSequence endpoint, boolean performWebSocketEndpointDiscovery)
throws URISyntaxException {
URI endpointUri = new URI(endpoint.toString());
return explicitlySetWebSocketEndpointAndDiscovery(endpointUri, performWebSocketEndpointDiscovery);
}
public Builder disableImplicitWebsocketEndpoint() {
implicitWebSocketEndpoint = false;
return this;
}
@Override
public ModularXmppClientToServerConnectionModuleDescriptor build() {
return new XmppWebSocketTransportModuleDescriptor(this);

View file

@ -1,47 +0,0 @@
/**
*
* 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;
}
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Aditya Borikar.
* Copyright 2020 Aditya Borikar, 2020-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,40 +18,92 @@ package org.jivesoftware.smack.websocket.impl;
import javax.net.ssl.SSLSession;
import org.jivesoftware.smack.SmackFuture;
import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal;
import org.jivesoftware.smack.packet.TopLevelStreamElement;
import org.jivesoftware.smack.packet.XmlEnvironment;
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint;
public abstract class AbstractWebSocket {
protected enum WebSocketConnectionPhase {
openFrameSent,
exchangingTopLevelStreamElements
protected final ModularXmppClientToServerConnectionInternal connectionInternal;
protected final WebSocketRemoteConnectionEndpoint endpoint;
protected AbstractWebSocket(WebSocketRemoteConnectionEndpoint endpoint,
ModularXmppClientToServerConnectionInternal connectionInternal) {
this.endpoint = endpoint;
this.connectionInternal = connectionInternal;
}
protected static String getStreamFromOpenElement(String openElement) {
public final WebSocketRemoteConnectionEndpoint getEndpoint() {
return endpoint;
}
private String streamOpen;
private String streamClose;
protected final void onIncomingWebSocketElement(String element) {
// TODO: Once smack-websocket-java15 is there, we have to re-evaluate if the async operation here is still
// required, or if it should only be performed if OkHTTP is used.
if (isOpenElement(element)) {
// Transform the XMPP WebSocket <open/> element to a RFC 6120 <stream> open tag.
streamOpen = getStreamFromOpenElement(element);
streamClose = connectionInternal.onStreamOpen(streamOpen);
return;
}
if (isCloseElement(element)) {
connectionInternal.onStreamClosed();
return;
}
connectionInternal.withSmackDebugger(debugger -> debugger.onIncomingElementCompleted());
// TODO: Do we need to wrap the element again in the stream open to get the
// correct XML scoping (just like the modular TCP connection does)? It appears
// that this not really required, as onStreamOpen() will set the incomingStreamEnvironment, which is used for
// parsing.
String wrappedCompleteElement = streamOpen + element + streamClose;
connectionInternal.parseAndProcessElement(wrappedCompleteElement);
}
static String getStreamFromOpenElement(String openElement) {
String streamElement = openElement.replaceFirst("\\A<open ", "<stream ")
.replace("urn:ietf:params:xml:ns:xmpp-framing", "jabber:client")
.replaceFirst("/>\\s*\\z", ">");
return streamElement;
}
protected static boolean isOpenElement(String text) {
// TODO: Make this method less fragile, e.g. by parsing a little bit into the element to ensure that this is an
// <open/> element qualified by the correct namespace.
static boolean isOpenElement(String text) {
if (text.startsWith("<open ")) {
return true;
}
return false;
}
protected static boolean isCloseElement(String text) {
// TODO: Make this method less fragile, e.g. by parsing a little bit into the element to ensure that this is an
// <close/> element qualified by the correct namespace. The fragility comes due the fact that the element could,
// inter alia, be specified as
// <close:close xmlns:close="urn:ietf:params:xml:ns:xmpp-framing"/>
static boolean isCloseElement(String text) {
if (text.startsWith("<close xmlns='urn:ietf:params:xml:ns:xmpp-framing'/>")) {
return true;
}
return false;
}
public abstract void connect(WebSocketRemoteConnectionEndpoint endpoint) throws Throwable;
public abstract SmackFuture<AbstractWebSocket, Exception> getFuture();
public abstract void send(TopLevelStreamElement element);
public final void send(TopLevelStreamElement element) {
XmlEnvironment outgoingStreamXmlEnvironment = connectionInternal.getOutgoingStreamXmlEnvironment();
String elementString = element.toXML(outgoingStreamXmlEnvironment).toString();
send(elementString);
}
protected abstract void send(String element);
public abstract void disconnect(int code, String message);

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Florian Schmaus.
* Copyright 2020-2021 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -17,9 +17,11 @@
package org.jivesoftware.smack.websocket.impl;
import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal;
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint;
public interface WebSocketFactory {
AbstractWebSocket create(ModularXmppClientToServerConnectionInternal connectionInternal);
AbstractWebSocket create(WebSocketRemoteConnectionEndpoint endpoint,
ModularXmppClientToServerConnectionInternal connectionInternal);
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Florian Schmaus.
* Copyright 2020-2021 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,12 +20,14 @@ import java.util.Iterator;
import java.util.ServiceLoader;
import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal;
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint;
public final class WebSocketFactoryService {
private static final ServiceLoader<WebSocketFactory> SERVICE_LOADER = ServiceLoader.load(WebSocketFactory.class);
public static AbstractWebSocket createWebSocket(ModularXmppClientToServerConnectionInternal connectionInternal) {
public static AbstractWebSocket createWebSocket(WebSocketRemoteConnectionEndpoint endpoint,
ModularXmppClientToServerConnectionInternal connectionInternal) {
assert connectionInternal != null;
Iterator<WebSocketFactory> websocketFactories = SERVICE_LOADER.iterator();
@ -34,7 +36,7 @@ public final class WebSocketFactoryService {
}
WebSocketFactory websocketFactory = websocketFactories.next();
return websocketFactory.create(connectionInternal);
return websocketFactory.create(endpoint, connectionInternal);
}
}

View file

@ -0,0 +1,39 @@
/**
*
* Copyright 2020-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.websocket.rce;
import java.net.URI;
public class InsecureWebSocketRemoteConnectionEndpoint extends WebSocketRemoteConnectionEndpoint {
protected InsecureWebSocketRemoteConnectionEndpoint(URI uri) {
super(uri);
}
@Override
public final boolean isSecureEndpoint() {
return false;
}
public static final InsecureWebSocketRemoteConnectionEndpoint from(CharSequence cs) {
URI uri = URI.create(cs.toString());
if (!uri.getScheme().equals(INSECURE_WEB_SOCKET_SCHEME)) {
throw new IllegalArgumentException(uri + " is not a insecure WebSocket");
}
return new InsecureWebSocketRemoteConnectionEndpoint(uri);
}
}

View file

@ -0,0 +1,39 @@
/**
*
* Copyright 2020-2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.websocket.rce;
import java.net.URI;
public class SecureWebSocketRemoteConnectionEndpoint extends WebSocketRemoteConnectionEndpoint {
protected SecureWebSocketRemoteConnectionEndpoint(URI uri) {
super(uri);
}
@Override
public final boolean isSecureEndpoint() {
return true;
}
public static final SecureWebSocketRemoteConnectionEndpoint from(CharSequence cs) {
URI uri = URI.create(cs.toString());
if (!uri.getScheme().equals(SECURE_WEB_SOCKET_SCHEME)) {
throw new IllegalArgumentException(uri + " is not a secure WebSocket");
}
return new SecureWebSocketRemoteConnectionEndpoint(uri);
}
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Aditya Borikar
* Copyright 2020-2021 Aditya Borikar, Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,66 +20,100 @@ import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.datatypes.UInt16;
import org.jivesoftware.smack.util.rce.RemoteConnectionEndpoint;
public final class WebSocketRemoteConnectionEndpoint implements RemoteConnectionEndpoint {
public abstract class WebSocketRemoteConnectionEndpoint implements RemoteConnectionEndpoint {
public static final String INSECURE_WEB_SOCKET_SCHEME = "ws";
public static final String SECURE_WEB_SOCKET_SCHEME = INSECURE_WEB_SOCKET_SCHEME + "s";
private static final Logger LOGGER = Logger.getAnonymousLogger();
private final URI uri;
private final UInt16 port;
public WebSocketRemoteConnectionEndpoint(String uri) throws URISyntaxException {
this(new URI(uri));
}
public WebSocketRemoteConnectionEndpoint(URI uri) {
protected WebSocketRemoteConnectionEndpoint(URI uri) {
this.uri = uri;
String scheme = uri.getScheme();
if (!(scheme.equals("ws") || scheme.equals("wss"))) {
throw new IllegalArgumentException("Only allowed protocols are ws and wss");
int portInt = uri.getPort();
if (portInt >= 0) {
port = UInt16.from(portInt);
} else {
port = null;
}
}
public URI getWebSocketEndpoint() {
public final URI getUri() {
return uri;
}
public boolean isSecureEndpoint() {
if (uri.getScheme().equals("wss")) {
return true;
}
return false;
}
@Override
public CharSequence getHost() {
public final String getHost() {
return uri.getHost();
}
@Override
public UInt16 getPort() {
return UInt16.from(uri.getPort());
return port;
}
public abstract boolean isSecureEndpoint();
private List<? extends InetAddress> inetAddresses;
private void resolveInetAddressesIfRequired() {
if (inetAddresses != null) {
return;
}
String host = getHost();
InetAddress[] addresses;
try {
addresses = InetAddress.getAllByName(host);
} catch (UnknownHostException e) {
LOGGER.log(Level.WARNING, "Could not resolve IP addresses of " + host, e);
return;
}
inetAddresses = Arrays.asList(addresses);
}
@Override
public Collection<? extends InetAddress> getInetAddresses() {
try {
InetAddress address = InetAddress.getByName(getHost().toString());
return Collections.singletonList(address);
} catch (UnknownHostException e) {
LOGGER.log(Level.INFO, "Unknown Host Exception ", e);
}
return null;
resolveInetAddressesIfRequired();
return inetAddresses;
}
@Override
public String getDescription() {
return null;
}
@Override
public String toString() {
return uri.toString();
}
public static WebSocketRemoteConnectionEndpoint from(CharSequence uriCharSequence) throws URISyntaxException {
String uriString = uriCharSequence.toString();
URI uri = URI.create(uriString);
return from(uri);
}
public static WebSocketRemoteConnectionEndpoint from(URI uri) {
String scheme = uri.getScheme();
switch (scheme) {
case INSECURE_WEB_SOCKET_SCHEME:
return new InsecureWebSocketRemoteConnectionEndpoint(uri);
case SECURE_WEB_SOCKET_SCHEME:
return new SecureWebSocketRemoteConnectionEndpoint(uri);
default:
throw new IllegalArgumentException("Only allowed protocols are " + INSECURE_WEB_SOCKET_SCHEME + " and " + SECURE_WEB_SOCKET_SCHEME);
}
}
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Aditya Borikar
* Copyright 2020 Aditya Borikar, Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,10 +20,9 @@ import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Collections;
import java.util.List;
import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.altconnections.HttpLookupMethod;
import org.jivesoftware.smack.altconnections.HttpLookupMethod.LinkRelation;
import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure;
@ -33,9 +32,8 @@ import org.jxmpp.jid.DomainBareJid;
public final class WebSocketRemoteConnectionEndpointLookup {
public static Result lookup(DomainBareJid domainBareJid, SecurityMode securityMode) {
public static Result lookup(DomainBareJid domainBareJid) {
List<RemoteConnectionEndpointLookupFailure> lookupFailures = new ArrayList<>(1);
List<WebSocketRemoteConnectionEndpoint> discoveredRemoteConnectionEndpoints = new ArrayList<>();
List<URI> rcUriList = null;
try {
@ -45,67 +43,69 @@ public final class WebSocketRemoteConnectionEndpointLookup {
} catch (IOException | XmlPullParserException | URISyntaxException e) {
lookupFailures.add(new RemoteConnectionEndpointLookupFailure.HttpLookupFailure(
domainBareJid, e));
return new Result(discoveredRemoteConnectionEndpoints, lookupFailures);
return new Result(lookupFailures);
}
if (rcUriList.isEmpty()) {
throw new IllegalStateException("No endpoints were found inside host-meta");
List<SecureWebSocketRemoteConnectionEndpoint> discoveredSecureEndpoints = new ArrayList<>(rcUriList.size());
List<InsecureWebSocketRemoteConnectionEndpoint> discoveredInsecureEndpoints = new ArrayList<>(rcUriList.size());
for (URI webSocketUri : rcUriList) {
WebSocketRemoteConnectionEndpoint wsRce = WebSocketRemoteConnectionEndpoint.from(webSocketUri);
if (wsRce instanceof SecureWebSocketRemoteConnectionEndpoint) {
SecureWebSocketRemoteConnectionEndpoint secureWsRce = (SecureWebSocketRemoteConnectionEndpoint) wsRce;
discoveredSecureEndpoints.add(secureWsRce);
} else if (wsRce instanceof InsecureWebSocketRemoteConnectionEndpoint) {
InsecureWebSocketRemoteConnectionEndpoint insecureWsRce = (InsecureWebSocketRemoteConnectionEndpoint) wsRce;
discoveredInsecureEndpoints.add(insecureWsRce);
} else {
// WebSocketRemoteConnectionEndpoint.from() must return an instance which type is one of the above.
throw new AssertionError();
}
}
// Convert rcUriList to List<WebSocketRemoteConnectionEndpoint>
Iterator<URI> iterator = rcUriList.iterator();
List<WebSocketRemoteConnectionEndpoint> 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<WebSocketRemoteConnectionEndpoint> secureEndpointsForSecurityModeIfPossible = new ArrayList<>();
List<WebSocketRemoteConnectionEndpoint> insecureEndpointsForSecurityModeIfPossible = new ArrayList<>();
for (WebSocketRemoteConnectionEndpoint uri : rceList) {
if (uri.isSecureEndpoint()) {
secureEndpointsForSecurityModeIfPossible.add(uri);
} else {
insecureEndpointsForSecurityModeIfPossible.add(uri);
}
}
discoveredRemoteConnectionEndpoints = secureEndpointsForSecurityModeIfPossible;
discoveredRemoteConnectionEndpoints.addAll(insecureEndpointsForSecurityModeIfPossible);
break;
case required:
case disabled:
/**
* If, SecurityMode equals to required, accept wss endpoints (secure endpoints) only or,
* if SecurityMode equals to disabled, accept ws endpoints (unsecure endpoints) only.
*/
for (WebSocketRemoteConnectionEndpoint uri : rceList) {
if ((securityMode.equals(SecurityMode.disabled) && !uri.isSecureEndpoint())
|| (securityMode.equals(SecurityMode.required) && uri.isSecureEndpoint())) {
discoveredRemoteConnectionEndpoints.add(uri);
}
}
break;
default:
}
return new Result(discoveredRemoteConnectionEndpoints, lookupFailures);
return new Result(discoveredSecureEndpoints, discoveredInsecureEndpoints, lookupFailures);
}
public static final class Result {
public final List<WebSocketRemoteConnectionEndpoint> discoveredRemoteConnectionEndpoints;
public final List<SecureWebSocketRemoteConnectionEndpoint> discoveredSecureEndpoints;
public final List<InsecureWebSocketRemoteConnectionEndpoint> discoveredInsecureEndpoints;
public final List<RemoteConnectionEndpointLookupFailure> lookupFailures;
public Result(List<WebSocketRemoteConnectionEndpoint> discoveredRemoteConnectionEndpoints,
public Result() {
this(Collections.emptyList());
}
public Result(List<RemoteConnectionEndpointLookupFailure> lookupFailures) {
// The list of endpoints needs to be mutable, because maybe a user supplied endpoint will be added to it.
// Hence we do not use Collections.emptyList() as argument for the discovered endpoints.
this(new ArrayList<>(1), new ArrayList<>(1), lookupFailures);
}
public Result(List<SecureWebSocketRemoteConnectionEndpoint> discoveredSecureEndpoints,
List<InsecureWebSocketRemoteConnectionEndpoint> discoveredInsecureEndpoints,
List<RemoteConnectionEndpointLookupFailure> lookupFailures) {
this.discoveredRemoteConnectionEndpoints = discoveredRemoteConnectionEndpoints;
this.discoveredSecureEndpoints = discoveredSecureEndpoints;
this.discoveredInsecureEndpoints = discoveredInsecureEndpoints;
this.lookupFailures = lookupFailures;
}
public List<WebSocketRemoteConnectionEndpoint> getDiscoveredRemoteConnectionEndpoints() {
return discoveredRemoteConnectionEndpoints;
public boolean isEmpty() {
return discoveredSecureEndpoints.isEmpty() && discoveredInsecureEndpoints.isEmpty();
}
public int discoveredEndpointCount() {
return discoveredSecureEndpoints.size() + discoveredInsecureEndpoints.size();
}
// TODO: Remove the following methods since the fields are already public? Or make the fields private and use
// the methods? I tend to remove the methods, as their method name is pretty long. But OTOH the fields reference
// mutable datastructes, which is uncommon to be public.
public List<SecureWebSocketRemoteConnectionEndpoint> getDiscoveredSecureRemoteConnectionEndpoints() {
return discoveredSecureEndpoints;
}
public List<InsecureWebSocketRemoteConnectionEndpoint> getDiscoveredInsecureRemoteConnectionEndpoints() {
return discoveredInsecureEndpoints;
}
public List<RemoteConnectionEndpointLookupFailure> getLookupFailures() {

View file

@ -17,24 +17,15 @@
package org.jivesoftware.smack.websocket;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection;
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration;
import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal;
import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure;
import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure.HttpLookupFailure;
import org.jivesoftware.smack.websocket.XmppWebSocketTransportModule.XmppWebSocketTransport.DiscoveredWebSocketEndpoints;
import org.jivesoftware.smack.websocket.XmppWebSocketTransportModule.XmppWebSocketTransport.WebSocketEndpointsDiscoveryFailed;
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint;
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpointLookup.Result;
import org.junit.jupiter.api.Test;
import org.jxmpp.stringprep.XmppStringprepException;
@ -64,42 +55,6 @@ public class XmppWebSocketTransportModuleTest {
assertNotNull(websocketTransportModuleDescriptor);
}
@Test
public void websocketEndpointDiscoveryTest() throws URISyntaxException {
XmppWebSocketTransportModuleDescriptor websocketTransportModuleDescriptor = getWebSocketDescriptor();
ModularXmppClientToServerConnectionInternal connectionInternal = mock(ModularXmppClientToServerConnectionInternal.class);
XmppWebSocketTransportModule transportModule
= new XmppWebSocketTransportModule(websocketTransportModuleDescriptor, connectionInternal);
XmppWebSocketTransportModule.XmppWebSocketTransport transport = transportModule.getTransport();
assertThrows(AssertionError.class, () -> transport.new DiscoveredWebSocketEndpoints(null));
assertThrows(AssertionError.class, () -> transport.new WebSocketEndpointsDiscoveryFailed(null));
WebSocketRemoteConnectionEndpoint endpoint = new WebSocketRemoteConnectionEndpoint("wss://localhost.org:7443/ws/");
List<WebSocketRemoteConnectionEndpoint> discoveredRemoteConnectionEndpoints = new ArrayList<>();
discoveredRemoteConnectionEndpoints.add(endpoint);
HttpLookupFailure httpLookupFailure = new RemoteConnectionEndpointLookupFailure.HttpLookupFailure(null, null);
List<RemoteConnectionEndpointLookupFailure> 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();

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Aditya Borikar
* Copyright 2020 Aditya Borikar, 2021 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -26,20 +26,22 @@ import org.jivesoftware.smack.datatypes.UInt16;
import org.junit.jupiter.api.Test;
public class WebSocketRemoteConnectionEndpointTest {
@Test
public void endpointTest() throws URISyntaxException {
String endpointString = "ws://fooDomain.org:7070/ws/";
WebSocketRemoteConnectionEndpoint endpoint = new WebSocketRemoteConnectionEndpoint(endpointString);
WebSocketRemoteConnectionEndpoint endpoint = WebSocketRemoteConnectionEndpoint.from(endpointString);
assertEquals("fooDomain.org", endpoint.getHost());
assertEquals(UInt16.from(7070), endpoint.getPort());
assertEquals(endpointString, endpoint.getWebSocketEndpoint().toString());
assertEquals(endpointString, endpoint.getUri().toString());
}
@Test
public void faultyEndpointTest() {
String faultyProtocolString = "wst://fooDomain.org:7070/ws/";
assertThrows(IllegalArgumentException.class, () -> {
new WebSocketRemoteConnectionEndpoint(faultyProtocolString);
WebSocketRemoteConnectionEndpoint.from(faultyProtocolString);
});
}
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2020 Florian Schmaus.
* Copyright 2020-2021 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,16 +20,21 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import java.net.URISyntaxException;
import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal;
import org.jivesoftware.smack.websocket.impl.AbstractWebSocket;
import org.jivesoftware.smack.websocket.impl.WebSocketFactoryService;
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint;
public class WebSocketFactoryServiceTestUtil {
public static void createWebSocketTest(Class<? extends AbstractWebSocket> expected) {
public static void createWebSocketTest(Class<? extends AbstractWebSocket> expected) throws URISyntaxException {
WebSocketRemoteConnectionEndpoint endpoint = WebSocketRemoteConnectionEndpoint.from("wss://example.org");
ModularXmppClientToServerConnectionInternal connectionInternal = mock(ModularXmppClientToServerConnectionInternal.class);
AbstractWebSocket websocket = WebSocketFactoryService.createWebSocket(connectionInternal);
AbstractWebSocket websocket = WebSocketFactoryService.createWebSocket(endpoint, connectionInternal);
assertEquals(expected, websocket.getClass());
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2019 Florian Schmaus.
* Copyright 2019-2021 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -75,6 +75,10 @@ public interface XmlPullParser {
String getNamespace(String prefix);
default String getDefaultNamespace() {
return getNamespace(null);
}
int getDepth();
String getPositionDescription();