From cc636fff21cace34759a350dfe267fe2d1ec5a7d Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sat, 4 Apr 2020 13:03:31 +0200 Subject: [PATCH] Introduce Smack's Modular Connection Architecture This is a complete redesign of what was previously XmppNioTcpConnection. The new architecture allows to extend an XMPP client to server (c2s) connection with new transport bindings and other extensions. --- build.gradle | 60 +- documentation/connection-modules.md | 35 + documentation/developer/integrationtest.md | 44 +- documentation/index.md | 1 + settings.gradle | 1 + .../smack/bosh/XMPPBOSHConnection.java | 5 +- smack-core/build.gradle | 2 + .../smack/AbstractXMPPConnection.java | 54 +- .../smack/AbstractXmppNioConnection.java | 50 - .../smack/ConnectionConfiguration.java | 10 +- .../smack/SmackConfiguration.java | 19 +- .../jivesoftware/smack/SmackException.java | 128 +- .../org/jivesoftware/smack/SmackFuture.java | 28 +- .../smack/SmackInitialization.java | 7 + .../org/jivesoftware/smack/SmackReactor.java | 6 +- .../smack/XmppInputOutputFilter.java | 8 +- .../jivesoftware/smack/bind2/Bind2Module.java | 77 + .../smack/bind2/Bind2ModuleDescriptor.java | 53 + .../smack/bind2/package-info.java | 21 + .../ModularXmppClientToServerConnection.java | 1114 ++++++++++ ...ClientToServerConnectionConfiguration.java | 167 ++ ...larXmppClientToServerConnectionModule.java | 40 + ...entToServerConnectionModuleDescriptor.java | 48 + .../c2s/XmppClientToServerTransport.java | 76 + ...rXmppClientToServerConnectionInternal.java | 125 ++ .../c2s/internal/WalkStateGraphContext.java | 179 ++ .../smack/c2s/internal/package-info.java | 21 + .../jivesoftware/smack/c2s/package-info.java | 21 + .../smack/compression/CompressionModule.java | 132 ++ .../CompressionModuleDescriptor.java | 54 + .../zlib/ZlibXmppCompressionFactory.java | 5 + .../AbstractXmppStateMachineConnection.java | 806 ------- .../smack/fsm/ConnectionStateEvent.java | 60 +- .../fsm/ConnectionStateMachineListener.java | 4 +- .../jivesoftware/smack/fsm/LoginContext.java | 10 +- .../org/jivesoftware/smack/fsm/NoOpState.java | 43 + .../org/jivesoftware/smack/fsm/State.java | 81 + .../smack/fsm/StateDescriptor.java | 61 +- .../smack/fsm/StateDescriptorGraph.java | 26 +- .../smack/fsm/StateMachineException.java | 41 +- .../smack/fsm/StateTransitionResult.java | 87 + .../isr/InstantStreamResumptionModule.java | 85 + ...stantStreamResumptionModuleDescriptor.java | 54 + .../jivesoftware/smack/isr/package-info.java | 23 + .../util/ArrayBlockingQueueWithShutdown.java | 7 +- .../smack/util/CollectionUtil.java | 11 +- .../org/jivesoftware/smack/util/DNSUtil.java | 204 +- .../org/jivesoftware/smack/util/Function.java | 5 +- .../org/jivesoftware/smack/util/MultiMap.java | 13 +- .../jivesoftware/smack/util/StringUtils.java | 34 +- .../smack/util/dns/DNSResolver.java | 33 +- .../smack/util/dns/HostAddress.java | 182 -- .../smack/util/dns/SRVRecord.java | 91 - .../util/rce/RemoteConnectionEndpoint.java | 59 + ...RemoteConnectionEndpointLookupFailure.java | 70 + .../util/rce/RemoteConnectionException.java | 66 + ...SingleAddressRemoteConnectionEndpoint.java | 31 + .../smack/util/rce/package-info.java | 21 + .../smack/SmackExceptionTest.java | 59 - .../jivesoftware/smack/util/DnsUtilTest.java | 12 +- smack-integration-test/build.gradle | 8 +- .../smack/inttest/AbstractSmackIntTest.java | 4 +- .../inttest/AbstractSmackIntegrationTest.java | 4 +- .../AbstractSmackLowLevelIntegrationTest.java | 6 +- ...tSmackSpecificLowLevelIntegrationTest.java | 9 +- .../smack/inttest/Configuration.java | 167 +- .../SmackIntegrationTestEnvironment.java | 12 +- .../SmackIntegrationTestFramework.java | 367 ++-- .../inttest/XmppConnectionDescriptor.java | 76 +- .../smack/inttest/XmppConnectionManager.java | 137 +- .../java/org/jivesoftware/smack/ChatTest.java | 4 +- .../smack/LoginIntegrationTest.java | 4 +- .../smack/StreamManagementTest.java | 4 +- .../WaitForClosingStreamElementTest.java | 4 +- .../smack/XmppConnectionIntegrationTest.java | 14 +- .../chat2/AbstractChatIntegrationTest.java | 4 +- ...ncomingMessageListenerIntegrationTest.java | 2 +- ...utgoingMessageListenerIntegrationTest.java | 2 +- .../roster/LowLevelRosterIntegrationTest.java | 4 +- .../smack/roster/RosterIntegrationTest.java | 4 +- ...oTcpConnectionLowLevelIntegrationTest.java | 11 +- .../smackx/caps/EntityCapsTest.java | 4 +- .../chatstate/ChatStateIntegrationTest.java | 2 +- .../FileTransferIntegrationTest.java | 4 +- .../HttpFileUploadIntegrationTest.java | 4 +- .../smackx/iot/IoTControlIntegrationTest.java | 4 +- .../smackx/iot/IoTDataIntegrationTest.java | 4 +- .../iot/IoTDiscoveryIntegrationTest.java | 4 +- .../iqversion/VersionIntegrationTest.java | 4 +- .../smackx/mam/MamIntegrationTest.java | 4 +- .../smackx/mood/MoodIntegrationTest.java | 4 +- .../muc/MultiUserChatIntegrationTest.java | 4 +- .../MultiUserChatLowLevelIntegrationTest.java | 4 +- .../omemo/AbstractOmemoIntegrationTest.java | 4 +- .../AbstractTwoUsersOmemoIntegrationTest.java | 2 +- .../MessageEncryptionIntegrationTest.java | 2 +- .../smackx/omemo/OmemoMamDecryptionTest.java | 2 +- .../omemo/ReadOnlyDeviceIntegrationTest.java | 2 +- .../SessionRenegotiationIntegrationTest.java | 2 +- .../ox/AbstractOpenPgpIntegrationTest.java | 2 +- .../ox/OXSecretKeyBackupIntegrationTest.java | 2 +- .../OXInstantMessagingIntegrationTest.java | 2 +- .../smackx/ping/PingIntegrationTest.java | 4 +- .../smackx/pubsub/PubSubIntegrationTest.java | 4 +- .../usertune/UserTuneIntegrationTest.java | 2 +- .../jivesoftware/smackx/xdata/FormTest.java | 4 +- .../DummySmackIntegrationTestFramework.java | 21 +- .../SmackIntegrationTestUnitTestUtil.java | 6 +- ...egrationTestXmppConnectionManagerTest.java | 21 +- ...SmackIntegrationTestFrameworkUnitTest.java | 6 +- smack-java8-full/build.gradle | 48 + .../smack/full/doc-files/.gitignore | 2 + ...dularXmppClientToServerConnectionTool.java | 71 + .../jivesoftware/smack/full/package-info.java | 28 + ...lientToServerConnectionStateGraphTest.java | 83 + .../src/test/resources/state-graph.dot | 36 + smack-repl/build.gradle | 21 +- .../igniterealtime/smack/smackrepl/Nio.java | 40 +- .../smack/smackrepl/StateGraph.java | 48 - .../util/dns/dnsjava/DNSJavaResolver.java | 31 +- .../smack/util/dns/javax/JavaxResolver.java | 30 +- .../util/dns/minidns/MiniDnsResolver.java | 64 +- smack-tcp/Makefile | 21 - .../smack/tcp/doc-files/.gitignore | 2 - .../smack/sm/StreamManagementModule.java | 141 ++ .../sm/StreamManagementModuleDescriptor.java | 58 + .../smack/tcp/ConnectionAttemptState.java | 175 ++ .../smack/tcp/TCPInitializer.java | 9 +- .../jivesoftware/smack/tcp/TcpHostEvent.java | 79 + .../smack/tcp/XMPPTCPConnection.java | 44 +- .../smack/tcp/XmppNioTcpConnection.java | 1883 ----------------- .../smack/tcp/XmppTcpTransportModule.java | 1431 +++++++++++++ .../tcp/XmppTcpTransportModuleDescriptor.java | 91 + .../rce/IpTcpRemoteConnectionEndpoint.java | 78 + .../rce/RemoteXmppTcpConnectionEndpoints.java | 232 ++ .../Rfc6120TcpRemoteConnectionEndpoint.java | 23 + .../tcp/rce/SrvRemoteConnectionEndpoint.java | 57 + .../rce/SrvXmppRemoteConnectionEndpoint.java | 35 + .../rce/SrvXmppsRemoteConnectionEndpoint.java | 34 + .../smack/tcp/rce/package-info.java | 21 + .../smack/tcp/XmppNioTcpConnectionTest.java | 33 - .../RemoteXmppTcpConnectionEndpointsTest.java | 87 + 142 files changed, 6819 insertions(+), 4068 deletions(-) create mode 100644 documentation/connection-modules.md delete mode 100644 smack-core/src/main/java/org/jivesoftware/smack/AbstractXmppNioConnection.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/bind2/Bind2Module.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/bind2/Bind2ModuleDescriptor.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/bind2/package-info.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnection.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionConfiguration.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionModule.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionModuleDescriptor.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/c2s/XmppClientToServerTransport.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/ModularXmppClientToServerConnectionInternal.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/WalkStateGraphContext.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/package-info.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/c2s/package-info.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/compression/CompressionModule.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/compression/CompressionModuleDescriptor.java delete mode 100644 smack-core/src/main/java/org/jivesoftware/smack/fsm/AbstractXmppStateMachineConnection.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/fsm/NoOpState.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/fsm/State.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/fsm/StateTransitionResult.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/isr/InstantStreamResumptionModule.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/isr/InstantStreamResumptionModuleDescriptor.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/isr/package-info.java delete mode 100644 smack-core/src/main/java/org/jivesoftware/smack/util/dns/HostAddress.java delete mode 100644 smack-core/src/main/java/org/jivesoftware/smack/util/dns/SRVRecord.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/util/rce/RemoteConnectionEndpoint.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/util/rce/RemoteConnectionEndpointLookupFailure.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/util/rce/RemoteConnectionException.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/util/rce/SingleAddressRemoteConnectionEndpoint.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/util/rce/package-info.java delete mode 100644 smack-core/src/test/java/org/jivesoftware/smack/SmackExceptionTest.java create mode 100644 smack-java8-full/build.gradle create mode 100644 smack-java8-full/src/javadoc/org/jivesoftware/smack/full/doc-files/.gitignore create mode 100644 smack-java8-full/src/main/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionTool.java create mode 100644 smack-java8-full/src/main/java/org/jivesoftware/smack/full/package-info.java create mode 100644 smack-java8-full/src/test/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionStateGraphTest.java create mode 100644 smack-java8-full/src/test/resources/state-graph.dot delete mode 100644 smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/StateGraph.java delete mode 100644 smack-tcp/Makefile delete mode 100644 smack-tcp/src/javadoc/org/jivesoftware/smack/tcp/doc-files/.gitignore create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementModule.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementModuleDescriptor.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/ConnectionAttemptState.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/TcpHostEvent.java delete mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnection.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModuleDescriptor.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/IpTcpRemoteConnectionEndpoint.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/RemoteXmppTcpConnectionEndpoints.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/Rfc6120TcpRemoteConnectionEndpoint.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/SrvRemoteConnectionEndpoint.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/SrvXmppRemoteConnectionEndpoint.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/SrvXmppsRemoteConnectionEndpoint.java create mode 100644 smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/package-info.java delete mode 100644 smack-tcp/src/test/java/org/jivesoftware/smack/tcp/XmppNioTcpConnectionTest.java create mode 100644 smack-tcp/src/test/java/org/jivesoftware/smack/tcp/rce/RemoteXmppTcpConnectionEndpointsTest.java diff --git a/build.gradle b/build.gradle index 2e9b23ecb..117cf8a7d 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,8 @@ buildscript { plugins { id 'ru.vyarus.animalsniffer' version '1.5.0' id 'net.ltgt.errorprone' version '1.1.1' + // Use e.g. "gradle taskTree" to show its dependency tree. + id 'com.dorongold.task-tree' version '1.5' } apply plugin: 'org.kordamp.gradle.markdown' @@ -288,14 +290,7 @@ configure (junit4Projects) { } } -task copyAllJavadocDocFiles(type: Copy) { - from javadocAllProjects.collect { project -> - "${project.projectDir}/src/javadoc" } - into javadocAllDir - include '**/doc-files/*.*' -} - -task javadocAll(type: Javadoc, dependsOn: copyAllJavadocDocFiles) { +task javadocAll(type: Javadoc) { source javadocAllProjects.collect {project -> project.sourceSets.main.allJava.findAll { // Filter out symbolic links to avoid @@ -325,6 +320,25 @@ task javadocAll(type: Javadoc, dependsOn: copyAllJavadocDocFiles) { ] as String[] overview = "$projectDir/resources/javadoc-overview.html" } + + // Finally copy the javadoc doc-files from the subprojects, which + // are potentially generated, to the javadocAll directory. Note + // that we use a copy *method* and not a *task* because the inputs + // of copy tasks is determined within the configuration phase. And + // since some of the inputs are generated, they will not get + // picked up if we used a copy method. See also + // https://stackoverflow.com/a/40518516/194894 + doLast { + copy { + javadocAllProjects.each { + from ("${it.projectDir}/src/javadoc") { + include '**/doc-files/*.*' + } + } + + into javadocAllDir + } + } } import org.apache.tools.ant.filters.ReplaceTokens @@ -494,31 +508,25 @@ subprojects { } // Work around https://github.com/gradle/gradle/issues/4046 - javadoc.dependsOn('copyJavadocDocFiles') task copyJavadocDocFiles(type: Copy) { from('src/javadoc') into 'build/docs/javadoc' include '**/doc-files/*.*' } + javadoc.dependsOn copyJavadocDocFiles - // If this subproject has a Makefile then make copyJavadocDocFiles - // and the root project's javadocAll task dependend on - // generateFiles. - if (file("$projectDir/Makefile").exists()) { - copyJavadocDocFiles.dependsOn('generateFiles') - rootProject.copyAllJavadocDocFiles.dependsOn("${project.name}:generateFiles") - task generateFiles(type: Exec) { - workingDir projectDir - commandLine 'make' - } + // Make sure root projects 'javadocAll' depends on the + // subproject's javadoc, to ensure that all all doc-files/ are + // generated and up-to-date. Obviously this means that the + // javadocAll task will also create the individual javadoc's of the + // subprojects. + javadocAll.dependsOn javadoc +} - clean.dependsOn('cleanGeneratedFiles') - rootProject.clean.dependsOn("${project.name}:cleanGeneratedFiles") - task cleanGeneratedFiles(type: Exec) { - workingDir projectDir - commandLine 'make', 'clean' - } - } +// The smack-java8-full project generates the dot and png files of the +// current state graph. Ensure they are generated before copied. +configure (project(':smack-java8-full')) { + copyJavadocDocFiles.dependsOn convertModularXmppClientToServerConnectionStateGraphDotToPng } configure (androidProjects + androidBootClasspathProjects) { diff --git a/documentation/connection-modules.md b/documentation/connection-modules.md new file mode 100644 index 000000000..50110e6f7 --- /dev/null +++ b/documentation/connection-modules.md @@ -0,0 +1,35 @@ +Smack's Modular Connection Architecture +====================================== + +[Back](index.md) + +**Note: Everything related to the modular connection architecture is currently considered experimental and should not be used in production. Use the mature `XMPPTCPConnection` if you do not feel adventurous. + +Smack's modular connection architecture allows to extend a XMPP c2s (client-to-server) connection with additional functionalty by adding modules. +Those modules extend the Finite State Machine (FSM) within the `ModularXmppClientToServerConnection` with new states. + +Connection modules can either be +- Transports +- Extensions + +Transports bind the XMPP XML stream to an underlying transport like TCP, WebSockets, BOSH, and allow for the different particularities of transports like DirectTLS ([XEP-0368](https://xmpp.org/extensions/xep-0368.html)). +This eventually means that a single transport module can implement multiple transport mechanisms. +For example the TCP transport module implements the RFC6120 TCP and the XEP-0368 direct TLS TCP transport bindings. + +Extensions allow for a richer functionality of the connection. Those include +- Compression + - zlib ([XEP-0138](https://xmpp.org/extensions/xep-0138.html)) + - [Efficient XML Interchange (EXI)](https://www.w3.org/TR/exi/) +- Instant Stream Resumption ([XEP-0397](https://xmpp.org/extensions/xep-0397.html) +- Bind2 +- Stream Management + +Note that not all extensions work with every transport. +For example compression only works with TCP-based transport bindings. + + +Connection modules are plugged into the the modular connection via their constructor. and they usually declare backwards edges to some common, generic connection state of the FSM. + +Modules and states always have an accompanying *descriptor* type. +`ModuleDescriptor` and `StateDescriptor` exist without an connection instance. +They describe the module and state metadata, while their modules are states are instanciated once a modular connection is instanciated. diff --git a/documentation/developer/integrationtest.md b/documentation/developer/integrationtest.md index 2bf2143db..e9caed4e3 100644 --- a/documentation/developer/integrationtest.md +++ b/documentation/developer/integrationtest.md @@ -58,24 +58,28 @@ debugger=console ### Framework properties -| Name | | -|----------------------|-------------------------------------------| -| service | XMPP service to run the tests on | -| serviceTlsPin | TLS Pin (used by [java-pinning](https://github.com/Flowdalic/java-pinning)) | -| securityMode | Either 'required' or 'disabled' | -| replyTimeout | In milliseconds | -| adminAccountUsername | Username of the XEP-0133 Admin account | -| adminAccountPassword | Password of the XEP-0133 Admin account | -| accountOneUsername | Username of the first XMPP account | -| accountOnePassword | Password of the first XMPP account | -| accountTwoUsername | Username of the second XMPP account | -| accountTwoPassword | Password of the second XMPP account | -| accountThreeUsername | Username of the third XMPP account | -| accountThreePassword | Password of the third XMPP account | -| debugger | 'console' for console debugger, 'enhanced' for the enhanced debugger | -| enabledTests | List of enabled tests | -| disabledTests | List of disabled tests | -| testPackages | List of packages with tests | +| Name | Description | +|----------------------|-----------------------------------------------------------------------------| +| service | XMPP service to run the tests on | +| serviceTlsPin | TLS Pin (used by [java-pinning](https://github.com/Flowdalic/java-pinning)) | +| securityMode | Either 'required' or 'disabled' | +| replyTimeout | In milliseconds | +| adminAccountUsername | Username of the XEP-0133 Admin account | +| adminAccountPassword | Password of the XEP-0133 Admin account | +| accountOneUsername | Username of the first XMPP account | +| accountOnePassword | Password of the first XMPP account | +| accountTwoUsername | Username of the second XMPP account | +| accountTwoPassword | Password of the second XMPP account | +| accountThreeUsername | Username of the third XMPP account | +| accountThreePassword | Password of the third XMPP account | +| debugger | 'console' for console debugger, 'enhanced' for the enhanced debugger | +| enabledTests | List of enabled tests | +| disabledTests | List of disabled tests | +| defaultConnection | Nickname of the default connection | +| enabledConnections | List of enabled connection's nicknames | +| disabledConnections | List of disabled connection's nicknames | +| testPackages | List of packages with tests | +| verbose | If `true` set output to verbose | ### Where to place the properties file @@ -99,6 +103,10 @@ The base class that integration tests need to subclass. Allows low level integration test, i.e. ever test method will have its on exclusive XMPPTCPConnection instances. +### `AbstractSmackSpecificLowLevelIntegrationTest` + +Operates, like `AbstractSmackLowLevelIntegrationTest` on its own `XMPPConnection` instances, but is limited to a particular type of `XMPPConnection`. + ### `IntegrationTestEnvironment` The environment, e.g. the `XMPPConnections` provided to the integration tests by the framework. Note that for convenience `AbstractSmackIntegrationTest` contains some of those as protected members. diff --git a/documentation/index.md b/documentation/index.md index 034f18070..5b9d27e9f 100644 --- a/documentation/index.md +++ b/documentation/index.md @@ -13,3 +13,4 @@ * [Debugging with Smack](debugging.md) * [Smack Extensions Manual](extensions/index.md) + * [Smack's Modular Connection Architecture](connection-modules.md) diff --git a/settings.gradle b/settings.gradle index d3b53d80e..0f84a8753 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,6 +23,7 @@ include 'smack-core', 'smack-android', 'smack-android-extensions', 'smack-java7', + 'smack-java8-full', 'smack-integration-test', 'smack-omemo', 'smack-omemo-signal', diff --git a/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java b/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java index 824dedc44..c3d7109f8 100644 --- a/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java +++ b/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java @@ -27,7 +27,7 @@ import java.util.logging.Logger; import org.jivesoftware.smack.AbstractXMPPConnection; import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.SmackException.ConnectionException; +import org.jivesoftware.smack.SmackException.GenericConnectionException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.SmackWrappedException; import org.jivesoftware.smack.XMPPConnection; @@ -136,6 +136,7 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection { this.config = config; } + @SuppressWarnings("deprecation") @Override protected void connectInternal() throws SmackException, InterruptedException { done = false; @@ -177,7 +178,7 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection { .setAttribute(BodyQName.createWithPrefix(XMPP_BOSH_NS, "version", "xmpp"), "1.0") .build()); } catch (Exception e) { - throw new ConnectionException(e); + throw new GenericConnectionException(e); } // Wait for the response from the server diff --git a/smack-core/build.gradle b/smack-core/build.gradle index 8d29cfd89..d9da388b1 100644 --- a/smack-core/build.gradle +++ b/smack-core/build.gradle @@ -25,6 +25,8 @@ dependencies { testCompile "org.xmlunit:xmlunit-assertj:$xmlUnitVersion" testCompile 'com.jamesmurty.utils:java-xmlbuilder:1.2' testCompile 'org.bouncycastle:bcprov-jdk15on:1.64' + testCompile 'com.google.guava:guava:28.2-jre' + testCompile 'org.jgrapht:jgrapht-io:1.3.1' } class CreateFileTask extends DefaultTask { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java index 0e13c28c7..3c20fce8b 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -1,6 +1,6 @@ /** * - * Copyright 2009 Jive Software, 2018-2019 Florian Schmaus. + * Copyright 2009 Jive Software, 2018-2020 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,6 @@ import java.security.SecureRandom; import java.security.Security; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; -import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; @@ -87,6 +86,7 @@ import org.jivesoftware.smack.XMPPException.StreamErrorException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.compress.packet.Compress; import org.jivesoftware.smack.compression.XMPPInputOutputStream; +import org.jivesoftware.smack.datatypes.UInt16; import org.jivesoftware.smack.debugger.SmackDebugger; import org.jivesoftware.smack.debugger.SmackDebuggerFactory; import org.jivesoftware.smack.filter.IQReplyFilter; @@ -136,7 +136,6 @@ import org.jivesoftware.smack.util.ParserUtils; import org.jivesoftware.smack.util.Predicate; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.TLSUtils; -import org.jivesoftware.smack.util.dns.HostAddress; import org.jivesoftware.smack.util.dns.SmackDaneProvider; import org.jivesoftware.smack.util.dns.SmackDaneVerifier; import org.jivesoftware.smack.xml.XmlPullParser; @@ -150,8 +149,6 @@ import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Resourcepart; import org.jxmpp.stringprep.XmppStringprepException; import org.jxmpp.util.XmppStringUtils; -import org.minidns.dnsname.DnsName; - /** * This abstract class is commonly used as super class for XMPP connection mechanisms like TCP and BOSH. Hence it @@ -394,7 +391,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { /** * The used port to establish the connection to */ - protected int port; + protected UInt16 port; /** * Flag that indicates if the user is currently authenticated with the server. @@ -484,7 +481,12 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { @Override public int getPort() { - return port; + final UInt16 port = this.port; + if (port == null) { + return -1; + } + + return port.intValue(); } @Override @@ -525,6 +527,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { saslFeatureReceived.init(); lastFeaturesReceived.init(); tlsHandled.init(); + closingStreamReceived.init(); } /** @@ -778,38 +781,6 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { private DomainBareJid xmppServiceDomain; - protected List hostAddresses; - - /** - * Populates {@link #hostAddresses} with the resolved addresses or with the configured host address. If no host - * address was configured and all lookups failed, for example with NX_DOMAIN, then {@link #hostAddresses} will be - * populated with the empty list. - * - * @return a list of host addresses where DNS (SRV) RR resolution failed. - */ - protected List populateHostAddresses() { - List failedAddresses = new LinkedList<>(); - if (config.hostAddress != null) { - hostAddresses = new ArrayList<>(1); - HostAddress hostAddress = new HostAddress(config.port, config.hostAddress); - hostAddresses.add(hostAddress); - } - else if (config.host != null) { - hostAddresses = new ArrayList<>(1); - HostAddress hostAddress = DNSUtil.getDNSResolver().lookupHostAddress(config.host, config.port, failedAddresses, config.getDnssecMode()); - if (hostAddress != null) { - hostAddresses.add(hostAddress); - } - } else { - // N.B.: Important to use config.serviceName and not AbstractXMPPConnection.serviceName - DnsName dnsName = DnsName.from(config.getXMPPServiceDomain()); - hostAddresses = DNSUtil.resolveXMPPServiceDomain(dnsName, failedAddresses, config.getDnssecMode()); - } - // Either the populated host addresses are not empty *or* there must be at least one failed address. - assert !hostAddresses.isEmpty() || !failedAddresses.isEmpty(); - return failedAddresses; - } - protected Lock getConnectionLock() { return connectionLock; } @@ -980,6 +951,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { tlsHandled.reportGenericFailure(smackWrappedException); saslFeatureReceived.reportGenericFailure(smackWrappedException); lastFeaturesReceived.reportGenericFailure(smackWrappedException); + closingStreamReceived.reportFailure(smackWrappedException); // TODO From XMPPTCPConnection. Was called in Smack 4.3 where notifyConnectionError() was part of // XMPPTCPConnection. Create delegation method? // maybeCompressFeaturesReceived.reportGenericFailure(smackWrappedException); @@ -2182,6 +2154,10 @@ public abstract class AbstractXMPPConnection implements XMPPConnection { CACHED_EXECUTOR_SERVICE.execute(runnable); } + protected final SmackReactor getReactor() { + return SMACK_REACTOR; + } + protected static ScheduledAction schedule(Runnable runnable, long delay, TimeUnit unit) { return SMACK_REACTOR.schedule(runnable, delay, unit); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXmppNioConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXmppNioConnection.java deleted file mode 100644 index 8655db39f..000000000 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXmppNioConnection.java +++ /dev/null @@ -1,50 +0,0 @@ -/** - * - * Copyright 2018 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; - -import java.nio.channels.ClosedChannelException; -import java.nio.channels.SelectableChannel; -import java.nio.channels.SelectionKey; - -import org.jivesoftware.smack.SmackReactor.ChannelSelectedCallback; -import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection; -import org.jivesoftware.smack.fsm.StateDescriptor; -import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex; - -public abstract class AbstractXmppNioConnection extends AbstractXmppStateMachineConnection { - - protected AbstractXmppNioConnection(ConnectionConfiguration configuration, GraphVertex initialStateDescriptorVertex) { - super(configuration, initialStateDescriptorVertex); - } - - protected SelectionKey registerWithSelector(SelectableChannel channel, int ops, ChannelSelectedCallback callback) - throws ClosedChannelException { - return SMACK_REACTOR.registerWithSelector(channel, ops, callback); - } - - /** - * Set the interest Ops of a SelectionKey. Since Java's NIO interestOps(int) can block at any time, we use a queue - * to perform the actual operation in the reactor where we can perform this operation non-blocking. - * - * @param selectionKey TODO javadoc me please - * @param interestOps TODO javadoc me please - */ - protected void setInterestOps(SelectionKey selectionKey, int interestOps) { - SMACK_REACTOR.setInterestOps(selectionKey, interestOps); - } - -} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java b/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java index 33d1bfdd4..3d59269c7 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2017-2019 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2017-2020 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -229,14 +229,18 @@ public abstract class ConnectionConfiguration { } - DnsName getHost() { + public DnsName getHost() { return host; } - InetAddress getHostAddress() { + public InetAddress getHostAddress() { return hostAddress; } + public int getPort() { + return port; + } + /** * Returns the server name of the target server. * diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java index cd49af68e..9e15ef8b0 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software. + * Copyright 2003-2007 Jive Software, 2018-2020 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,8 @@ import java.util.Set; import javax.net.ssl.HostnameVerifier; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModuleDescriptor; import org.jivesoftware.smack.compression.XMPPInputOutputStream; import org.jivesoftware.smack.debugger.ReflectionDebuggerFactory; import org.jivesoftware.smack.debugger.SmackDebuggerFactory; @@ -379,4 +381,19 @@ public final class SmackConfiguration { return defaultConcurrencyLevelLimit; } + private static final Set> KNOWN_MODULES = new HashSet<>(); + + public static boolean addModule(Class moduleDescriptor) { + synchronized (KNOWN_MODULES) { + return KNOWN_MODULES.add(moduleDescriptor); + } + } + + public static void addAllKnownModulesTo(ModularXmppClientToServerConnectionConfiguration.Builder builder) { + synchronized (KNOWN_MODULES) { + for (Class moduleDescriptor : KNOWN_MODULES) { + builder.addModule(moduleDescriptor); + } + } + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java index 41b3b417f..fbf6ebd40 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackException.java @@ -1,6 +1,6 @@ /** * - * Copyright 2014-2019 Florian Schmaus + * Copyright 2014-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,15 @@ */ package org.jivesoftware.smack; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import org.jivesoftware.smack.c2s.XmppClientToServerTransport.LookupConnectionEndpointsFailed; import org.jivesoftware.smack.filter.StanzaFilter; -import org.jivesoftware.smack.util.dns.HostAddress; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.util.rce.RemoteConnectionEndpoint; +import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure; +import org.jivesoftware.smack.util.rce.RemoteConnectionException; import org.jxmpp.jid.Jid; @@ -90,6 +94,7 @@ public abstract class SmackException extends Exception { public static NoResponseException newWith(XMPPConnection connection, String waitingFor) { final StringBuilder sb = getWaitingFor(connection); sb.append(" While waiting for ").append(waitingFor); + sb.append(" [").append(connection).append(']'); return new NoResponseException(sb.toString()); } @@ -264,45 +269,112 @@ public abstract class SmackException extends Exception { } } + public abstract static class ConnectionException extends SmackException { + + private static final long serialVersionUID = 1L; + + protected ConnectionException(Throwable wrappedThrowable) { + super(wrappedThrowable); + } + + protected ConnectionException(String message) { + super(message); + } + + } + + public static final class GenericConnectionException extends ConnectionException { + + private static final long serialVersionUID = 1L; + + /** + * Deprecated, do not use. + * + * @param wrappedThrowable the wrapped throwable. + */ + @Deprecated + public GenericConnectionException(Throwable wrappedThrowable) { + super(wrappedThrowable); + } + } + /** - * ConnectionException is thrown if Smack is unable to connect to all hosts of a given XMPP - * service. The failed hosts can be retrieved with - * {@link ConnectionException#getFailedAddresses()}, which will have the exception causing the - * connection failure set and retrievable with {@link HostAddress#getExceptions()}. + * This exception is thrown if Smack is unable to connect to all hosts of a given XMPP + * service. The connection exceptions can be retrieved with + * {@link EndpointConnectionException#getConnectionExceptions()}, which will have the exception causing the + * connection failure set and retrievable with {@link RemoteConnectionException#getException()}. */ - public static class ConnectionException extends SmackException { + public static final class EndpointConnectionException extends ConnectionException { /** * */ - private static final long serialVersionUID = 1686944201672697996L; + private static final long serialVersionUID = 1; - private final List failedAddresses; + private final List lookupFailures; + private final List> connectionExceptions; - public ConnectionException(Throwable wrappedThrowable) { - super(wrappedThrowable); - failedAddresses = new ArrayList<>(0); - } - - private ConnectionException(String message, List failedAddresses) { + private EndpointConnectionException(String message, List lookupFailures, + List> connectionExceptions) { super(message); - this.failedAddresses = failedAddresses; + // At least one list must contain an entry. + assert !lookupFailures.isEmpty() || !connectionExceptions.isEmpty(); + this.lookupFailures = lookupFailures; + this.connectionExceptions = connectionExceptions; } - public static ConnectionException from(List failedAddresses) { - final String DELIMITER = ", "; - StringBuilder sb = new StringBuilder("The following addresses failed: "); - for (HostAddress hostAddress : failedAddresses) { - sb.append(hostAddress.getErrorMessage()); - sb.append(DELIMITER); + public static EndpointConnectionException from(List lookupFailures, + List> connectionExceptions) { + StringBuilder sb = new StringBuilder(256); + + if (!lookupFailures.isEmpty()) { + sb.append("Could not lookup the following endpoints: "); + StringUtils.appendTo(lookupFailures, sb); } - // Remove the last delimiter - sb.setLength(sb.length() - DELIMITER.length()); - return new ConnectionException(sb.toString(), failedAddresses); + + if (!connectionExceptions.isEmpty()) { + sb.append("The following addresses failed: "); + StringUtils.appendTo(connectionExceptions, sb, rce -> sb.append(rce.getErrorMessage())); + } + + return new EndpointConnectionException(sb.toString(), lookupFailures, connectionExceptions); } - public List getFailedAddresses() { - return failedAddresses; + public List getLookupFailures() { + return lookupFailures; + } + + public List> getConnectionExceptions() { + return connectionExceptions; + } + } + + public static final class NoEndpointsDiscoveredException extends ConnectionException { + + private static final long serialVersionUID = 1L; + + private final List lookupFailures; + + private NoEndpointsDiscoveredException(String message, List lookupFailures) { + super(message); + this.lookupFailures = Collections.unmodifiableList(lookupFailures); + } + + public List getLookupFailures() { + return lookupFailures; + } + + public static NoEndpointsDiscoveredException from(List lookupFailures) { + StringBuilder sb = new StringBuilder(); + + if (lookupFailures.isEmpty()) { + sb.append("No endpoint lookup finished within the timeout"); + } else { + sb.append("Not endpoints could be discovered due the following lookup failures: "); + StringUtils.appendTo(lookupFailures, sb); + } + + return new NoEndpointsDiscoveredException(sb.toString(), lookupFailures); } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackFuture.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackFuture.java index b08a50603..08646728a 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackFuture.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackFuture.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017-2018 Florian Schmaus + * Copyright 2017-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,9 @@ package org.jivesoftware.smack; import java.io.IOException; import java.net.Socket; import java.net.SocketAddress; +import java.util.Collection; import java.util.concurrent.CancellationException; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -31,6 +33,7 @@ import javax.net.SocketFactory; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.util.CallbackRecipient; +import org.jivesoftware.smack.util.Consumer; import org.jivesoftware.smack.util.ExceptionCallback; import org.jivesoftware.smack.util.SuccessCallback; @@ -48,6 +51,8 @@ public abstract class SmackFuture implements Future, private ExceptionCallback exceptionCallback; + private Consumer> completionCallback; + @Override public final synchronized boolean cancel(boolean mayInterruptIfRunning) { if (isDone()) { @@ -87,6 +92,11 @@ public abstract class SmackFuture implements Future, return this; } + public void onCompletion(Consumer> completionCallback) { + this.completionCallback = completionCallback; + maybeInvokeCallbacks(); + } + private V getOrThrowExecutionException() throws ExecutionException { assert result != null || exception != null || cancelled; if (result != null) { @@ -148,11 +158,19 @@ public abstract class SmackFuture implements Future, return getOrThrowExecutionException(); } + public V getIfAvailable() { + return result; + } + protected final synchronized void maybeInvokeCallbacks() { if (cancelled) { return; } + if ((result != null || exception != null) && completionCallback != null) { + completionCallback.accept(this); + } + if (result != null && successCallback != null) { AbstractXMPPConnection.asyncGo(new Runnable() { @Override @@ -308,4 +326,12 @@ public abstract class SmackFuture implements Future, return future; } + public static boolean await(Collection> futures, long timeout, TimeUnit unit) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(futures.size()); + for (SmackFuture future : futures) { + future.onCompletion(f -> latch.countDown()); + } + + return latch.await(timeout, unit); + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackInitialization.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackInitialization.java index 92c122571..2d5aeb30e 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackInitialization.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackInitialization.java @@ -26,12 +26,15 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import org.jivesoftware.smack.bind2.Bind2ModuleDescriptor; import org.jivesoftware.smack.compress.provider.CompressedProvider; import org.jivesoftware.smack.compress.provider.FailureProvider; +import org.jivesoftware.smack.compression.CompressionModuleDescriptor; import org.jivesoftware.smack.compression.Java7ZlibInputOutputStream; import org.jivesoftware.smack.compression.XmppCompressionManager; import org.jivesoftware.smack.compression.zlib.ZlibXmppCompressionFactory; import org.jivesoftware.smack.initializer.SmackInitializer; +import org.jivesoftware.smack.isr.InstantStreamResumptionModuleDescriptor; import org.jivesoftware.smack.packet.Bind; import org.jivesoftware.smack.packet.Message.Body; import org.jivesoftware.smack.provider.BindIQProvider; @@ -136,6 +139,10 @@ public final class SmackInitialization { ProviderManager.addNonzaProvider(CompressedProvider.INSTANCE); ProviderManager.addNonzaProvider(FailureProvider.INSTANCE); + SmackConfiguration.addModule(Bind2ModuleDescriptor.class); + SmackConfiguration.addModule(CompressionModuleDescriptor.class); + SmackConfiguration.addModule(InstantStreamResumptionModuleDescriptor.class); + SmackConfiguration.smackInitialized = true; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackReactor.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackReactor.java index 1c2477789..76d028204 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackReactor.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackReactor.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019 Florian Schmaus + * Copyright 2018-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -116,7 +116,7 @@ public class SmackReactor { setReactorThreadCount(DEFAULT_REACTOR_THREAD_COUNT); } - SelectionKey registerWithSelector(SelectableChannel channel, int ops, ChannelSelectedCallback callback) + public SelectionKey registerWithSelector(SelectableChannel channel, int ops, ChannelSelectedCallback callback) throws ClosedChannelException { SelectionKeyAttachment selectionKeyAttachment = new SelectionKeyAttachment(callback); @@ -129,7 +129,7 @@ public class SmackReactor { } } - void setInterestOps(SelectionKey selectionKey, int interestOps) { + public void setInterestOps(SelectionKey selectionKey, int interestOps) { SetInterestOps setInterestOps = new SetInterestOps(selectionKey, interestOps); pendingSetInterestOps.add(setInterestOps); selector.wakeup(); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/XmppInputOutputFilter.java b/smack-core/src/main/java/org/jivesoftware/smack/XmppInputOutputFilter.java index b7b14ce05..18432f564 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/XmppInputOutputFilter.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/XmppInputOutputFilter.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018 Florian Schmaus + * Copyright 2018-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -70,7 +70,7 @@ public interface XmppInputOutputFilter { default void waitUntilInputOutputClosed() throws IOException, NoResponseException, CertificateException, InterruptedException, SmackException { } - default Object getStats() { - return null; - } + Object getStats(); + + String getFilterName(); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/bind2/Bind2Module.java b/smack-core/src/main/java/org/jivesoftware/smack/bind2/Bind2Module.java new file mode 100644 index 000000000..53162ddce --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/bind2/Bind2Module.java @@ -0,0 +1,77 @@ +/** + * + * Copyright 2019-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.bind2; + +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.AuthenticatedAndResourceBoundStateDescriptor; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.ConnectedButUnauthenticatedStateDescriptor; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.SaslAuthenticationStateDescriptor; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModule; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.c2s.internal.WalkStateGraphContext; +import org.jivesoftware.smack.fsm.State; +import org.jivesoftware.smack.fsm.StateDescriptor; +import org.jivesoftware.smack.fsm.StateTransitionResult; + +public class Bind2Module extends ModularXmppClientToServerConnectionModule { + + protected Bind2Module(Bind2ModuleDescriptor moduleDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + super(moduleDescriptor, connectionInternal); + } + + public static final class Bind2StateDescriptor extends StateDescriptor { + private Bind2StateDescriptor() { + super(Bind2State.class, 386, StateDescriptor.Property.notImplemented); + + addPredeccessor(ConnectedButUnauthenticatedStateDescriptor.class); + addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); + declarePrecedenceOver(SaslAuthenticationStateDescriptor.class); + } + + @Override + protected Bind2Module.Bind2State constructState(ModularXmppClientToServerConnectionInternal connectionInternal) { + // This is the trick: the module is constructed prior the states, so we get the actual state out of the module by fetching the module from the connection. + Bind2Module bind2Module = connectionInternal.connection.getConnectionModuleFor(Bind2ModuleDescriptor.class); + return bind2Module.constructBind2State(this, connectionInternal); + } + } + + private static final class Bind2State extends State { + + private Bind2State(Bind2StateDescriptor bind2StateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + super(bind2StateDescriptor, connectionInternal); + } + + @Override + public StateTransitionResult.TransitionImpossible isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { + return new StateTransitionResult.TransitionImpossibleBecauseNotImplemented(stateDescriptor); + } + + @Override + public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + throw new IllegalStateException("Bind2 not implemented"); + } + + } + + public Bind2State constructBind2State(Bind2StateDescriptor bind2StateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + return new Bind2State(bind2StateDescriptor, connectionInternal); + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/bind2/Bind2ModuleDescriptor.java b/smack-core/src/main/java/org/jivesoftware/smack/bind2/Bind2ModuleDescriptor.java new file mode 100644 index 000000000..bf09a4522 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/bind2/Bind2ModuleDescriptor.java @@ -0,0 +1,53 @@ +/** + * + * Copyright 2019-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.bind2; + +import java.util.Collections; +import java.util.Set; + +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModuleDescriptor; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.fsm.StateDescriptor; + +public class Bind2ModuleDescriptor extends ModularXmppClientToServerConnectionModuleDescriptor { + + private static final Bind2ModuleDescriptor INSTANCE = new Bind2ModuleDescriptor(); + + @Override + protected Set> getStateDescriptors() { + return Collections.singleton(Bind2Module.Bind2StateDescriptor.class); + } + + @Override + protected Bind2Module constructXmppConnectionModule( + ModularXmppClientToServerConnectionInternal connectionInternal) { + return new Bind2Module(this, connectionInternal); + } + + public static class Builder extends ModularXmppClientToServerConnectionModuleDescriptor.Builder { + + protected Builder(ModularXmppClientToServerConnectionConfiguration.Builder connectionConfigurationBuilder) { + super(connectionConfigurationBuilder); + } + + @Override + protected Bind2ModuleDescriptor build() { + return INSTANCE; + } + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/bind2/package-info.java b/smack-core/src/main/java/org/jivesoftware/smack/bind2/package-info.java new file mode 100644 index 000000000..bc422ed02 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/bind2/package-info.java @@ -0,0 +1,21 @@ +/** + * + * Copyright 2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Classes and interfaces for Bind 2.0 (XEP-0386). + */ +package org.jivesoftware.smack.bind2; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnection.java new file mode 100644 index 000000000..9694b47b8 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnection.java @@ -0,0 +1,1114 @@ +/** + * + * Copyright 2018-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.c2s; + +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.ssl.SSLSession; + +import org.jivesoftware.smack.AbstractXMPPConnection; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.SmackException.ConnectionUnexpectedTerminatedException; +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.SmackFuture; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.XMPPException.FailedNonzaException; +import org.jivesoftware.smack.XMPPException.StreamErrorException; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.XmppInputOutputFilter; +import org.jivesoftware.smack.c2s.XmppClientToServerTransport.LookupConnectionEndpointsFailed; +import org.jivesoftware.smack.c2s.XmppClientToServerTransport.LookupConnectionEndpointsResult; +import org.jivesoftware.smack.c2s.XmppClientToServerTransport.LookupConnectionEndpointsSuccess; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.c2s.internal.WalkStateGraphContext; +import org.jivesoftware.smack.fsm.ConnectionStateEvent; +import org.jivesoftware.smack.fsm.ConnectionStateMachineListener; +import org.jivesoftware.smack.fsm.LoginContext; +import org.jivesoftware.smack.fsm.NoOpState; +import org.jivesoftware.smack.fsm.State; +import org.jivesoftware.smack.fsm.StateDescriptor; +import org.jivesoftware.smack.fsm.StateDescriptorGraph; +import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex; +import org.jivesoftware.smack.fsm.StateMachineException; +import org.jivesoftware.smack.fsm.StateTransitionResult; +import org.jivesoftware.smack.fsm.StateTransitionResult.AttemptResult; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Nonza; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.packet.StreamClose; +import org.jivesoftware.smack.packet.StreamError; +import org.jivesoftware.smack.packet.TopLevelStreamElement; +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.parsing.SmackParsingException; +import org.jivesoftware.smack.sasl.SASLErrorException; +import org.jivesoftware.smack.sasl.SASLMechanism; +import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.xml.XmlPullParser; +import org.jivesoftware.smack.xml.XmlPullParserException; + +import org.jxmpp.jid.parts.Resourcepart; + +public final class ModularXmppClientToServerConnection extends AbstractXMPPConnection { + + private static final Logger LOGGER = Logger.getLogger(ModularXmppClientToServerConnectionConfiguration.class.getName()); + + private final ArrayBlockingQueueWithShutdown outgoingElementsQueue = new ArrayBlockingQueueWithShutdown<>( + 100, true); + + private XmppClientToServerTransport activeTransport; + + private final List connectionStateMachineListeners = new CopyOnWriteArrayList<>(); + + private boolean featuresReceived; + + protected boolean streamResumed; + + private GraphVertex currentStateVertex; + + private List walkFromDisconnectToAuthenticated; + + private final ModularXmppClientToServerConnectionConfiguration configuration; + + private final ModularXmppClientToServerConnectionInternal connectionInternal; + + private final Map, ModularXmppClientToServerConnectionModule> connectionModules = new HashMap<>(); + + private final Map, XmppClientToServerTransport> transports = new HashMap<>(); + /** + * This is one of those cases where the field is modified by one thread and read by another. We currently use + * CopyOnWriteArrayList but should potentially use a VarHandle once Smack supports them. + */ + private final List inputOutputFilters = new CopyOnWriteArrayList<>(); + + private List previousInputOutputFilters; + + public ModularXmppClientToServerConnection(ModularXmppClientToServerConnectionConfiguration configuration) { + super(configuration); + + this.configuration = configuration; + + // Construct the internal connection API. + connectionInternal = new ModularXmppClientToServerConnectionInternal(this, getReactor(), debugger, outgoingElementsQueue) { + + @Override + public void parseAndProcessElement(String wrappedCompleteElement) { + ModularXmppClientToServerConnection.this.parseAndProcessElement(wrappedCompleteElement); + } + + @Override + public void notifyConnectionError(Exception e) { + ModularXmppClientToServerConnection.this.notifyConnectionError(e); + } + + @Override + public void onStreamOpen(XmlPullParser parser) { + ModularXmppClientToServerConnection.this.onStreamOpen(parser); + } + + @Override + public void onStreamClosed() { + ModularXmppClientToServerConnection.this.closingStreamReceived.reportSuccess(); + } + + @Override + public void fireFirstLevelElementSendListeners(TopLevelStreamElement element) { + ModularXmppClientToServerConnection.this.firePacketSendingListeners(element); + } + + @Override + public void invokeConnectionStateMachineListener(ConnectionStateEvent connectionStateEvent) { + ModularXmppClientToServerConnection.this.invokeConnectionStateMachineListener(connectionStateEvent); + } + + @Override + public XmlEnvironment getOutgoingStreamXmlEnvironment() { + return outgoingStreamXmlEnvironment; + } + + @Override + public void addXmppInputOutputFilter(XmppInputOutputFilter xmppInputOutputFilter) { + inputOutputFilters.add(0, xmppInputOutputFilter); + } + + @Override + public ListIterator getXmppInputOutputFilterBeginIterator() { + return inputOutputFilters.listIterator(); + } + + @Override + public ListIterator getXmppInputOutputFilterEndIterator() { + return inputOutputFilters.listIterator(inputOutputFilters.size()); + } + + @Override + public void newStreamOpenWaitForFeaturesSequence(String waitFor) throws InterruptedException, + ConnectionUnexpectedTerminatedException, NoResponseException, NotConnectedException { + ModularXmppClientToServerConnection.this.newStreamOpenWaitForFeaturesSequence(waitFor); + } + + @Override + public SmackTlsContext getSmackTlsContext() + throws KeyManagementException, NoSuchAlgorithmException, CertificateException, IOException, + UnrecoverableKeyException, KeyStoreException, NoSuchProviderException { + return ModularXmppClientToServerConnection.this.getSmackTlsContext(); + } + + @Override + public SN sendAndWaitForResponse(Nonza nonza, Class successNonzaClass, + Class failedNonzaClass) throws NoResponseException, NotConnectedException, FailedNonzaException, InterruptedException { + return ModularXmppClientToServerConnection.this.sendAndWaitForResponse(nonza, successNonzaClass, failedNonzaClass); + } + + @Override + public void asyncGo(Runnable runnable) { + AbstractXMPPConnection.asyncGo(runnable); + } + + @Override + public Exception getCurrentConnectionException() { + return ModularXmppClientToServerConnection.this.currentConnectionException; + } + + @Override + public void setCompressionEnabled(boolean compressionEnabled) { + ModularXmppClientToServerConnection.this.compressionEnabled = compressionEnabled; + } + + @Override + public void setTransport(XmppClientToServerTransport xmppTransport) { + ModularXmppClientToServerConnection.this.activeTransport = xmppTransport; + } + + }; + + // Construct the modules from the module descriptor. We do this before constructing the state graph, as the + // modules are sometimes used to construct the states. + for (ModularXmppClientToServerConnectionModuleDescriptor moduleDescriptor : configuration.moduleDescriptors) { + Class moduleDescriptorClass = moduleDescriptor.getClass(); + ModularXmppClientToServerConnectionModule connectionModule = moduleDescriptor.constructXmppConnectionModule(connectionInternal); + connectionModules.put(moduleDescriptorClass, connectionModule); + + XmppClientToServerTransport transport = connectionModule.getTransport(); + // Not every module may provide a transport. + if (transport != null) { + transports.put(moduleDescriptorClass, transport); + } + } + + GraphVertex initialStateDescriptorVertex = configuration.initialStateDescriptorVertex; + // Convert the graph of state descriptors to a graph of states, bound to this very connection. + currentStateVertex = StateDescriptorGraph.convertToStateGraph(initialStateDescriptorVertex, connectionInternal); + } + + @SuppressWarnings("unchecked") + public > CM getConnectionModuleFor( + Class descriptorClass) { + return (CM) connectionModules.get(descriptorClass); + } + + @Override + protected void loginInternal(String username, String password, Resourcepart resource) + throws XMPPException, SmackException, IOException, InterruptedException { + WalkStateGraphContext walkStateGraphContext = buildNewWalkTo( + AuthenticatedAndResourceBoundStateDescriptor.class).withLoginContext(username, password, + resource).build(); + walkStateGraph(walkStateGraphContext); + } + + protected WalkStateGraphContext.Builder buildNewWalkTo(Class finalStateClass) { + return WalkStateGraphContext.builder(currentStateVertex.getElement().getStateDescriptor().getClass(), finalStateClass); + } + + /** + * Unwind the state. This will revert the effects of the state by calling {@link State#resetState()} prior issuing a + * connection state event of {@link ConnectionStateEvent#StateRevertBackwardsWalk}. + * + * @param revertedState the state which is going to get reverted. + */ + private void unwindState(State revertedState) { + invokeConnectionStateMachineListener(new ConnectionStateEvent.StateRevertBackwardsWalk(revertedState)); + revertedState.resetState(); + } + + protected void walkStateGraph(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, + SASLErrorException, FailedNonzaException, IOException, SmackException, InterruptedException { + // Save a copy of the current state + GraphVertex previousStateVertex = currentStateVertex; + try { + walkStateGraphInternal(walkStateGraphContext); + } catch (XMPPErrorException | SASLErrorException | FailedNonzaException | IOException | SmackException + | InterruptedException e) { + currentStateVertex = previousStateVertex; + // Unwind the state. + State revertedState = currentStateVertex.getElement(); + unwindState(revertedState); + throw e; + } + } + + private void walkStateGraphInternal(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, + SASLErrorException, IOException, SmackException, InterruptedException, FailedNonzaException { + // Save a copy of the current state + final GraphVertex initialStateVertex = currentStateVertex; + final State initialState = initialStateVertex.getElement(); + final StateDescriptor initialStateDescriptor = initialState.getStateDescriptor(); + + walkStateGraphContext.recordWalkTo(initialState); + + // Check if this is the walk's final state. + if (walkStateGraphContext.isWalksFinalState(initialStateDescriptor)) { + // If this is used as final state, then it should be marked as such. + assert initialStateDescriptor.isFinalState(); + + // We reached the final state. + invokeConnectionStateMachineListener(new ConnectionStateEvent.FinalStateReached(initialState)); + return; + } + + List> outgoingStateEdges = initialStateVertex.getOutgoingEdges(); + + // See if we need to handle mandatory intermediate states. + GraphVertex mandatoryIntermediateStateVertex = walkStateGraphContext.maybeReturnMandatoryImmediateState(outgoingStateEdges); + if (mandatoryIntermediateStateVertex != null) { + StateTransitionResult reason = attemptEnterState(mandatoryIntermediateStateVertex, walkStateGraphContext); + + if (reason instanceof StateTransitionResult.Success) { + walkStateGraph(walkStateGraphContext); + return; + } + + // We could not enter a mandatory intermediate state. Throw here. + throw new StateMachineException.SmackMandatoryStateFailedException( + mandatoryIntermediateStateVertex.getElement(), reason); + } + + for (Iterator> it = outgoingStateEdges.iterator(); it.hasNext();) { + GraphVertex successorStateVertex = it.next(); + State successorState = successorStateVertex.getElement(); + + // Ignore successorStateVertex if the only way to the final state is via the initial state. This happens + // typically if we are in the ConnectedButUnauthenticated state on the way to ResourceboundAndAuthenticated, + // where we do not want to walk via InstantShutdown/Shtudown in a cycle over the initial state towards this + // state. + if (walkStateGraphContext.wouldCauseCycle(successorStateVertex)) { + // Ignore this successor. + invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionIgnoredDueCycle(initialStateVertex, successorStateVertex)); + } else { + StateTransitionResult result = attemptEnterState(successorStateVertex, walkStateGraphContext); + + if (result instanceof StateTransitionResult.Success) { + break; + } + + // If attemptEnterState did not throw and did not return a value of type TransitionSuccessResult, then we + // just record this value and go on from there. Note that reason may be null, which is returned by + // attemptEnterState in case the state was already successfully handled. If this is the case, then we don't + // record it. + if (result != null) { + walkStateGraphContext.recordFailedState(successorState, result); + } + } + + if (!it.hasNext()) { + throw StateMachineException.SmackStateGraphDeadEndException.from(walkStateGraphContext, + initialStateVertex); + } + } + + // Walk the state graph by recursion. + walkStateGraph(walkStateGraphContext); + } + + /** + * Attempt to enter a state. Note that this method may return null if this state can be safely ignored ignored. + * + * @param successorStateVertex the successor state vertex. + * @param walkStateGraphContext the "walk state graph" context. + * @return A state transition result or null if this state can be ignored. + * @throws SmackException if Smack detected an exceptional situation. + * @throws XMPPErrorException if an XMPP protocol error was received. + * @throws SASLErrorException if a SASL protocol error was returned. + * @throws IOException if an I/O error occurred. + * @throws InterruptedException if the calling thread was interrupted. + * @throws FailedNonzaException if an XMPP protocol failure was received. + */ + private StateTransitionResult attemptEnterState(GraphVertex successorStateVertex, + WalkStateGraphContext walkStateGraphContext) throws SmackException, XMPPErrorException, + SASLErrorException, IOException, InterruptedException, FailedNonzaException { + final GraphVertex initialStateVertex = currentStateVertex; + final State initialState = initialStateVertex.getElement(); + final State successorState = successorStateVertex.getElement(); + final StateDescriptor successorStateDescriptor = successorState.getStateDescriptor(); + + if (!successorStateDescriptor.isMultiVisitState() + && walkStateGraphContext.stateAlreadyVisited(successorState)) { + // This can happen if a state leads back to the state where it originated from. See for example the + // 'Compression' state. We return 'null' here to signal that the state can safely be ignored. + return null; + } + + if (successorStateDescriptor.isNotImplemented()) { + StateTransitionResult.TransitionImpossibleBecauseNotImplemented transtionImpossibleBecauseNotImplemented = new StateTransitionResult.TransitionImpossibleBecauseNotImplemented( + successorStateDescriptor); + invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionNotPossible(initialState, successorState, + transtionImpossibleBecauseNotImplemented)); + return transtionImpossibleBecauseNotImplemented; + } + + final StateTransitionResult.AttemptResult transitionAttemptResult; + try { + StateTransitionResult.TransitionImpossible transitionImpossible = successorState.isTransitionToPossible( + walkStateGraphContext); + if (transitionImpossible != null) { + invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionNotPossible(initialState, successorState, + transitionImpossible)); + return transitionImpossible; + } + + invokeConnectionStateMachineListener(new ConnectionStateEvent.AboutToTransitionInto(initialState, successorState)); + transitionAttemptResult = successorState.transitionInto(walkStateGraphContext); + } catch (SmackException | XMPPErrorException | SASLErrorException | IOException | InterruptedException + | FailedNonzaException e) { + // Unwind the state here too, since this state will not be unwound by walkStateGraph(), as it will not + // become a predecessor state in the walk. + unwindState(successorState); + throw e; + } + if (transitionAttemptResult instanceof StateTransitionResult.Failure) { + StateTransitionResult.Failure transitionFailureResult = (StateTransitionResult.Failure) transitionAttemptResult; + invokeConnectionStateMachineListener( + new ConnectionStateEvent.TransitionFailed(initialState, successorState, transitionFailureResult)); + return transitionAttemptResult; + } + + // If transitionAttemptResult is not an instance of TransitionFailureResult, then it has to be of type + // TransitionSuccessResult. + StateTransitionResult.Success transitionSuccessResult = (StateTransitionResult.Success) transitionAttemptResult; + + currentStateVertex = successorStateVertex; + invokeConnectionStateMachineListener( + new ConnectionStateEvent.SuccessfullyTransitionedInto(successorState, transitionSuccessResult)); + + return transitionSuccessResult; + } + + @Override + protected void sendStanzaInternal(Stanza stanza) throws NotConnectedException, InterruptedException { + sendTopLevelStreamElement(stanza); + } + + @Override + public void sendNonza(Nonza nonza) throws NotConnectedException, InterruptedException { + sendTopLevelStreamElement(nonza); + } + + private void sendTopLevelStreamElement(TopLevelStreamElement element) throws NotConnectedException, InterruptedException { + final XmppClientToServerTransport transport = activeTransport; + if (transport == null) { + throw new NotConnectedException(); + } + + outgoingElementsQueue.put(element); + transport.notifyAboutNewOutgoingElements(); + } + + @Override + protected void shutdown() { + shutdown(false); + } + + @Override + public synchronized void instantShutdown() { + shutdown(true); + } + + @Override + public ModularXmppClientToServerConnectionConfiguration getConfiguration() { + return configuration; + } + + private void shutdown(boolean instant) { + Class mandatoryIntermediateState; + if (instant) { + mandatoryIntermediateState = InstantShutdownStateDescriptor.class; + } else { + mandatoryIntermediateState = ShutdownStateDescriptor.class; + } + + WalkStateGraphContext context = buildNewWalkTo( + DisconnectedStateDescriptor.class).withMandatoryIntermediateState( + mandatoryIntermediateState).build(); + + try { + walkStateGraph(context); + } catch (XMPPErrorException | SASLErrorException | IOException | SmackException | InterruptedException + | FailedNonzaException e) { + throw new IllegalStateException("A walk to disconnected state should never throw", e); + } + } + + protected SSLSession getSSLSession() { + final XmppClientToServerTransport transport = activeTransport; + if (transport == null) { + return null; + } + return transport.getSslSession(); + } + + @Override + protected void afterFeaturesReceived() { + featuresReceived = true; + synchronized (this) { + notifyAll(); + } + } + + protected void parseAndProcessElement(String element) { + try { + XmlPullParser parser = PacketParserUtils.getParserFor(element); + + // Skip the enclosing stream open what is guaranteed to be there. + parser.next(); + + XmlPullParser.Event event = parser.getEventType(); + outerloop: while (true) { + switch (event) { + case START_ELEMENT: + final String name = parser.getName(); + // Note that we don't handle "stream" here as it's done in the splitter. + switch (name) { + case Message.ELEMENT: + case IQ.IQ_ELEMENT: + case Presence.ELEMENT: + try { + parseAndProcessStanza(parser); + } finally { + // TODO: Here would be the following stream management code. + // clientHandledStanzasCount = SMUtils.incrementHeight(clientHandledStanzasCount); + } + break; + case "error": + StreamError streamError = PacketParserUtils.parseStreamError(parser, null); + saslFeatureReceived.reportFailure(new StreamErrorException(streamError)); + throw new StreamErrorException(streamError); + case "features": + parseFeatures(parser); + afterFeaturesReceived(); + break; + default: + parseAndProcessNonza(parser); + break; + } + break; + case END_DOCUMENT: + break outerloop; + default: // fall out + } + event = parser.next(); + } + } catch (XmlPullParserException | IOException | InterruptedException | StreamErrorException + | SmackParsingException e) { + notifyConnectionError(e); + } + } + + protected synchronized void prepareToWaitForFeaturesReceived() { + featuresReceived = false; + } + + protected void waitForFeaturesReceived(String waitFor) + throws InterruptedException, ConnectionUnexpectedTerminatedException, NoResponseException { + long waitStartMs = System.currentTimeMillis(); + long timeoutMs = getReplyTimeout(); + synchronized (this) { + while (!featuresReceived && currentConnectionException == null) { + long remainingWaitMs = timeoutMs - (System.currentTimeMillis() - waitStartMs); + if (remainingWaitMs <= 0) { + throw NoResponseException.newWith(this, waitFor); + } + wait(remainingWaitMs); + } + if (currentConnectionException != null) { + throw new SmackException.ConnectionUnexpectedTerminatedException(currentConnectionException); + } + } + } + + protected void newStreamOpenWaitForFeaturesSequence(String waitFor) throws InterruptedException, + ConnectionUnexpectedTerminatedException, NoResponseException, NotConnectedException { + prepareToWaitForFeaturesReceived(); + sendStreamOpen(); + waitForFeaturesReceived(waitFor); + } + + public static class DisconnectedStateDescriptor extends StateDescriptor { + protected DisconnectedStateDescriptor() { + super(DisconnectedState.class, StateDescriptor.Property.finalState); + addSuccessor(LookupRemoteConnectionEndpointsStateDescriptor.class); + } + } + + private final class DisconnectedState extends State { + + private DisconnectedState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { + super(stateDescriptor, connectionInternal); + } + + @Override + public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + synchronized (ModularXmppClientToServerConnection.this) { + if (inputOutputFilters.isEmpty()) { + previousInputOutputFilters = null; + } else { + previousInputOutputFilters = new ArrayList<>(inputOutputFilters.size()); + previousInputOutputFilters.addAll(inputOutputFilters); + inputOutputFilters.clear(); + } + } + + // Reset all states we have visited when transitioning from disconnected to authenticated. This assumes that + // every state after authenticated does not need to be reset. + ListIterator it = walkFromDisconnectToAuthenticated.listIterator( + walkFromDisconnectToAuthenticated.size()); + while (it.hasPrevious()) { + State stateToReset = it.previous(); + stateToReset.resetState(); + } + walkFromDisconnectToAuthenticated = null; + + return StateTransitionResult.Success.EMPTY_INSTANCE; + } + } + + public static final class LookupRemoteConnectionEndpointsStateDescriptor extends StateDescriptor { + private LookupRemoteConnectionEndpointsStateDescriptor() { + super(LookupRemoteConnectionEndpointsState.class); + } + } + + private final class LookupRemoteConnectionEndpointsState extends State { + boolean outgoingElementsQueueWasShutdown; + + private LookupRemoteConnectionEndpointsState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { + super(stateDescriptor, connectionInternal); + } + + @Override + public AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, + SASLErrorException, IOException, SmackException, InterruptedException, FailedNonzaException { + // There is a challenge here: We are going to trigger the discovery of endpoints which will run + // asynchronously. After a timeout, all discovered endpoints are collected. To prevent stale results from + // previous discover runs, the results are communicated via SmackFuture, so that we always handle the most + // up-to-date results. + + Map>> lookupFutures = new HashMap<>( + transports.size()); + + final int numberOfFutures; + { + List> allFutures = new ArrayList<>(); + for (XmppClientToServerTransport transport : transports.values()) { + // First we clear the transport of any potentially previously discovered connection endpoints. + transport.resetDiscoveredConnectionEndpoints(); + + // Ask the transport to start the discovery of remote connection endpoints asynchronously. + List> transportFutures = transport.lookupConnectionEndpoints(); + + lookupFutures.put(transport, transportFutures); + allFutures.addAll(transportFutures); + } + + numberOfFutures = allFutures.size(); + + // Wait until all features are ready or if the timeout occurs. Note that we do not inspect and react the + // return value of SmackFuture.await() as we want to collect the LookupConnectionEndpointsFailed later. + SmackFuture.await(allFutures, getReplyTimeout(), TimeUnit.MILLISECONDS); + } + + // Note that we do not pass the lookupFailures in case there is at least one successful transport. The + // lookup failures are also recording in LookupConnectionEndpointsSuccess, e.g. as part of + // RemoteXmppTcpConnectionEndpoints.Result. + List lookupFailures = new ArrayList<>(numberOfFutures); + + boolean atLeastOneConnectionEndpointDiscovered = false; + for (Map.Entry>> entry : lookupFutures.entrySet()) { + XmppClientToServerTransport transport = entry.getKey(); + + for (SmackFuture future : entry.getValue()) { + LookupConnectionEndpointsResult result = future.getIfAvailable(); + + if (result == null) { + continue; + } + + if (result instanceof LookupConnectionEndpointsFailed) { + LookupConnectionEndpointsFailed lookupFailure = (LookupConnectionEndpointsFailed) result; + lookupFailures.add(lookupFailure); + continue; + } + + LookupConnectionEndpointsSuccess successResult = (LookupConnectionEndpointsSuccess) result; + + // Arm the transport with the success result, so that its information can be used by the transport + // to establish the connection. + transport.loadConnectionEndpoints(successResult); + + // Mark that the connection attempt can continue. + atLeastOneConnectionEndpointDiscovered = true; + } + } + + if (!atLeastOneConnectionEndpointDiscovered) { + throw SmackException.NoEndpointsDiscoveredException.from(lookupFailures); + } + + // 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. + outgoingElementsQueueWasShutdown = outgoingElementsQueue.start(); + + return StateTransitionResult.Success.EMPTY_INSTANCE; + } + + @Override + public void resetState() { + for (XmppClientToServerTransport transport : transports.values()) { + transport.resetDiscoveredConnectionEndpoints(); + } + + if (outgoingElementsQueueWasShutdown) { + // Reset the outgoing elements queue in this state, since we also start it in this state. + outgoingElementsQueue.shutdown(); + } + } + } + + public static final class ConnectedButUnauthenticatedStateDescriptor extends StateDescriptor { + private ConnectedButUnauthenticatedStateDescriptor() { + super(ConnectedButUnauthenticatedState.class, StateDescriptor.Property.finalState); + addSuccessor(SaslAuthenticationStateDescriptor.class); + addSuccessor(InstantShutdownStateDescriptor.class); + addSuccessor(ShutdownStateDescriptor.class); + } + } + + private final class ConnectedButUnauthenticatedState extends State { + private ConnectedButUnauthenticatedState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { + super(stateDescriptor, connectionInternal); + } + + @Override + public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + assert walkFromDisconnectToAuthenticated == null; + + if (walkStateGraphContext.isWalksFinalState(getStateDescriptor())) { + // If this is the final state, then record the walk so far. + walkFromDisconnectToAuthenticated = walkStateGraphContext.getWalk(); + } + + connected = true; + return StateTransitionResult.Success.EMPTY_INSTANCE; + } + + @Override + public void resetState() { + connected = false; + } + } + + public static final class SaslAuthenticationStateDescriptor extends StateDescriptor { + private SaslAuthenticationStateDescriptor() { + super(SaslAuthenticationState.class, "RFC 6120 ยง 6"); + addSuccessor(AuthenticatedButUnboundStateDescriptor.class); + } + } + + private final class SaslAuthenticationState extends State { + private SaslAuthenticationState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { + super(stateDescriptor, connectionInternal); + } + + @Override + public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) + throws XMPPErrorException, SASLErrorException, IOException, SmackException, + InterruptedException { + prepareToWaitForFeaturesReceived(); + + LoginContext loginContext = walkStateGraphContext.getLoginContext(); + SASLMechanism usedSaslMechanism = authenticate(loginContext.username, loginContext.password, + config.getAuthzid(), getSSLSession()); + // authenticate() will only return if the SASL authentication was successful, but we also need to wait for + // the next round of stream features. + + waitForFeaturesReceived("server stream features after SASL authentication"); + + return new SaslAuthenticationSuccessResult(usedSaslMechanism); + } + } + + public static final class SaslAuthenticationSuccessResult extends StateTransitionResult.Success { + private final String saslMechanismName; + + private SaslAuthenticationSuccessResult(SASLMechanism usedSaslMechanism) { + super("SASL authentication successfull using " + usedSaslMechanism.getName()); + this.saslMechanismName = usedSaslMechanism.getName(); + } + + public String getSaslMechanismName() { + return saslMechanismName; + } + } + + public static final class AuthenticatedButUnboundStateDescriptor extends StateDescriptor { + private AuthenticatedButUnboundStateDescriptor() { + super(StateDescriptor.Property.multiVisitState); + addSuccessor(ResourceBindingStateDescriptor.class); + } + } + + public static final class ResourceBindingStateDescriptor extends StateDescriptor { + private ResourceBindingStateDescriptor() { + super(ResourceBindingState.class, "RFC 6120 ยง 7"); + addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); + } + } + + private final class ResourceBindingState extends State { + private ResourceBindingState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { + super(stateDescriptor, connectionInternal); + } + + @Override + public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) + throws XMPPErrorException, SASLErrorException, IOException, SmackException, + InterruptedException { + // Calling bindResourceAndEstablishSession() below requires the lastFeaturesReceived sync point to be signaled. + // Since we entered this state, the FSM has decided that the last features have been received, hence signal + // the sync point. + lastFeaturesReceived.reportSuccess(); + + LoginContext loginContext = walkStateGraphContext.getLoginContext(); + Resourcepart resource = bindResourceAndEstablishSession(loginContext.resource); + + // TODO: This should be a field in the Stream Management (SM) module. Here should be hook which the SM + // module can use to set the field instead. + streamResumed = false; + + return new ResourceBoundResult(resource, loginContext.resource); + } + } + + public static final class ResourceBoundResult extends StateTransitionResult.Success { + private final Resourcepart resource; + + private ResourceBoundResult(Resourcepart boundResource, Resourcepart requestedResource) { + super("Resource '" + boundResource + "' bound (requested: '" + requestedResource + "')"); + this.resource = boundResource; + } + + public Resourcepart getResource() { + return resource; + } + } + + private boolean compressionEnabled; + + @Override + public boolean isUsingCompression() { + return compressionEnabled; + } + + public static final class AuthenticatedAndResourceBoundStateDescriptor extends StateDescriptor { + private AuthenticatedAndResourceBoundStateDescriptor() { + super(AuthenticatedAndResourceBoundState.class, StateDescriptor.Property.finalState); + addSuccessor(InstantShutdownStateDescriptor.class); + addSuccessor(ShutdownStateDescriptor.class); + } + } + + private final class AuthenticatedAndResourceBoundState extends State { + private AuthenticatedAndResourceBoundState(StateDescriptor stateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + super(stateDescriptor, connectionInternal); + } + + @Override + public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) + throws NotConnectedException, InterruptedException { + if (walkFromDisconnectToAuthenticated != null) { + // If there was already a previous walk to ConnectedButUnauthenticated, then the context of the current + // walk must not start from the 'Disconnected' state. + assert walkStateGraphContext.getWalk().get(0).getStateDescriptor().getClass() + != DisconnectedStateDescriptor.class; + // Append the current walk to the previous one. + walkStateGraphContext.appendWalkTo(walkFromDisconnectToAuthenticated); + } else { + walkFromDisconnectToAuthenticated = new ArrayList<>( + walkStateGraphContext.getWalkLength() + 1); + walkStateGraphContext.appendWalkTo(walkFromDisconnectToAuthenticated); + } + walkFromDisconnectToAuthenticated.add(this); + + afterSuccessfulLogin(streamResumed); + + return StateTransitionResult.Success.EMPTY_INSTANCE; + } + + @Override + public void resetState() { + authenticated = false; + } + } + + static final class ShutdownStateDescriptor extends StateDescriptor { + private ShutdownStateDescriptor() { + super(ShutdownState.class); + addSuccessor(CloseConnectionStateDescriptor.class); + } + } + + private final class ShutdownState extends State { + private ShutdownState(StateDescriptor stateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + super(stateDescriptor, connectionInternal); + } + + @Override + public StateTransitionResult.TransitionImpossible isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { + ensureNotOnOurWayToAuthenticatedAndResourceBound(walkStateGraphContext); + return null; + } + + @Override + public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + closingStreamReceived.init(); + + boolean streamCloseIssued = outgoingElementsQueue.offerAndShutdown(StreamClose.INSTANCE); + + if (streamCloseIssued) { + activeTransport.notifyAboutNewOutgoingElements(); + + boolean successfullyReceivedStreamClose = waitForClosingStreamTagFromServer(); + + if (successfullyReceivedStreamClose) { + for (Iterator it = connectionInternal.getXmppInputOutputFilterBeginIterator(); it.hasNext();) { + XmppInputOutputFilter filter = it.next(); + filter.closeInputOutput(); + } + + // Closing the filters may produced new outgoing data. Notify the transport about it. + activeTransport.afterFiltersClosed(); + + for (Iterator it = connectionInternal.getXmppInputOutputFilterBeginIterator(); it.hasNext();) { + XmppInputOutputFilter filter = it.next(); + try { + filter.waitUntilInputOutputClosed(); + } catch (IOException | CertificateException | InterruptedException | SmackException e) { + LOGGER.log(Level.WARNING, "waitUntilInputOutputClosed() threw", e); + } + } + + // For correctness we set authenticated to false here, even though we will later again set it to + // false in the disconnected state. + authenticated = false; + } + } + + return StateTransitionResult.Success.EMPTY_INSTANCE; + } + } + + static final class InstantShutdownStateDescriptor extends StateDescriptor { + private InstantShutdownStateDescriptor() { + super(InstantShutdownState.class); + addSuccessor(CloseConnectionStateDescriptor.class); + } + } + + private static final class InstantShutdownState extends NoOpState { + private InstantShutdownState(ModularXmppClientToServerConnection connection, StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { + super(connection, stateDescriptor, connectionInternal); + } + + @Override + public StateTransitionResult.TransitionImpossible isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { + ensureNotOnOurWayToAuthenticatedAndResourceBound(walkStateGraphContext); + return null; + } + } + + private static final class CloseConnectionStateDescriptor extends StateDescriptor { + private CloseConnectionStateDescriptor() { + super(CloseConnectionState.class); + addSuccessor(DisconnectedStateDescriptor.class); + } + } + + private final class CloseConnectionState extends State { + private CloseConnectionState(StateDescriptor stateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + super(stateDescriptor, connectionInternal); + } + + @Override + public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + activeTransport.disconnect(); + activeTransport = null; + + authenticated = connected = false; + + return StateTransitionResult.Success.EMPTY_INSTANCE; + } + } + + public void addConnectionStateMachineListener(ConnectionStateMachineListener connectionStateMachineListener) { + connectionStateMachineListeners.add(connectionStateMachineListener); + } + + public boolean removeConnectionStateMachineListener(ConnectionStateMachineListener connectionStateMachineListener) { + return connectionStateMachineListeners.remove(connectionStateMachineListener); + } + + protected void invokeConnectionStateMachineListener(ConnectionStateEvent connectionStateEvent) { + if (connectionStateMachineListeners.isEmpty()) { + return; + } + + ASYNC_BUT_ORDERED.performAsyncButOrdered(this, () -> { + for (ConnectionStateMachineListener connectionStateMachineListener : connectionStateMachineListeners) { + connectionStateMachineListener.onConnectionStateEvent(connectionStateEvent, this); + } + }); + } + + @Override + public boolean isSecureConnection() { + final XmppClientToServerTransport transport = activeTransport; + if (transport == null) { + return false; + } + return transport.isTransportSecured(); + } + + @Override + protected void connectInternal() throws SmackException, IOException, XMPPException, InterruptedException { + WalkStateGraphContext walkStateGraphContext = buildNewWalkTo(ConnectedButUnauthenticatedStateDescriptor.class) + .build(); + walkStateGraph(walkStateGraphContext); + } + + protected Map getFilterStats() { + Collection filters; + synchronized (this) { + if (inputOutputFilters.isEmpty() && previousInputOutputFilters != null) { + filters = previousInputOutputFilters; + } else { + filters = inputOutputFilters; + } + } + + Map filterStats = new HashMap<>(filters.size()); + for (XmppInputOutputFilter xmppInputOutputFilter : filters) { + Object stats = xmppInputOutputFilter.getStats(); + String filterName = xmppInputOutputFilter.getFilterName(); + + filterStats.put(filterName, stats); + } + + return filterStats; + } + + public Stats getStats() { + Map, XmppClientToServerTransport.Stats> transportsStats = new HashMap<>( + transports.size()); + for (Map.Entry, XmppClientToServerTransport> entry : transports.entrySet()) { + XmppClientToServerTransport.Stats transportStats = entry.getValue().getStats(); + + transportsStats.put(entry.getKey(), transportStats); + } + + Map filterStats = getFilterStats(); + + return new Stats(transportsStats, filterStats); + } + + public static final class Stats { + public final Map, XmppClientToServerTransport.Stats> transportsStats; + public final Map filtersStats; + + private Stats(Map, XmppClientToServerTransport.Stats> transportsStats, + Map filtersStats) { + this.transportsStats = Collections.unmodifiableMap(transportsStats); + this.filtersStats = Collections.unmodifiableMap(filtersStats); + } + + public void appendStatsTo(Appendable appendable) throws IOException { + StringUtils.appendHeading(appendable, "Connection stats", '#').append('\n'); + + for (Map.Entry, XmppClientToServerTransport.Stats> entry : transportsStats.entrySet()) { + Class transportClass = entry.getKey(); + XmppClientToServerTransport.Stats stats = entry.getValue(); + + StringUtils.appendHeading(appendable, transportClass.getName()); + appendable.append(stats.toString()).append('\n'); + } + + for (Map.Entry entry : filtersStats.entrySet()) { + String filterName = entry.getKey(); + Object filterStats = entry.getValue(); + + StringUtils.appendHeading(appendable, filterName); + appendable.append(filterStats.toString()).append('\n'); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + try { + appendStatsTo(sb); + } catch (IOException e) { + // Should never happen. + throw new AssertionError(e); + } + return sb.toString(); + } + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionConfiguration.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionConfiguration.java new file mode 100644 index 000000000..a3bf16911 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionConfiguration.java @@ -0,0 +1,167 @@ +/** + * + * Copyright 2019-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.c2s; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.fsm.StateDescriptor; +import org.jivesoftware.smack.fsm.StateDescriptorGraph; +import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex; +import org.jivesoftware.smack.util.CollectionUtil; + +public final class ModularXmppClientToServerConnectionConfiguration extends ConnectionConfiguration { + + final Set moduleDescriptors; + + final GraphVertex initialStateDescriptorVertex; + + private ModularXmppClientToServerConnectionConfiguration(Builder builder) { + super(builder); + + moduleDescriptors = Collections.unmodifiableSet(CollectionUtil.newSetWith(builder.modulesDescriptors.values())); + + Set> backwardEdgeStateDescriptors = new HashSet<>(); + // Add backward edges from configured connection modules. Note that all state descriptors from module + // descriptors are backwards edges. + for (ModularXmppClientToServerConnectionModuleDescriptor moduleDescriptor : moduleDescriptors) { + Set> moduleStateDescriptors = moduleDescriptor.getStateDescriptors(); + backwardEdgeStateDescriptors.addAll(moduleStateDescriptors); + } + + try { + initialStateDescriptorVertex = StateDescriptorGraph.constructStateDescriptorGraph(backwardEdgeStateDescriptors); + } + catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException + | NoSuchMethodException | SecurityException e) { + // TODO: Depending on the exact exception thrown, this potentially indicates an invalid connection + // configuration, e.g. there is no edge from disconnected to connected. + throw new IllegalStateException(e); + } + } + + public void printStateGraphInDotFormat(PrintWriter pw, boolean breakStateName) { + StateDescriptorGraph.stateDescriptorGraphToDot(Collections.singleton(initialStateDescriptorVertex), pw, + breakStateName); + } + + public String getStateGraphInDotFormat() { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + + printStateGraphInDotFormat(pw, true); + + return sw.toString(); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder + extends ConnectionConfiguration.Builder { + + private final Map, ModularXmppClientToServerConnectionModuleDescriptor> modulesDescriptors = new HashMap<>(); + + private Builder() { + SmackConfiguration.addAllKnownModulesTo(this); + } + + @Override + public ModularXmppClientToServerConnectionConfiguration build() { + return new ModularXmppClientToServerConnectionConfiguration(this); + } + + void addModule(ModularXmppClientToServerConnectionModuleDescriptor connectionModule) { + Class moduleDescriptorClass = connectionModule.getClass(); + if (modulesDescriptors.containsKey(moduleDescriptorClass)) { + throw new IllegalArgumentException("A connection module for " + moduleDescriptorClass + " is already configured"); + } + modulesDescriptors.put(moduleDescriptorClass, connectionModule); + } + + @SuppressWarnings("unchecked") + public Builder addModule(Class moduleClass) { + Class[] declaredClasses = moduleClass.getDeclaredClasses(); + + Class builderClass = null; + for (Class declaredClass : declaredClasses) { + if (!ModularXmppClientToServerConnectionModuleDescriptor.Builder.class.isAssignableFrom(declaredClass)) { + continue; + } + + builderClass = (Class) declaredClass; + break; + } + + if (builderClass == null) { + throw new IllegalArgumentException( + "Found no builder for " + moduleClass + ". Delcared classes: " + Arrays.toString(declaredClasses)); + } + + return with(builderClass).buildModule(); + } + + public B with( + Class moduleDescriptorBuilderClass) { + Constructor moduleDescriptorBuilderCosntructor; + try { + moduleDescriptorBuilderCosntructor = moduleDescriptorBuilderClass.getDeclaredConstructor( + ModularXmppClientToServerConnectionConfiguration.Builder.class); + } catch (NoSuchMethodException | SecurityException e) { + throw new IllegalArgumentException(e); + } + + moduleDescriptorBuilderCosntructor.setAccessible(true); + + B moduleDescriptorBuilder; + try { + moduleDescriptorBuilder = moduleDescriptorBuilderCosntructor.newInstance(this); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + throw new IllegalArgumentException(e); + } + + return moduleDescriptorBuilder; + } + + public Builder removeModule(Class moduleClass) { + modulesDescriptors.remove(moduleClass); + return getThis(); + } + + public Builder removeAllModules() { + modulesDescriptors.clear(); + return getThis(); + } + + @Override + protected Builder getThis() { + return this; + } + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionModule.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionModule.java new file mode 100644 index 000000000..eef942ebb --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionModule.java @@ -0,0 +1,40 @@ +/** + * + * Copyright 2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.c2s; + +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; + +public abstract class ModularXmppClientToServerConnectionModule { + + protected final MD moduleDescriptor; + + protected final ModularXmppClientToServerConnectionInternal connectionInternal; + + protected ModularXmppClientToServerConnectionModule(MD moduleDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + this.moduleDescriptor = moduleDescriptor; + this.connectionInternal = connectionInternal; + } + + public MD getModuleDescriptor() { + return moduleDescriptor; + } + + protected XmppClientToServerTransport getTransport() { + return null; + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionModuleDescriptor.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionModuleDescriptor.java new file mode 100644 index 000000000..2c1054a36 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/ModularXmppClientToServerConnectionModuleDescriptor.java @@ -0,0 +1,48 @@ +/** + * + * Copyright 2019-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.c2s; + +import java.util.Set; + +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.fsm.StateDescriptor; + +public abstract class ModularXmppClientToServerConnectionModuleDescriptor { + + protected abstract Set> getStateDescriptors(); + + protected abstract ModularXmppClientToServerConnectionModule constructXmppConnectionModule( + ModularXmppClientToServerConnectionInternal connectionInternal); + + public abstract static class Builder { + private final ModularXmppClientToServerConnectionConfiguration.Builder connectionConfigurationBuilder; + + protected Builder(ModularXmppClientToServerConnectionConfiguration.Builder connectionConfigurationBuilder) { + this.connectionConfigurationBuilder = connectionConfigurationBuilder; + } + + protected abstract ModularXmppClientToServerConnectionModuleDescriptor build(); + + public ModularXmppClientToServerConnectionConfiguration.Builder buildModule() { + ModularXmppClientToServerConnectionModuleDescriptor moduleDescriptor = build(); + connectionConfigurationBuilder.addModule(moduleDescriptor); + return connectionConfigurationBuilder; + } + + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/XmppClientToServerTransport.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/XmppClientToServerTransport.java new file mode 100644 index 000000000..d68440fc3 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/XmppClientToServerTransport.java @@ -0,0 +1,76 @@ +/** + * + * Copyright 2019-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.c2s; + +import java.util.List; + +import javax.net.ssl.SSLSession; + +import org.jivesoftware.smack.SmackFuture; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; + +public abstract class XmppClientToServerTransport { + + protected final ModularXmppClientToServerConnectionInternal connectionInternal; + + protected XmppClientToServerTransport(ModularXmppClientToServerConnectionInternal connectionInternal) { + this.connectionInternal = connectionInternal; + } + + protected abstract void resetDiscoveredConnectionEndpoints(); + + protected abstract List> lookupConnectionEndpoints(); + + protected abstract void loadConnectionEndpoints(LookupConnectionEndpointsSuccess lookupConnectionEndpointsSuccess); + + /** + * 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. + */ + protected abstract void afterFiltersClosed(); + + /** + * Called by the CloseConnection state. + */ + protected abstract void disconnect(); + + protected abstract void notifyAboutNewOutgoingElements(); + + public abstract SSLSession getSslSession(); + + public abstract boolean isConnected(); + + public boolean isTransportSecured() { + return getSslSession() != null; + } + + public abstract Stats getStats(); + + public abstract static class Stats { + } + + protected interface LookupConnectionEndpointsResult { + } + + protected interface LookupConnectionEndpointsSuccess extends LookupConnectionEndpointsResult { + } + + public interface LookupConnectionEndpointsFailed extends LookupConnectionEndpointsResult { + // TODO: Add something like getExceptions() or getConnectionExceptions()? + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/ModularXmppClientToServerConnectionInternal.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/ModularXmppClientToServerConnectionInternal.java new file mode 100644 index 000000000..57452f270 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/ModularXmppClientToServerConnectionInternal.java @@ -0,0 +1,125 @@ +/** + * + * Copyright 2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.ListIterator; +import java.util.Queue; + +import org.jivesoftware.smack.AbstractXMPPConnection.SmackTlsContext; +import org.jivesoftware.smack.SmackException.ConnectionUnexpectedTerminatedException; +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.SmackReactor; +import org.jivesoftware.smack.SmackReactor.ChannelSelectedCallback; +import org.jivesoftware.smack.XMPPException.FailedNonzaException; +import org.jivesoftware.smack.XmppInputOutputFilter; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; +import org.jivesoftware.smack.c2s.XmppClientToServerTransport; +import org.jivesoftware.smack.debugger.SmackDebugger; +import org.jivesoftware.smack.fsm.ConnectionStateEvent; +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.xml.XmlPullParser; + +public abstract class ModularXmppClientToServerConnectionInternal { + + private final SmackReactor reactor; + + public final ModularXmppClientToServerConnection connection; + + public final SmackDebugger smackDebugger; + + public final Queue outgoingElementsQueue; + + public ModularXmppClientToServerConnectionInternal(ModularXmppClientToServerConnection connection, SmackReactor reactor, + SmackDebugger smackDebugger, Queue outgoingElementsQueue) { + this.connection = connection; + this.reactor = reactor; + this.smackDebugger = smackDebugger; + this.outgoingElementsQueue = outgoingElementsQueue; + } + + public SelectionKey registerWithSelector(SelectableChannel channel, int ops, ChannelSelectedCallback callback) + throws ClosedChannelException { + return reactor.registerWithSelector(channel, ops, callback); + } + + public void setInterestOps(SelectionKey selectionKey, int interestOps) { + reactor.setInterestOps(selectionKey, interestOps); + } + + public final void withSmackDebugger(Consumer smackDebuggerConsumer) { + if (smackDebugger == null) { + return; + } + + smackDebuggerConsumer.accept(smackDebugger); + } + + public abstract XmlEnvironment getOutgoingStreamXmlEnvironment(); + + // TODO: The incomingElement parameter was previously of type TopLevelStreamElement, but I believe it has to be + // of type string. But would this also work for BOSH or WebSocket? + public abstract void parseAndProcessElement(String wrappedCompleteIncomingElement); + + public abstract void notifyConnectionError(Exception e); + + public abstract void onStreamOpen(XmlPullParser parser); + + public abstract void onStreamClosed(); + + public abstract void fireFirstLevelElementSendListeners(TopLevelStreamElement element); + + public abstract void invokeConnectionStateMachineListener(ConnectionStateEvent connectionStateEvent); + + public abstract void addXmppInputOutputFilter(XmppInputOutputFilter xmppInputOutputFilter); + + public abstract ListIterator getXmppInputOutputFilterBeginIterator(); + + public abstract ListIterator getXmppInputOutputFilterEndIterator(); + + public abstract void newStreamOpenWaitForFeaturesSequence(String waitFor) throws InterruptedException, + ConnectionUnexpectedTerminatedException, NoResponseException, NotConnectedException; + + public abstract SmackTlsContext getSmackTlsContext() + throws KeyManagementException, NoSuchAlgorithmException, CertificateException, IOException, + UnrecoverableKeyException, KeyStoreException, NoSuchProviderException; + + public abstract SN sendAndWaitForResponse(Nonza nonza, + Class successNonzaClass, Class failedNonzaClass) + throws NoResponseException, NotConnectedException, FailedNonzaException, InterruptedException; + + public abstract void asyncGo(Runnable runnable); + + public abstract Exception getCurrentConnectionException(); + + public abstract void setCompressionEnabled(boolean compressionEnabled); + + public abstract void setTransport(XmppClientToServerTransport xmppTransport); +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/WalkStateGraphContext.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/WalkStateGraphContext.java new file mode 100644 index 000000000..ed2895d97 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/WalkStateGraphContext.java @@ -0,0 +1,179 @@ +/** + * + * Copyright 2018-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.c2s.internal; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.AuthenticatedAndResourceBoundStateDescriptor; +import org.jivesoftware.smack.fsm.LoginContext; +import org.jivesoftware.smack.fsm.State; +import org.jivesoftware.smack.fsm.StateDescriptor; +import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex; +import org.jivesoftware.smack.fsm.StateTransitionResult; +import org.jivesoftware.smack.util.CollectionUtil; +import org.jivesoftware.smack.util.Objects; + +import org.jxmpp.jid.parts.Resourcepart; + +public final class WalkStateGraphContext { + private final Class initialStateClass; + private final Class finalStateClass; + private final Class mandatoryIntermediateState; + private final LoginContext loginContext; + + private final List walkedStateGraphPath = new ArrayList<>(); + + /** + * A linked Map of failed States with their reason as value. + */ + final Map failedStates = new LinkedHashMap<>(); + + boolean mandatoryIntermediateStateHandled; + + WalkStateGraphContext(Builder builder) { + initialStateClass = builder.initialStateClass; + finalStateClass = builder.finalStateClass; + mandatoryIntermediateState = builder.mandatoryIntermediateState; + loginContext = builder.loginContext; + } + + public void recordWalkTo(State state) { + walkedStateGraphPath.add(state); + } + + public boolean isWalksFinalState(StateDescriptor stateDescriptor) { + return stateDescriptor.getClass() == finalStateClass; + } + + public boolean isFinalStateAuthenticatedAndResourceBound() { + return finalStateClass == AuthenticatedAndResourceBoundStateDescriptor.class; + } + + public GraphVertex maybeReturnMandatoryImmediateState(List> outgoingStateEdges) { + for (GraphVertex outgoingStateVertex : outgoingStateEdges) { + if (outgoingStateVertex.getElement().getStateDescriptor().getClass() == mandatoryIntermediateState) { + mandatoryIntermediateStateHandled = true; + return outgoingStateVertex; + } + } + + return null; + } + + public List getWalk() { + return CollectionUtil.newListWith(walkedStateGraphPath); + } + + public int getWalkLength() { + return walkedStateGraphPath.size(); + } + + public void appendWalkTo(List walk) { + walk.addAll(walkedStateGraphPath); + } + + public LoginContext getLoginContext() { + return loginContext; + } + + public boolean stateAlreadyVisited(State state) { + return walkedStateGraphPath.contains(state); + } + + public void recordFailedState(State state, StateTransitionResult stateTransitionResult) { + failedStates.put(state, stateTransitionResult); + } + + public Map getFailedStates() { + return new HashMap<>(failedStates); + } + + /** + * Check if the way to the final state via the given successor state that would loop, i.e., lead over the initial state and + * thus from a cycle. + * + * @param successorStateVertex the successor state to use on the way. + * @return true if it would loop, false otherwise. + */ + public boolean wouldCauseCycle(GraphVertex successorStateVertex) { + Set> visited = new HashSet<>(); + return wouldCycleRecursive(successorStateVertex, visited); + } + + private boolean wouldCycleRecursive(GraphVertex stateVertex, Set> visited) { + Class stateVertexClass = stateVertex.getElement().getStateDescriptor().getClass(); + + if (stateVertexClass == initialStateClass) { + return true; + } + if (finalStateClass == stateVertexClass || visited.contains(stateVertexClass)) { + return false; + } + + visited.add(stateVertexClass); + + for (GraphVertex successorStateVertex : stateVertex.getOutgoingEdges()) { + boolean cycle = wouldCycleRecursive(successorStateVertex, visited); + if (cycle) { + return true; + } + } + + return false; + } + + public static Builder builder(Class initialStateClass, Class finalStateClass) { + return new Builder(initialStateClass, finalStateClass); + } + + public static final class Builder { + private final Class initialStateClass; + private final Class finalStateClass; + private Class mandatoryIntermediateState; + private LoginContext loginContext; + + private Builder(Class initialStateClass, Class finalStateClass) { + this.initialStateClass = Objects.requireNonNull(initialStateClass); + this.finalStateClass = Objects.requireNonNull(finalStateClass); + } + + public Builder withMandatoryIntermediateState(Class mandatoryIntermedidateState) { + this.mandatoryIntermediateState = mandatoryIntermedidateState; + return this; + } + + public Builder withLoginContext(String username, String password, Resourcepart resource) { + LoginContext loginContext = new LoginContext(username, password, resource); + return withLoginContext(loginContext); + } + + public Builder withLoginContext(LoginContext loginContext) { + this.loginContext = loginContext; + return this; + } + + public WalkStateGraphContext build() { + return new WalkStateGraphContext(this); + } + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/package-info.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/package-info.java new file mode 100644 index 000000000..9705f1cb1 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/internal/package-info.java @@ -0,0 +1,21 @@ +/** + * + * Copyright 2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Smack's internal API for client-to-server (c2s) connections. + */ +package org.jivesoftware.smack.c2s.internal; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/c2s/package-info.java b/smack-core/src/main/java/org/jivesoftware/smack/c2s/package-info.java new file mode 100644 index 000000000..2c97e4c92 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/c2s/package-info.java @@ -0,0 +1,21 @@ +/** + * + * Copyright 2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Smack's (new) API for client-to-server (c2s) connections. + */ +package org.jivesoftware.smack.c2s; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compression/CompressionModule.java b/smack-core/src/main/java/org/jivesoftware/smack/compression/CompressionModule.java new file mode 100644 index 000000000..2a018899b --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/compression/CompressionModule.java @@ -0,0 +1,132 @@ +/** + * + * Copyright 2018-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.compression; + +import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.SmackException.ConnectionUnexpectedTerminatedException; +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.XMPPException.FailedNonzaException; +import org.jivesoftware.smack.XmppInputOutputFilter; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.AuthenticatedButUnboundStateDescriptor; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.ResourceBindingStateDescriptor; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModule; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.c2s.internal.WalkStateGraphContext; +import org.jivesoftware.smack.compress.packet.Compress; +import org.jivesoftware.smack.compress.packet.Compressed; +import org.jivesoftware.smack.compress.packet.Failure; +import org.jivesoftware.smack.fsm.State; +import org.jivesoftware.smack.fsm.StateDescriptor; +import org.jivesoftware.smack.fsm.StateTransitionResult; + +public class CompressionModule extends ModularXmppClientToServerConnectionModule { + + protected CompressionModule(CompressionModuleDescriptor moduleDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + super(moduleDescriptor, connectionInternal); + } + + public static final class CompressionStateDescriptor extends StateDescriptor { + private CompressionStateDescriptor() { + super(CompressionModule.CompressionState.class, 138); + addPredeccessor(AuthenticatedButUnboundStateDescriptor.class); + addSuccessor(AuthenticatedButUnboundStateDescriptor.class); + declarePrecedenceOver(ResourceBindingStateDescriptor.class); + } + + @Override + protected CompressionModule.CompressionState constructState(ModularXmppClientToServerConnectionInternal connectionInternal) { + CompressionModule compressionModule = connectionInternal.connection.getConnectionModuleFor(CompressionModuleDescriptor.class); + return compressionModule.constructCompressionState(this, connectionInternal); + } + } + + private static final class CompressionState extends State { + private XmppCompressionFactory selectedCompressionFactory; + private XmppInputOutputFilter usedXmppInputOutputCompressionFitler; + + private CompressionState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { + super(stateDescriptor, connectionInternal); + } + + @Override + public StateTransitionResult.TransitionImpossible isTransitionToPossible( + WalkStateGraphContext walkStateGraphContext) { + final ConnectionConfiguration config = connectionInternal.connection.getConfiguration(); + if (!config.isCompressionEnabled()) { + return new StateTransitionResult.TransitionImpossibleReason("Stream compression disabled by connection configuration"); + } + + Compress.Feature compressFeature = connectionInternal.connection.getFeature(Compress.Feature.ELEMENT, Compress.NAMESPACE); + if (compressFeature == null) { + return new StateTransitionResult.TransitionImpossibleReason("Stream compression not supported or enabled by service"); + } + + selectedCompressionFactory = XmppCompressionManager.getBestFactory(compressFeature); + if (selectedCompressionFactory == null) { + return new StateTransitionResult.TransitionImpossibleReason( + "No matching compression factory for " + compressFeature.getMethods()); + } + + usedXmppInputOutputCompressionFitler = selectedCompressionFactory.fabricate(config); + + return null; + } + + @Override + public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) + throws NoResponseException, NotConnectedException, FailedNonzaException, InterruptedException, + ConnectionUnexpectedTerminatedException { + final String compressionMethod = selectedCompressionFactory.getCompressionMethod(); + connectionInternal.sendAndWaitForResponse(new Compress(compressionMethod), Compressed.class, Failure.class); + + connectionInternal.addXmppInputOutputFilter(usedXmppInputOutputCompressionFitler); + + connectionInternal.newStreamOpenWaitForFeaturesSequence("server stream features after compression enabled"); + + connectionInternal.setCompressionEnabled(true); + + return new CompressionTransitionSuccessResult(compressionMethod); + } + + @Override + public void resetState() { + selectedCompressionFactory = null; + usedXmppInputOutputCompressionFitler = null; + connectionInternal.setCompressionEnabled(false); + } + } + + public static final class CompressionTransitionSuccessResult extends StateTransitionResult.Success { + private final String compressionMethod; + + private CompressionTransitionSuccessResult(String compressionMethod) { + super(compressionMethod + " compression enabled"); + this.compressionMethod = compressionMethod; + } + + public String getCompressionMethod() { + return compressionMethod; + } + } + + public CompressionState constructCompressionState(CompressionStateDescriptor compressionStateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + return new CompressionState(compressionStateDescriptor, connectionInternal); + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compression/CompressionModuleDescriptor.java b/smack-core/src/main/java/org/jivesoftware/smack/compression/CompressionModuleDescriptor.java new file mode 100644 index 000000000..3cf43ee79 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/compression/CompressionModuleDescriptor.java @@ -0,0 +1,54 @@ +/** + * + * Copyright 2018-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.compression; + +import java.util.Collections; +import java.util.Set; + +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModuleDescriptor; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.fsm.StateDescriptor; + +public class CompressionModuleDescriptor extends ModularXmppClientToServerConnectionModuleDescriptor { + + private static final CompressionModuleDescriptor INSTANCE = new CompressionModuleDescriptor(); + + @Override + protected Set> getStateDescriptors() { + return Collections.singleton(CompressionModule.CompressionStateDescriptor.class); + } + + @Override + protected CompressionModule constructXmppConnectionModule( + ModularXmppClientToServerConnectionInternal connectionInternal) { + return new CompressionModule(this, connectionInternal); + } + + public static final class Builder extends ModularXmppClientToServerConnectionModuleDescriptor.Builder { + + private Builder(ModularXmppClientToServerConnectionConfiguration.Builder connectionConfigurationBuilder) { + super(connectionConfigurationBuilder); + } + + @Override + protected ModularXmppClientToServerConnectionModuleDescriptor build() { + return INSTANCE; + } + + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/compression/zlib/ZlibXmppCompressionFactory.java b/smack-core/src/main/java/org/jivesoftware/smack/compression/zlib/ZlibXmppCompressionFactory.java index cbb295c1b..096147931 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/compression/zlib/ZlibXmppCompressionFactory.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/compression/zlib/ZlibXmppCompressionFactory.java @@ -230,6 +230,11 @@ public final class ZlibXmppCompressionFactory extends XmppCompressionFactory { public Stats getStats() { return new Stats(this); } + + @Override + public String getFilterName() { + return "Compression (zlib)"; + } } public static final class Stats { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/AbstractXmppStateMachineConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/AbstractXmppStateMachineConnection.java deleted file mode 100644 index 2c7168148..000000000 --- a/smack-core/src/main/java/org/jivesoftware/smack/fsm/AbstractXmppStateMachineConnection.java +++ /dev/null @@ -1,806 +0,0 @@ -/** - * - * Copyright 2018-2019 Florian Schmaus - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jivesoftware.smack.fsm; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; - -import javax.net.ssl.SSLSession; - -import org.jivesoftware.smack.AbstractXMPPConnection; -import org.jivesoftware.smack.ConnectionConfiguration; -import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.SmackException.ConnectionUnexpectedTerminatedException; -import org.jivesoftware.smack.SmackException.NoResponseException; -import org.jivesoftware.smack.SmackException.NotConnectedException; -import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.XMPPException.FailedNonzaException; -import org.jivesoftware.smack.XMPPException.StreamErrorException; -import org.jivesoftware.smack.XMPPException.XMPPErrorException; -import org.jivesoftware.smack.XmppInputOutputFilter; -import org.jivesoftware.smack.compress.packet.Compress; -import org.jivesoftware.smack.compress.packet.Compressed; -import org.jivesoftware.smack.compress.packet.Failure; -import org.jivesoftware.smack.compression.XmppCompressionFactory; -import org.jivesoftware.smack.compression.XmppCompressionManager; -import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex; -import org.jivesoftware.smack.packet.IQ; -import org.jivesoftware.smack.packet.Message; -import org.jivesoftware.smack.packet.Presence; -import org.jivesoftware.smack.packet.StreamError; -import org.jivesoftware.smack.parsing.SmackParsingException; -import org.jivesoftware.smack.sasl.SASLErrorException; -import org.jivesoftware.smack.sasl.SASLMechanism; -import org.jivesoftware.smack.util.Objects; -import org.jivesoftware.smack.util.PacketParserUtils; -import org.jivesoftware.smack.xml.XmlPullParser; -import org.jivesoftware.smack.xml.XmlPullParserException; - -import org.jxmpp.jid.parts.Resourcepart; - -public abstract class AbstractXmppStateMachineConnection extends AbstractXMPPConnection { - - private final List connectionStateMachineListeners = new CopyOnWriteArrayList<>(); - - private boolean featuresReceived; - - protected boolean streamResumed; - - private GraphVertex currentStateVertex; - - private List walkFromDisconnectToAuthenticated; - - private final List inputOutputFilters = new CopyOnWriteArrayList<>(); - private List previousInputOutputFilters; - - protected AbstractXmppStateMachineConnection(ConnectionConfiguration configuration, GraphVertex initialStateDescriptorVertex) { - super(configuration); - currentStateVertex = StateDescriptorGraph.convertToStateGraph(initialStateDescriptorVertex, this); - } - - @Override - protected void loginInternal(String username, String password, Resourcepart resource) - throws XMPPException, SmackException, IOException, InterruptedException { - WalkStateGraphContext walkStateGraphContext = buildNewWalkTo(AuthenticatedAndResourceBoundStateDescriptor.class) - .withLoginContext(username, password, resource) - .build(); - walkStateGraph(walkStateGraphContext); - } - - protected static WalkStateGraphContextBuilder buildNewWalkTo(Class finalStateClass) { - return new WalkStateGraphContextBuilder(finalStateClass); - } - - protected static final class WalkStateGraphContext { - private final Class finalStateClass; - private final Class mandatoryIntermediateState; - private final LoginContext loginContext; - - private final List walkedStateGraphPath = new ArrayList<>(); - - /** - * A linked Map of failed States with their reason as value. - */ - private final Map failedStates = new LinkedHashMap<>(); - - private boolean mandatoryIntermediateStateHandled; - - private WalkStateGraphContext(Class finalStateClass, Class mandatoryIntermedidateState, LoginContext loginContext) { - this.finalStateClass = Objects.requireNonNull(finalStateClass); - this.mandatoryIntermediateState = mandatoryIntermedidateState; - this.loginContext = loginContext; - } - - public boolean isFinalStateAuthenticatedAndResourceBound() { - return finalStateClass == AuthenticatedAndResourceBoundStateDescriptor.class; - } - } - - protected static final class WalkStateGraphContextBuilder { - private final Class finalStateClass; - private Class mandatoryIntermedidateState; - private LoginContext loginContext; - - private WalkStateGraphContextBuilder(Class finalStateClass) { - this.finalStateClass = finalStateClass; - } - - public WalkStateGraphContextBuilder withMandatoryIntermediateState(Class mandatoryIntermedidateState) { - this.mandatoryIntermedidateState = mandatoryIntermedidateState; - return this; - } - - public WalkStateGraphContextBuilder withLoginContext(String username, String password, Resourcepart resource) { - LoginContext loginContext = new LoginContext(username, password, resource); - return withLoginContext(loginContext); - } - - public WalkStateGraphContextBuilder withLoginContext(LoginContext loginContext) { - this.loginContext = loginContext; - return this; - } - - public WalkStateGraphContext build() { - return new WalkStateGraphContext(finalStateClass, mandatoryIntermedidateState, loginContext); - } - } - - protected final void walkStateGraph(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, SASLErrorException, - FailedNonzaException, IOException, SmackException, InterruptedException { - // Save a copy of the current state - GraphVertex previousStateVertex = currentStateVertex; - try { - walkStateGraphInternal(walkStateGraphContext); - } - catch (XMPPErrorException | SASLErrorException | FailedNonzaException | IOException | SmackException - | InterruptedException e) { - currentStateVertex = previousStateVertex; - // Reset that state. - State revertedState = currentStateVertex.getElement(); - invokeConnectionStateMachineListener(new ConnectionStateEvent.StateRevertBackwardsWalk(revertedState)); - revertedState.resetState(); - throw e; - } - } - - private void walkStateGraphInternal(WalkStateGraphContext walkStateGraphContext) - throws XMPPErrorException, SASLErrorException, IOException, SmackException, InterruptedException, FailedNonzaException { - // Save a copy of the current state - final GraphVertex initialStateVertex = currentStateVertex; - final State initialState = initialStateVertex.getElement(); - final StateDescriptor initialStateDescriptor = initialState.getStateDescriptor(); - - walkStateGraphContext.walkedStateGraphPath.add(initialState); - - if (initialStateDescriptor.getClass() == walkStateGraphContext.finalStateClass) { - // If this is used as final state, then it should be marked as such. - assert initialStateDescriptor.isFinalState(); - - // We reached the final state. - invokeConnectionStateMachineListener(new ConnectionStateEvent.FinalStateReached(initialState)); - return; - } - - - List> outgoingStateEdges = currentStateVertex.getOutgoingEdges(); - - // See if we need to handle mandatory intermediate states. - if (walkStateGraphContext.mandatoryIntermediateState != null && !walkStateGraphContext.mandatoryIntermediateStateHandled) { - // Check if outgoingStateEdges contains the mandatory intermediate state. - GraphVertex mandatoryIntermediateStateVertex = null; - for (GraphVertex outgoingStateVertex : outgoingStateEdges) { - if (outgoingStateVertex.getElement().getStateDescriptor().getClass() == walkStateGraphContext.mandatoryIntermediateState) { - mandatoryIntermediateStateVertex = outgoingStateVertex; - break; - } - } - - if (mandatoryIntermediateStateVertex != null) { - walkStateGraphContext.mandatoryIntermediateStateHandled = true; - TransitionReason reason = attemptEnterState(mandatoryIntermediateStateVertex, walkStateGraphContext); - if (reason instanceof TransitionSuccessResult) { - walkStateGraph(walkStateGraphContext); - return; - } - - // We could not enter a mandatory intermediate state. Throw here. - throw new StateMachineException.SmackMandatoryStateFailedException( - mandatoryIntermediateStateVertex.getElement(), reason); - } - } - - for (Iterator> it = outgoingStateEdges.iterator(); it.hasNext();) { - GraphVertex successorStateVertex = it.next(); - State successorState = successorStateVertex.getElement(); - TransitionReason reason = attemptEnterState(successorStateVertex, walkStateGraphContext); - if (reason instanceof TransitionSuccessResult) { - break; - } - - // If attemptEnterState did not throw and did not return a value of type TransitionSuccessResult, then we - // just record this value and go on from there. Note that reason may be null, which is returned by - // attemptEnterState in case the state was already successfully handled. If this is the case, then we don't - // record it. - if (reason != null) { - walkStateGraphContext.failedStates.put(successorState, reason); - } - - if (!it.hasNext()) { - throw new StateMachineException.SmackStateGraphDeadEndException(walkStateGraphContext.walkedStateGraphPath, walkStateGraphContext.failedStates); - } - } - - // Walk the state graph by recursion. - walkStateGraph(walkStateGraphContext); - } - - private TransitionReason attemptEnterState(GraphVertex successorStateVertex, - WalkStateGraphContext walkStateGraphContext) - throws SmackException, XMPPErrorException, SASLErrorException, IOException, InterruptedException, FailedNonzaException { - final State successorState = successorStateVertex.getElement(); - final StateDescriptor successorStateDescriptor = successorState.getStateDescriptor(); - - if (!successorStateDescriptor.isMultiVisitState() && walkStateGraphContext.walkedStateGraphPath.contains(successorState)) { - // This can happen if a state leads back to the state where it originated from. See for example the - // 'Compression' state. We return 'null' here to signal that the state can safely be ignored. - return null; - } - - if (successorStateDescriptor.isNotImplemented()) { - TransitionImpossibleBecauseNotImplemented transtionImpossibleBecauseNotImplemented = new TransitionImpossibleBecauseNotImplemented( - successorStateDescriptor); - invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionNotPossible(successorState, - transtionImpossibleBecauseNotImplemented)); - return transtionImpossibleBecauseNotImplemented; - } - - final TransitionIntoResult transitionIntoResult; - try { - TransitionImpossibleReason transitionImpossibleReason = successorState.isTransitionToPossible(walkStateGraphContext); - if (transitionImpossibleReason != null) { - invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionNotPossible(successorState, - transitionImpossibleReason)); - return transitionImpossibleReason; - } - - invokeConnectionStateMachineListener(new ConnectionStateEvent.AboutToTransitionInto(successorState)); - transitionIntoResult = successorState.transitionInto(walkStateGraphContext); - } catch (SmackException | XMPPErrorException | SASLErrorException | IOException | InterruptedException - | FailedNonzaException e) { - // TODO Document why this is required given that there is another call site of resetState(). - invokeConnectionStateMachineListener(new ConnectionStateEvent.StateRevertBackwardsWalk(successorState)); - successorState.resetState(); - throw e; - } - if (transitionIntoResult instanceof TransitionFailureResult) { - TransitionFailureResult transitionFailureResult = (TransitionFailureResult) transitionIntoResult; - invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionFailed(successorState, transitionFailureResult)); - return transitionIntoResult; - } - - // If transitionIntoResult is not an instance of TransitionFailureResult, then it has to be of type - // TransitionSuccessResult. - TransitionSuccessResult transitionSuccessResult = (TransitionSuccessResult) transitionIntoResult; - - currentStateVertex = successorStateVertex; - invokeConnectionStateMachineListener(new ConnectionStateEvent.SuccessfullyTransitionedInto(successorState, - transitionSuccessResult)); - - return transitionSuccessResult; - } - - protected abstract SSLSession getSSLSession(); - - @Override - protected void afterFeaturesReceived() { - featuresReceived = true; - synchronized (this) { - notifyAll(); - } - } - - protected final void parseAndProcessElement(String element) throws XmlPullParserException, IOException, - InterruptedException, StreamErrorException, SmackException, SmackParsingException { - XmlPullParser parser = PacketParserUtils.getParserFor(element); - - // Skip the enclosing stream open what is guaranteed to be there. - parser.next(); - - XmlPullParser.Event event = parser.getEventType(); - outerloop: while (true) { - switch (event) { - case START_ELEMENT: - final String name = parser.getName(); - // Note that we don't handle "stream" here as it's done in the splitter. - switch (name) { - case Message.ELEMENT: - case IQ.IQ_ELEMENT: - case Presence.ELEMENT: - try { - parseAndProcessStanza(parser); - } finally { - // TODO: Here would be the following stream management code. - // clientHandledStanzasCount = SMUtils.incrementHeight(clientHandledStanzasCount); - } - break; - case "error": - StreamError streamError = PacketParserUtils.parseStreamError(parser, null); - saslFeatureReceived.reportFailure(new StreamErrorException(streamError)); - throw new StreamErrorException(streamError); - case "features": - parseFeatures(parser); - afterFeaturesReceived(); - break; - default: - parseAndProcessNonza(parser); - break; - } - break; - case END_DOCUMENT: - break outerloop; - default: // fall out - } - event = parser.next(); - } - } - - protected synchronized void prepareToWaitForFeaturesReceived() { - featuresReceived = false; - } - - protected void waitForFeaturesReceived(String waitFor) - throws InterruptedException, ConnectionUnexpectedTerminatedException, NoResponseException { - long waitStartMs = System.currentTimeMillis(); - long timeoutMs = getReplyTimeout(); - synchronized (this) { - while (!featuresReceived && currentConnectionException == null) { - long remainingWaitMs = timeoutMs - (System.currentTimeMillis() - waitStartMs); - if (remainingWaitMs <= 0) { - throw NoResponseException.newWith(this, waitFor); - } - wait(remainingWaitMs); - } - if (currentConnectionException != null) { - throw new SmackException.ConnectionUnexpectedTerminatedException(currentConnectionException); - } - } - } - - protected void newStreamOpenWaitForFeaturesSequence(String waitFor) throws InterruptedException, - ConnectionUnexpectedTerminatedException, NoResponseException, NotConnectedException { - prepareToWaitForFeaturesReceived(); - sendStreamOpen(); - waitForFeaturesReceived(waitFor); - } - - protected final void addXmppInputOutputFilter(XmppInputOutputFilter xmppInputOutputFilter) { - inputOutputFilters.add(0, xmppInputOutputFilter); - } - - protected final ListIterator getXmppInputOutputFilterBeginIterator() { - return inputOutputFilters.listIterator(); - } - - protected final ListIterator getXmppInputOutputFilterEndIterator() { - return inputOutputFilters.listIterator(inputOutputFilters.size()); - } - - protected final synchronized List getFilterStats() { - Collection filters; - if (inputOutputFilters.isEmpty() && previousInputOutputFilters != null) { - filters = previousInputOutputFilters; - } else { - filters = inputOutputFilters; - } - - List filterStats = new ArrayList<>(filters.size()); - for (XmppInputOutputFilter xmppInputOutputFilter : filters) { - Object stats = xmppInputOutputFilter.getStats(); - if (stats != null) { - filterStats.add(stats); - } - } - - return Collections.unmodifiableList(filterStats); - } - - protected abstract class State { - private final StateDescriptor stateDescriptor; - - protected State(StateDescriptor stateDescriptor) { - this.stateDescriptor = stateDescriptor; - } - - /** - * Check if the state should be activated. - * - * @param walkStateGraphContext the context of the current state graph walk. - * @return null if the state should be activated. - * @throws SmackException in case a Smack exception occurs. - */ - protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) throws SmackException { - return null; - } - - protected abstract TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) - throws XMPPErrorException, SASLErrorException, IOException, SmackException, InterruptedException, FailedNonzaException; - - StateDescriptor getStateDescriptor() { - return stateDescriptor; - } - - protected void resetState() { - } - - @Override - public String toString() { - return "State " + stateDescriptor + ' ' + AbstractXmppStateMachineConnection.this; - } - - protected final void ensureNotOnOurWayToAuthenticatedAndResourceBound(WalkStateGraphContext walkStateGraphContext) { - if (walkStateGraphContext.isFinalStateAuthenticatedAndResourceBound()) { - throw new IllegalStateException( - "Smack should never attempt to reach the authenticated and resource bound state over " + this - + ". This is probably a programming error within Smack, please report it to the develoeprs."); - } - } - } - - abstract static class TransitionReason { - public final String reason; - private TransitionReason(String reason) { - this.reason = reason; - } - - @Override - public final String toString() { - return reason; - } - } - - protected static class TransitionImpossibleReason extends TransitionReason { - public TransitionImpossibleReason(String reason) { - super(reason); - } - } - - protected static class TransitionImpossibleBecauseNotImplemented extends TransitionImpossibleReason { - public TransitionImpossibleBecauseNotImplemented(StateDescriptor stateDescriptor) { - super(stateDescriptor.getFullStateName(false) + " is not implemented (yet)"); - } - } - - protected abstract static class TransitionIntoResult extends TransitionReason { - public TransitionIntoResult(String reason) { - super(reason); - } - } - - public static class TransitionSuccessResult extends TransitionIntoResult { - - public static final TransitionSuccessResult EMPTY_INSTANCE = new TransitionSuccessResult(); - - private TransitionSuccessResult() { - super(""); - } - - public TransitionSuccessResult(String reason) { - super(reason); - } - } - - public static final class TransitionFailureResult extends TransitionIntoResult { - private TransitionFailureResult(String reason) { - super(reason); - } - } - - protected final class NoOpState extends State { - - private NoOpState(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { - // Transition into a NoOpState is always possible. - return null; - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { - // Transition into a NoOpState always succeeds. - return TransitionSuccessResult.EMPTY_INSTANCE; - } - } - - protected static class DisconnectedStateDescriptor extends StateDescriptor { - protected DisconnectedStateDescriptor() { - super(DisconnectedState.class, StateDescriptor.Property.finalState); - } - } - - private final class DisconnectedState extends State { - - private DisconnectedState(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { - if (inputOutputFilters.isEmpty()) { - previousInputOutputFilters = null; - } else { - previousInputOutputFilters = new ArrayList<>(inputOutputFilters.size()); - previousInputOutputFilters.addAll(inputOutputFilters); - inputOutputFilters.clear(); - } - - ListIterator it = walkFromDisconnectToAuthenticated.listIterator( - walkFromDisconnectToAuthenticated.size()); - while (it.hasPrevious()) { - State stateToReset = it.previous(); - stateToReset.resetState(); - } - walkFromDisconnectToAuthenticated = null; - - return TransitionSuccessResult.EMPTY_INSTANCE; - } - } - - protected static final class ConnectedButUnauthenticatedStateDescriptor extends StateDescriptor { - private ConnectedButUnauthenticatedStateDescriptor() { - super(ConnectedButUnauthenticatedState.class, StateDescriptor.Property.finalState); - addSuccessor(SaslAuthenticationStateDescriptor.class); - } - } - - private final class ConnectedButUnauthenticatedState extends State { - private ConnectedButUnauthenticatedState(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { - assert walkFromDisconnectToAuthenticated == null; - if (getStateDescriptor().getClass() == walkStateGraphContext.finalStateClass) { - // If this is the final state, then record the walk so far. - walkFromDisconnectToAuthenticated = new ArrayList<>(walkStateGraphContext.walkedStateGraphPath); - } - - connected = true; - return TransitionSuccessResult.EMPTY_INSTANCE; - } - - @Override - protected void resetState() { - connected = false; - } - } - - protected static final class SaslAuthenticationStateDescriptor extends StateDescriptor { - private SaslAuthenticationStateDescriptor() { - super(SaslAuthenticationState.class, "RFC 6120 ยง 6"); - addSuccessor(AuthenticatedButUnboundStateDescriptor.class); - } - } - - private final class SaslAuthenticationState extends State { - private SaslAuthenticationState(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, - SASLErrorException, IOException, SmackException, InterruptedException { - prepareToWaitForFeaturesReceived(); - - LoginContext loginContext = walkStateGraphContext.loginContext; - SASLMechanism usedSaslMechanism = authenticate(loginContext.username, loginContext.password, config.getAuthzid(), getSSLSession()); - // authenticate() will only return if the SASL authentication was successful, but we also need to wait for the next round of stream features. - - waitForFeaturesReceived("server stream features after SASL authentication"); - - return new SaslAuthenticationSuccessResult(usedSaslMechanism); - } - } - - public static final class SaslAuthenticationSuccessResult extends TransitionSuccessResult { - private final String saslMechanismName; - - private SaslAuthenticationSuccessResult(SASLMechanism usedSaslMechanism) { - super("SASL authentication successfull using " + usedSaslMechanism.getName()); - this.saslMechanismName = usedSaslMechanism.getName(); - } - - public String getSaslMechanismName() { - return saslMechanismName; - } - } - - protected static final class AuthenticatedButUnboundStateDescriptor extends StateDescriptor { - private AuthenticatedButUnboundStateDescriptor() { - super(StateDescriptor.Property.multiVisitState); - addSuccessor(ResourceBindingStateDescriptor.class); - addSuccessor(CompressionStateDescriptor.class); - } - } - - protected static final class ResourceBindingStateDescriptor extends StateDescriptor { - private ResourceBindingStateDescriptor() { - super(ResourceBindingState.class, "RFC 6120 ยง 7"); - addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); - } - } - - private final class ResourceBindingState extends State { - private ResourceBindingState(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, - SASLErrorException, IOException, SmackException, InterruptedException { - // TODO: The reportSuccess() is just a quick fix until there is a variant of the - // bindResourceAndEstablishSession() method which does not require this. - lastFeaturesReceived.reportSuccess(); - - LoginContext loginContext = walkStateGraphContext.loginContext; - Resourcepart resource = bindResourceAndEstablishSession(loginContext.resource); - streamResumed = false; - - return new ResourceBoundResult(resource, loginContext.resource); - } - } - - public static final class ResourceBoundResult extends TransitionSuccessResult { - private final Resourcepart resource; - - private ResourceBoundResult(Resourcepart boundResource, Resourcepart requestedResource) { - super("Resource '" + boundResource + "' bound (requested: '" + requestedResource + "'"); - this.resource = boundResource; - } - - public Resourcepart getResource() { - return resource; - } - } - - protected static final class CompressionStateDescriptor extends StateDescriptor { - private CompressionStateDescriptor() { - super(CompressionState.class, 138); - addSuccessor(AuthenticatedButUnboundStateDescriptor.class); - declarePrecedenceOver(ResourceBindingStateDescriptor.class); - } - } - - private boolean compressionEnabled; - - private class CompressionState extends State { - private XmppCompressionFactory selectedCompressionFactory; - private XmppInputOutputFilter usedXmppInputOutputCompressionFitler; - - protected CompressionState(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { - if (!config.isCompressionEnabled()) { - return new TransitionImpossibleReason("Stream compression disabled"); - } - - Compress.Feature compressFeature = getFeature(Compress.Feature.ELEMENT, Compress.NAMESPACE); - if (compressFeature == null) { - return new TransitionImpossibleReason("Stream compression not supported"); - } - - selectedCompressionFactory = XmppCompressionManager.getBestFactory(compressFeature); - if (selectedCompressionFactory == null) { - return new TransitionImpossibleReason("No matching compression factory"); - } - - usedXmppInputOutputCompressionFitler = selectedCompressionFactory.fabricate(config); - - return null; - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) - throws NoResponseException, NotConnectedException, FailedNonzaException, InterruptedException, - ConnectionUnexpectedTerminatedException { - final String compressionMethod = selectedCompressionFactory.getCompressionMethod(); - sendAndWaitForResponse(new Compress(compressionMethod), Compressed.class, Failure.class); - - addXmppInputOutputFilter(usedXmppInputOutputCompressionFitler); - - newStreamOpenWaitForFeaturesSequence("server stream features after compression enabled"); - - compressionEnabled = true; - - return new CompressionTransitionSuccessResult(compressionMethod); - } - - @Override - protected void resetState() { - selectedCompressionFactory = null; - usedXmppInputOutputCompressionFitler = null; - compressionEnabled = false; - } - } - - public static final class CompressionTransitionSuccessResult extends TransitionSuccessResult { - private final String compressionMethod; - - private CompressionTransitionSuccessResult(String compressionMethod) { - super(compressionMethod + " compression enabled"); - this.compressionMethod = compressionMethod; - } - - public String getCompressionMethod() { - return compressionMethod; - } - } - - @Override - public final boolean isUsingCompression() { - return compressionEnabled; - } - - protected static final class AuthenticatedAndResourceBoundStateDescriptor extends StateDescriptor { - private AuthenticatedAndResourceBoundStateDescriptor() { - super(AuthenticatedAndResourceBoundState.class, StateDescriptor.Property.finalState); - } - } - - private final class AuthenticatedAndResourceBoundState extends State { - private AuthenticatedAndResourceBoundState(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) - throws NotConnectedException, InterruptedException { - if (walkFromDisconnectToAuthenticated != null) { - // If there was already a previous walk to ConnectedButUnauthenticated, then the context of the current - // walk must not start from the 'Disconnected' state. - assert walkStateGraphContext.walkedStateGraphPath.get(0).stateDescriptor.getClass() != DisconnectedStateDescriptor.class; - walkFromDisconnectToAuthenticated.addAll(walkStateGraphContext.walkedStateGraphPath); - } else { - walkFromDisconnectToAuthenticated = new ArrayList<>(walkStateGraphContext.walkedStateGraphPath.size() + 1); - walkFromDisconnectToAuthenticated.addAll(walkStateGraphContext.walkedStateGraphPath); - } - walkFromDisconnectToAuthenticated.add(this); - - afterSuccessfulLogin(streamResumed); - return TransitionSuccessResult.EMPTY_INSTANCE; - } - - @Override - protected void resetState() { - authenticated = false; - } - } - - public void addConnectionStateMachineListener(ConnectionStateMachineListener connectionStateMachineListener) { - connectionStateMachineListeners.add(connectionStateMachineListener); - } - - public boolean removeConnectionStateMachineListener(ConnectionStateMachineListener connectionStateMachineListener) { - return connectionStateMachineListeners.remove(connectionStateMachineListener); - } - - protected void invokeConnectionStateMachineListener(ConnectionStateEvent connectionStateEvent) { - if (connectionStateMachineListeners.isEmpty()) { - return; - } - - ASYNC_BUT_ORDERED.performAsyncButOrdered(this, () -> { - for (ConnectionStateMachineListener connectionStateMachineListener : connectionStateMachineListeners) { - connectionStateMachineListener.onConnectionStateEvent(connectionStateEvent, this); - } - }); - } -} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/ConnectionStateEvent.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/ConnectionStateEvent.java index 01e0faf22..d4883fa8d 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/fsm/ConnectionStateEvent.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/ConnectionStateEvent.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018 Florian Schmaus + * Copyright 2018-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,29 +16,37 @@ */ package org.jivesoftware.smack.fsm; -import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.State; -import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.TransitionFailureResult; -import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.TransitionImpossibleReason; -import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.TransitionSuccessResult; +import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex; public class ConnectionStateEvent { - private final StateDescriptor stateDescriptor; + private final StateDescriptor currentStateDescriptor; + private final StateDescriptor successorStateDescriptor; private final long timestamp; - protected ConnectionStateEvent(StateDescriptor stateDescriptor) { - this.stateDescriptor = stateDescriptor; + public ConnectionStateEvent(StateDescriptor currentStateDescriptor) { + this(currentStateDescriptor, null); + } + + public ConnectionStateEvent(StateDescriptor currentStateDescriptor, StateDescriptor successorStateDescriptor) { + this.currentStateDescriptor = currentStateDescriptor; + this.successorStateDescriptor = successorStateDescriptor; this.timestamp = System.currentTimeMillis(); } public StateDescriptor getStateDescriptor() { - return stateDescriptor; + return currentStateDescriptor; } @Override public String toString() { - return stateDescriptor.getStateName() + ' ' + getClass().getSimpleName(); + if (successorStateDescriptor == null) { + return getClass().getSimpleName() + ": " + currentStateDescriptor.getStateName(); + } else { + return currentStateDescriptor.getStateName() + ' ' + getClass().getSimpleName() + ' ' + + successorStateDescriptor.getStateName(); + } } public long getTimestamp() { @@ -46,22 +54,22 @@ public class ConnectionStateEvent { } public static class StateRevertBackwardsWalk extends ConnectionStateEvent { - StateRevertBackwardsWalk(State state) { + public StateRevertBackwardsWalk(State state) { super(state.getStateDescriptor()); } } public static class FinalStateReached extends ConnectionStateEvent { - FinalStateReached(State state) { + public FinalStateReached(State state) { super(state.getStateDescriptor()); } } public static class TransitionNotPossible extends ConnectionStateEvent { - private final TransitionImpossibleReason transitionImpossibleReason; + private final StateTransitionResult.TransitionImpossible transitionImpossibleReason; - TransitionNotPossible(State state, TransitionImpossibleReason reason) { - super(state.getStateDescriptor()); + public TransitionNotPossible(State currentState, State successorState, StateTransitionResult.TransitionImpossible reason) { + super(currentState.getStateDescriptor(), successorState.getStateDescriptor()); this.transitionImpossibleReason = reason; } @@ -72,16 +80,16 @@ public class ConnectionStateEvent { } public static class AboutToTransitionInto extends ConnectionStateEvent { - AboutToTransitionInto(State state) { - super(state.getStateDescriptor()); + public AboutToTransitionInto(State currentState, State successorState) { + super(currentState.getStateDescriptor(), successorState.getStateDescriptor()); } } public static class TransitionFailed extends ConnectionStateEvent { - private final TransitionFailureResult transitionFailedReason; + private final StateTransitionResult.Failure transitionFailedReason; - TransitionFailed(State state, TransitionFailureResult transitionFailedReason) { - super(state.getStateDescriptor()); + public TransitionFailed(State currentState, State failedSuccessorState, StateTransitionResult.Failure transitionFailedReason) { + super(currentState.getStateDescriptor(), failedSuccessorState.getStateDescriptor()); this.transitionFailedReason = transitionFailedReason; } @@ -91,10 +99,16 @@ public class ConnectionStateEvent { } } - public static class SuccessfullyTransitionedInto extends ConnectionStateEvent { - private final TransitionSuccessResult transitionSuccessResult; + public static class TransitionIgnoredDueCycle extends ConnectionStateEvent { + public TransitionIgnoredDueCycle(GraphVertex currentStateVertex, GraphVertex successorStateVertexCausingCycle) { + super(currentStateVertex.getElement().getStateDescriptor(), successorStateVertexCausingCycle.getElement().getStateDescriptor()); + } + } - SuccessfullyTransitionedInto(State state, TransitionSuccessResult transitionSuccessResult) { + public static class SuccessfullyTransitionedInto extends ConnectionStateEvent { + private final StateTransitionResult.Success transitionSuccessResult; + + public SuccessfullyTransitionedInto(State state, StateTransitionResult.Success transitionSuccessResult) { super(state.getStateDescriptor()); this.transitionSuccessResult = transitionSuccessResult; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/ConnectionStateMachineListener.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/ConnectionStateMachineListener.java index e8dcb02b1..704d35e0a 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/fsm/ConnectionStateMachineListener.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/ConnectionStateMachineListener.java @@ -16,9 +16,11 @@ */ package org.jivesoftware.smack.fsm; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; + // TODO: Mark as java.lang.FunctionalInterface once Smack's minimum Android API level is 24 or higher. public interface ConnectionStateMachineListener { - void onConnectionStateEvent(ConnectionStateEvent connectionStateEvent, AbstractXmppStateMachineConnection connection); + void onConnectionStateEvent(ConnectionStateEvent connectionStateEvent, ModularXmppClientToServerConnection connection); } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/LoginContext.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/LoginContext.java index e0ac5ea14..9f58ce6d2 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/fsm/LoginContext.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/LoginContext.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018 Florian Schmaus + * Copyright 2018-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,11 +20,11 @@ import org.jxmpp.jid.parts.Resourcepart; // TODO: At one point SASL authzid should be part of this. public class LoginContext { - final String username; - final String password; - final Resourcepart resource; + public final String username; + public final String password; + public final Resourcepart resource; - LoginContext(String username, String password, Resourcepart resource) { + public LoginContext(String username, String password, Resourcepart resource) { this.username = username; this.password = password; this.resource = resource; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/NoOpState.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/NoOpState.java new file mode 100644 index 000000000..e059bb097 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/NoOpState.java @@ -0,0 +1,43 @@ +/** + * + * Copyright 2018-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.fsm; + +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.c2s.internal.WalkStateGraphContext; + +public class NoOpState extends State { + + /** + * Constructs a NoOpState. Note that the signature of this constructor is designed so that it mimics States which + * are non-static inner classes of ModularXmppClientToServerConnection. That is why the first argument is not used. + * + * @param connection the connection. + * @param stateDescriptor the related state descriptor + * @param connectionInternal the internal connection API. + */ + @SuppressWarnings("UnusedVariable") + protected NoOpState(ModularXmppClientToServerConnection connection, StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { + super(stateDescriptor, connectionInternal); + } + + @Override + public StateTransitionResult.Success transitionInto(WalkStateGraphContext walkStateGraphContext) { + // Transition into a NoOpState always succeeds. + return StateTransitionResult.Success.EMPTY_INSTANCE; + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/State.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/State.java new file mode 100644 index 000000000..f8749161d --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/State.java @@ -0,0 +1,81 @@ +/** + * + * Copyright 2018-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.fsm; + +import java.io.IOException; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException.FailedNonzaException; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.c2s.internal.WalkStateGraphContext; +import org.jivesoftware.smack.sasl.SASLErrorException; + +/** + * Note that this is an non-static inner class of XmppClientToServerConnection so that states can inspect and modify + * the connection. + */ +public abstract class State { + + protected final StateDescriptor stateDescriptor; + + protected final ModularXmppClientToServerConnectionInternal connectionInternal; + + protected State(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { + this.stateDescriptor = stateDescriptor; + this.connectionInternal = connectionInternal; + } + + /** + * Check if the state should be activated. + * + * @param walkStateGraphContext the context of the current state graph walk. + * @return null if the state should be activated. + * @throws SmackException in case a Smack exception occurs. + */ + public StateTransitionResult.TransitionImpossible isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) + throws SmackException { + return null; + } + + public abstract StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) + throws XMPPErrorException, SASLErrorException, IOException, SmackException, + InterruptedException, FailedNonzaException; + + public StateDescriptor getStateDescriptor() { + return stateDescriptor; + } + + public void resetState() { + } + + @Override + public String toString() { + return "State " + stateDescriptor + ' ' + connectionInternal.connection; + } + + protected final void ensureNotOnOurWayToAuthenticatedAndResourceBound( + WalkStateGraphContext walkStateGraphContext) { + if (walkStateGraphContext.isFinalStateAuthenticatedAndResourceBound()) { + throw new IllegalStateException( + "Smack should never attempt to reach the authenticated and resource bound state over " + + this + + ". This is probably a programming error within Smack, please report it to the develoeprs."); + } + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java index 261529f25..0ef98885c 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptor.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018 Florian Schmaus + * Copyright 2018-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,9 +22,9 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; -import java.util.logging.Logger; -import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.State; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; public abstract class StateDescriptor { @@ -34,15 +34,13 @@ public abstract class StateDescriptor { notImplemented, } - private static final Logger LOGGER = Logger.getLogger(StateDescriptor.class.getName()); - private final String stateName; private final int xepNum; private final String rfcSection; private final Set properties; - private final Class stateClass; - private final Constructor stateClassConstructor; + private final Class stateClass; + private final Constructor stateClassConstructor; private final Set> successors = new HashSet<>(); @@ -53,36 +51,36 @@ public abstract class StateDescriptor { private final Set> inferiorTo = new HashSet<>(); protected StateDescriptor() { - this(AbstractXmppStateMachineConnection.NoOpState.class, (Property) null); + this(NoOpState.class, (Property) null); } protected StateDescriptor(Property... properties) { - this(AbstractXmppStateMachineConnection.NoOpState.class, properties); + this(NoOpState.class, properties); } - protected StateDescriptor(Class stateClass) { + protected StateDescriptor(Class stateClass) { this(stateClass, -1, null, Collections.emptySet()); } - protected StateDescriptor(Class stateClass, Property... properties) { + protected StateDescriptor(Class stateClass, Property... properties) { this(stateClass, -1, null, new HashSet<>(Arrays.asList(properties))); } - protected StateDescriptor(Class stateClass, int xepNum) { + protected StateDescriptor(Class stateClass, int xepNum) { this(stateClass, xepNum, null, Collections.emptySet()); } - protected StateDescriptor(Class stateClass, int xepNum, + protected StateDescriptor(Class stateClass, int xepNum, Property... properties) { this(stateClass, xepNum, null, new HashSet<>(Arrays.asList(properties))); } - protected StateDescriptor(Class stateClass, String rfcSection) { + protected StateDescriptor(Class stateClass, String rfcSection) { this(stateClass, -1, rfcSection, Collections.emptySet()); } @SuppressWarnings("unchecked") - private StateDescriptor(Class stateClass, int xepNum, + private StateDescriptor(Class stateClass, int xepNum, String rfcSection, Set properties) { this.stateClass = stateClass; if (rfcSection != null && xepNum > 0) { @@ -92,26 +90,32 @@ public abstract class StateDescriptor { this.rfcSection = rfcSection; this.properties = properties; - Constructor selectedConstructor = null; + Constructor selectedConstructor = null; Constructor[] constructors = stateClass.getDeclaredConstructors(); for (Constructor constructor : constructors) { Class[] parameterTypes = constructor.getParameterTypes(); - if (parameterTypes.length != 2) { - LOGGER.warning("Invalid State class constructor: " + constructor); + if (parameterTypes.length != 3) { continue; } - if (!AbstractXmppStateMachineConnection.class.isAssignableFrom(parameterTypes[0])) { + if (!ModularXmppClientToServerConnection.class.isAssignableFrom(parameterTypes[0])) { + continue; + } + if (!StateDescriptor.class.isAssignableFrom(parameterTypes[1])) { + continue; + } + if (!ModularXmppClientToServerConnectionInternal.class.isAssignableFrom(parameterTypes[2])) { continue; } selectedConstructor = (Constructor) constructor; break; } - if (selectedConstructor == null) { - throw new IllegalArgumentException(); - } stateClassConstructor = selectedConstructor; - stateClassConstructor.setAccessible(true); + if (stateClassConstructor != null) { + stateClassConstructor.setAccessible(true); + } else { + // TODO: Add validation check that if stateClassConstructor is 'null' the cosntructState() method is overriden. + } String className = getClass().getSimpleName(); stateName = className.replaceFirst("StateDescriptor", ""); @@ -121,7 +125,7 @@ public abstract class StateDescriptor { addAndCheckNonExistent(successors, successor); } - protected void addPredeccessor(Class predeccessor) { + public void addPredeccessor(Class predeccessor) { addAndCheckNonExistent(predecessors, predeccessor); } @@ -189,7 +193,7 @@ public abstract class StateDescriptor { return referenceCache; } - public Class getStateClass() { + public Class getStateClass() { return stateClass; } @@ -205,9 +209,12 @@ public abstract class StateDescriptor { return properties.contains(Property.finalState); } - protected final AbstractXmppStateMachineConnection.State constructState(AbstractXmppStateMachineConnection connection) { + protected State constructState(ModularXmppClientToServerConnectionInternal connectionInternal) { + ModularXmppClientToServerConnection connection = connectionInternal.connection; try { - return stateClassConstructor.newInstance(connection, this); + // If stateClassConstructor is null here, then you probably forgot to override the the + // StateDescriptor.constructState() method? + return stateClassConstructor.newInstance(connection, this, connectionInternal); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new IllegalStateException(e); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptorGraph.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptorGraph.java index bc4fa7ca8..bbece8898 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptorGraph.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateDescriptorGraph.java @@ -30,7 +30,8 @@ import java.util.Map; import java.util.Set; import java.util.logging.Logger; -import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.DisconnectedStateDescriptor; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.DisconnectedStateDescriptor; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; import org.jivesoftware.smack.util.Consumer; import org.jivesoftware.smack.util.MultiMap; @@ -134,7 +135,7 @@ public class StateDescriptorGraph { // The preference graph is the graph where the precedence information of all successors is stored, which we will // topologically sort to find out which successor we should try first. It is a further new graph we use solely in - // this step for every node. The graph is representent as map. There is no special marker for the initial node + // this step for every node. The graph is represented as map. There is no special marker for the initial node // as it is not required for the topological sort performed later. Map, GraphVertex>> preferenceGraph = new HashMap<>(numSuccessors); @@ -171,7 +172,8 @@ public class StateDescriptorGraph { } } - // Perform a topological sort which returns the state descriptor classes in their priority. + // Perform a topological sort which returns the state descriptor classes sorted by their priority. Highest + // priority state descriptors first. List>> sortedSuccessors = topologicalSort(preferenceGraph.values()); // Handle the successor nodes which have not preference information available. Simply append them to the end of @@ -222,19 +224,19 @@ public class StateDescriptorGraph { return initialNode; } - private static GraphVertex convertToStateGraph(GraphVertex stateDescriptorVertex, - AbstractXmppStateMachineConnection connection, Map> handledStateDescriptors) { + private static GraphVertex convertToStateGraph(GraphVertex stateDescriptorVertex, + ModularXmppClientToServerConnectionInternal connectionInternal, Map> handledStateDescriptors) { StateDescriptor stateDescriptor = stateDescriptorVertex.getElement(); - GraphVertex stateVertex = handledStateDescriptors.get(stateDescriptor); + GraphVertex stateVertex = handledStateDescriptors.get(stateDescriptor); if (stateVertex != null) { return stateVertex; } - AbstractXmppStateMachineConnection.State state = stateDescriptor.constructState(connection); + State state = stateDescriptor.constructState(connectionInternal); stateVertex = new GraphVertex<>(state); handledStateDescriptors.put(stateDescriptor, stateVertex); for (GraphVertex successorStateDescriptorVertex : stateDescriptorVertex.getOutgoingEdges()) { - GraphVertex successorStateVertex = convertToStateGraph(successorStateDescriptorVertex, connection, handledStateDescriptors); + GraphVertex successorStateVertex = convertToStateGraph(successorStateDescriptorVertex, connectionInternal, handledStateDescriptors); // It is important that we keep the order of the edges. This should do it. stateVertex.addOutgoingEdge(successorStateVertex); } @@ -242,10 +244,10 @@ public class StateDescriptorGraph { return stateVertex; } - static GraphVertex convertToStateGraph(GraphVertex initialStateDescriptor, - AbstractXmppStateMachineConnection connection) { - Map> handledStateDescriptors = new HashMap<>(); - GraphVertex initialState = convertToStateGraph(initialStateDescriptor, connection, + public static GraphVertex convertToStateGraph(GraphVertex initialStateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + Map> handledStateDescriptors = new HashMap<>(); + GraphVertex initialState = convertToStateGraph(initialStateDescriptor, connectionInternal, handledStateDescriptors); return initialState; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateMachineException.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateMachineException.java index ed0f37fe1..818f506ca 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateMachineException.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateMachineException.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019 Florian Schmaus + * Copyright 2018-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,18 +21,26 @@ import java.util.List; import java.util.Map; import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.State; -import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.TransitionReason; +import org.jivesoftware.smack.c2s.internal.WalkStateGraphContext; +import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex; public abstract class StateMachineException extends SmackException { private static final long serialVersionUID = 1L; + protected StateMachineException(String message) { + super(message); + } + + protected StateMachineException() { + super(); + } + public static class SmackMandatoryStateFailedException extends StateMachineException { private static final long serialVersionUID = 1L; - SmackMandatoryStateFailedException(State state, TransitionReason failureReason) { + public SmackMandatoryStateFailedException(State state, StateTransitionResult failureReason) { } } @@ -40,21 +48,36 @@ public abstract class StateMachineException extends SmackException { private final List walkedStateGraphPath; - private final Map failedStates; + private final Map failedStates; + + private final StateDescriptor deadEndState; private static final long serialVersionUID = 1L; - SmackStateGraphDeadEndException(List walkedStateGraphPath, Map failedStates) { - this.walkedStateGraphPath = Collections.unmodifiableList(walkedStateGraphPath); - this.failedStates = Collections.unmodifiableMap(failedStates); + private SmackStateGraphDeadEndException(String message, WalkStateGraphContext walkStateGraphContext, GraphVertex stateVertex) { + super(message); + this.walkedStateGraphPath = Collections.unmodifiableList(walkStateGraphContext.getWalk()); + this.failedStates = Collections.unmodifiableMap(walkStateGraphContext.getFailedStates()); + + deadEndState = stateVertex.getElement().getStateDescriptor(); } public List getWalkedStateGraph() { return walkedStateGraphPath; } - public Map getFailedStates() { + public Map getFailedStates() { return failedStates; } + + public StateDescriptor getDeadEndState() { + return deadEndState; + } + + public static SmackStateGraphDeadEndException from(WalkStateGraphContext walkStateGraphContext, GraphVertex stateVertex) { + String message = stateVertex + " has no successor vertexes"; + + return new SmackStateGraphDeadEndException(message, walkStateGraphContext, stateVertex); + } } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateTransitionResult.java b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateTransitionResult.java new file mode 100644 index 000000000..053d35e77 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/fsm/StateTransitionResult.java @@ -0,0 +1,87 @@ +/** + * + * Copyright 2018-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.fsm; + +public abstract class StateTransitionResult { + + private final String message; + + protected StateTransitionResult(String message) { + this.message = message; + } + + @Override + public String toString() { + return message; + } + + public abstract static class AttemptResult extends StateTransitionResult { + protected AttemptResult(String message) { + super(message); + } + } + + public static class Success extends AttemptResult { + + public static final Success EMPTY_INSTANCE = new Success(); + + private Success() { + super(""); + } + + public Success(String successMessage) { + super(successMessage); + } + } + + public static class Failure extends AttemptResult { + public Failure(String failureMessage) { + super(failureMessage); + } + } + + public static final class FailureCausedByException extends Failure { + private final E exception; + + public FailureCausedByException(E exception) { + super(exception.getMessage()); + this.exception = exception; + } + + public E getException() { + return exception; + } + } + + public abstract static class TransitionImpossible extends StateTransitionResult { + protected TransitionImpossible(String message) { + super(message); + } + } + + public static class TransitionImpossibleReason extends TransitionImpossible { + public TransitionImpossibleReason(String reason) { + super(reason); + } + } + + public static class TransitionImpossibleBecauseNotImplemented extends TransitionImpossibleReason { + public TransitionImpossibleBecauseNotImplemented(StateDescriptor stateDescriptor) { + super(stateDescriptor.getFullStateName(false) + " is not implemented (yet)"); + } + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/isr/InstantStreamResumptionModule.java b/smack-core/src/main/java/org/jivesoftware/smack/isr/InstantStreamResumptionModule.java new file mode 100644 index 000000000..02219c20b --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/isr/InstantStreamResumptionModule.java @@ -0,0 +1,85 @@ +/** + * + * Copyright 2019-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.isr; + +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.AuthenticatedAndResourceBoundStateDescriptor; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.ConnectedButUnauthenticatedStateDescriptor; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.SaslAuthenticationStateDescriptor; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModule; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.c2s.internal.WalkStateGraphContext; +import org.jivesoftware.smack.fsm.State; +import org.jivesoftware.smack.fsm.StateDescriptor; +import org.jivesoftware.smack.fsm.StateTransitionResult; + +public class InstantStreamResumptionModule extends ModularXmppClientToServerConnectionModule { + + protected InstantStreamResumptionModule(InstantStreamResumptionModuleDescriptor instantStreamResumptionModuleDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + super(instantStreamResumptionModuleDescriptor, connectionInternal); + } + + public static final class InstantStreamResumptionStateDescriptor extends StateDescriptor { + private InstantStreamResumptionStateDescriptor() { + super(InstantStreamResumptionState.class, 397, StateDescriptor.Property.notImplemented); + + addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); + addPredeccessor(ConnectedButUnauthenticatedStateDescriptor.class); + declarePrecedenceOver(SaslAuthenticationStateDescriptor.class); + } + + @Override + protected InstantStreamResumptionModule.InstantStreamResumptionState constructState(ModularXmppClientToServerConnectionInternal connectionInternal) { + // This is the trick: the module is constructed prior the states, so we get the actual state out of the module by fetching the module from the connection. + InstantStreamResumptionModule isrModule = connectionInternal.connection.getConnectionModuleFor(InstantStreamResumptionModuleDescriptor.class); + return isrModule.constructInstantStreamResumptionState(this, connectionInternal); + } + } + + private boolean useIsr = true; + + private final class InstantStreamResumptionState extends State { + private InstantStreamResumptionState(InstantStreamResumptionStateDescriptor instantStreamResumptionStateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + super(instantStreamResumptionStateDescriptor, connectionInternal); + } + + @Override + public StateTransitionResult.TransitionImpossible isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { + if (!useIsr) { + return new StateTransitionResult.TransitionImpossibleReason("Instant stream resumption not enabled nor implemented"); + } + + return new StateTransitionResult.TransitionImpossibleBecauseNotImplemented(stateDescriptor); + } + + @Override + public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + throw new IllegalStateException("Instant stream resumption not implemented"); + } + } + + public void setInstantStreamResumptionEnabled(boolean useIsr) { + this.useIsr = useIsr; + } + + public InstantStreamResumptionState constructInstantStreamResumptionState( + InstantStreamResumptionStateDescriptor instantStreamResumptionStateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + return new InstantStreamResumptionState(instantStreamResumptionStateDescriptor, connectionInternal); + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/isr/InstantStreamResumptionModuleDescriptor.java b/smack-core/src/main/java/org/jivesoftware/smack/isr/InstantStreamResumptionModuleDescriptor.java new file mode 100644 index 000000000..d26703eec --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/isr/InstantStreamResumptionModuleDescriptor.java @@ -0,0 +1,54 @@ +/** + * + * Copyright 2019-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.isr; + +import java.util.Collections; +import java.util.Set; + +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModuleDescriptor; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.fsm.StateDescriptor; + +public class InstantStreamResumptionModuleDescriptor extends ModularXmppClientToServerConnectionModuleDescriptor { + + private static final InstantStreamResumptionModuleDescriptor INSTANCE = new InstantStreamResumptionModuleDescriptor(); + + @Override + protected Set> getStateDescriptors() { + return Collections.singleton(InstantStreamResumptionModule.InstantStreamResumptionStateDescriptor.class); + } + + @Override + protected InstantStreamResumptionModule constructXmppConnectionModule( + ModularXmppClientToServerConnectionInternal connectionInternal) { + return new InstantStreamResumptionModule(this, connectionInternal); + } + + public static final class Builder extends ModularXmppClientToServerConnectionModuleDescriptor.Builder { + + private Builder(ModularXmppClientToServerConnectionConfiguration.Builder connectionConfigurationBuilder) { + super(connectionConfigurationBuilder); + } + + @Override + protected ModularXmppClientToServerConnectionModuleDescriptor build() { + return INSTANCE; + } + + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/isr/package-info.java b/smack-core/src/main/java/org/jivesoftware/smack/isr/package-info.java new file mode 100644 index 000000000..cf24d86ae --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/isr/package-info.java @@ -0,0 +1,23 @@ +/** + * + * Copyright 2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Classes and interfaces for Instant Stream Resumption (ISR) (XEP-0397). + * + * @see XEP-0397: Instant Stream Resumption + */ +package org.jivesoftware.smack.isr; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/ArrayBlockingQueueWithShutdown.java b/smack-core/src/main/java/org/jivesoftware/smack/util/ArrayBlockingQueueWithShutdown.java index d70ed77c9..01938c43c 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/ArrayBlockingQueueWithShutdown.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/ArrayBlockingQueueWithShutdown.java @@ -163,15 +163,20 @@ public class ArrayBlockingQueueWithShutdown extends AbstractQueue implemen /** * Start the queue. Newly created instances will be started automatically, thus this only needs * to be called after {@link #shutdown()}. + * + * @return true if the queues was shutdown before, false if not. */ - public void start() { + public boolean start() { + boolean previousIsShutdown; lock.lock(); try { + previousIsShutdown = isShutdown; isShutdown = false; } finally { lock.unlock(); } + return previousIsShutdown; } /** diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java index 4170ce70d..eb76ef3be 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/CollectionUtil.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2019 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,10 @@ package org.jivesoftware.smack.util; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Set; public class CollectionUtil { @@ -56,4 +58,11 @@ public class CollectionUtil { } return new ArrayList<>(collection); } + + public static Set newSetWith(Collection collection) { + if (collection == null) { + return null; + } + return new HashSet<>(collection); + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/DNSUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/DNSUtil.java index 3a2c6a316..7f98da029 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/DNSUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/DNSUtil.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2005 Jive Software, 2016-2018 Florian Schmaus. + * Copyright 2003-2005 Jive Software, 2016-2020 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,23 +16,9 @@ */ package org.jivesoftware.smack.util; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.SortedMap; -import java.util.TreeMap; -import java.util.logging.Level; -import java.util.logging.Logger; - -import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; import org.jivesoftware.smack.util.dns.DNSResolver; -import org.jivesoftware.smack.util.dns.HostAddress; -import org.jivesoftware.smack.util.dns.SRVRecord; import org.jivesoftware.smack.util.dns.SmackDaneProvider; -import org.minidns.dnsname.DnsName; - /** * Utility class to perform DNS lookups for XMPP services. * @@ -41,10 +27,6 @@ import org.minidns.dnsname.DnsName; */ public class DNSUtil { - public static final String XMPP_CLIENT_DNS_SRV_PREFIX = "_xmpp-client._tcp"; - public static final String XMPP_SERVER_DNS_SRV_PREFIX = "_xmpp-server._tcp"; - - private static final Logger LOGGER = Logger.getLogger(DNSUtil.class.getName()); private static DNSResolver dnsResolver = null; private static SmackDaneProvider daneProvider; @@ -84,188 +66,4 @@ public class DNSUtil { return daneProvider; } - @SuppressWarnings("ImmutableEnumChecker") - enum DomainType { - server(XMPP_SERVER_DNS_SRV_PREFIX), - client(XMPP_CLIENT_DNS_SRV_PREFIX), - ; - public final DnsName srvPrefix; - - DomainType(String srvPrefixString) { - srvPrefix = DnsName.from(srvPrefixString); - } - } - - /** - * Returns a list of HostAddresses under which the specified XMPP server can be reached at for client-to-server - * communication. A DNS lookup for a SRV record in the form "_xmpp-client._tcp.example.com" is attempted, according - * to section 3.2.1 of RFC 6120. If that lookup fails, it's assumed that the XMPP server lives at the host resolved - * by a DNS lookup at the specified domain on the default port of 5222. - *

- * As an example, a lookup for "example.com" may return "im.example.com:5269". - *

- * - * @param domain the domain. - * @param failedAddresses on optional list that will be populated with host addresses that failed to resolve. - * @param dnssecMode DNSSec mode. - * @return List of HostAddress, which encompasses the hostname and port that the - * XMPP server can be reached at for the specified domain. - */ - public static List resolveXMPPServiceDomain(DnsName domain, List failedAddresses, DnssecMode dnssecMode) { - return resolveDomain(domain, DomainType.client, failedAddresses, dnssecMode); - } - - /** - * Returns a list of HostAddresses under which the specified XMPP server can be reached at for server-to-server - * communication. A DNS lookup for a SRV record in the form "_xmpp-server._tcp.example.com" is attempted, according - * to section 3.2.1 of RFC 6120. If that lookup fails , it's assumed that the XMPP server lives at the host resolved - * by a DNS lookup at the specified domain on the default port of 5269. - *

- * As an example, a lookup for "example.com" may return "im.example.com:5269". - *

- * - * @param domain the domain. - * @param failedAddresses on optional list that will be populated with host addresses that failed to resolve. - * @param dnssecMode DNSSec mode. - * @return List of HostAddress, which encompasses the hostname and port that the - * XMPP server can be reached at for the specified domain. - */ - public static List resolveXMPPServerDomain(DnsName domain, List failedAddresses, DnssecMode dnssecMode) { - return resolveDomain(domain, DomainType.server, failedAddresses, dnssecMode); - } - - /** - * - * @param domain the domain. - * @param domainType the XMPP domain type, server or client. - * @param failedAddresses a list that will be populated with host addresses that failed to resolve. - * @return a list of resolver host addresses for this domain. - */ - private static List resolveDomain(DnsName domain, DomainType domainType, - List failedAddresses, DnssecMode dnssecMode) { - if (dnsResolver == null) { - throw new IllegalStateException("No DNS Resolver active in Smack"); - } - - List addresses = new ArrayList(); - - // Step one: Do SRV lookups - DnsName srvDomain = DnsName.from(domainType.srvPrefix, domain); - - List srvRecords = dnsResolver.lookupSRVRecords(srvDomain, failedAddresses, dnssecMode); - if (srvRecords != null && !srvRecords.isEmpty()) { - if (LOGGER.isLoggable(Level.FINE)) { - String logMessage = "Resolved SRV RR for " + srvDomain + ":"; - for (SRVRecord r : srvRecords) - logMessage += " " + r; - LOGGER.fine(logMessage); - } - List sortedRecords = sortSRVRecords(srvRecords); - addresses.addAll(sortedRecords); - } else { - LOGGER.info("Could not resolve DNS SRV resource records for " + srvDomain + ". Consider adding those."); - } - - int defaultPort = -1; - switch (domainType) { - case client: - defaultPort = 5222; - break; - case server: - defaultPort = 5269; - break; - } - // Step two: Add the hostname to the end of the list - HostAddress hostAddress = dnsResolver.lookupHostAddress(domain, defaultPort, failedAddresses, dnssecMode); - if (hostAddress != null) { - addresses.add(hostAddress); - } - - return addresses; - } - - /** - * Sort a given list of SRVRecords as described in RFC 2782 - * Note that we follow the RFC with one exception. In a group of the same priority, only the first entry - * is calculated by random. The others are ore simply ordered by their priority. - * - * @param records TODO javadoc me please - * @return the list of resolved HostAddresses - */ - private static List sortSRVRecords(List records) { - // RFC 2782, Usage rules: "If there is precisely one SRV RR, and its Target is "." - // (the root domain), abort." - if (records.size() == 1 && records.get(0).getFQDN().isRootLabel()) - return Collections.emptyList(); - - // sorting the records improves the performance of the bisection later - Collections.sort(records); - - // create the priority buckets - SortedMap> buckets = new TreeMap>(); - for (SRVRecord r : records) { - Integer priority = r.getPriority(); - List bucket = buckets.get(priority); - // create the list of SRVRecords if it doesn't exist - if (bucket == null) { - bucket = new LinkedList(); - buckets.put(priority, bucket); - } - bucket.add(r); - } - - List res = new ArrayList(records.size()); - - for (Integer priority : buckets.keySet()) { - List bucket = buckets.get(priority); - int bucketSize; - while ((bucketSize = bucket.size()) > 0) { - int[] totals = new int[bucketSize]; - int running_total = 0; - int count = 0; - int zeroWeight = 1; - - for (SRVRecord r : bucket) { - if (r.getWeight() > 0) { - zeroWeight = 0; - break; - } - } - - for (SRVRecord r : bucket) { - running_total += r.getWeight() + zeroWeight; - totals[count] = running_total; - count++; - } - int selectedPos; - if (running_total == 0) { - // If running total is 0, then all weights in this priority - // group are 0. So we simply select one of the weights randomly - // as the other 'normal' algorithm is unable to handle this case - selectedPos = (int) (Math.random() * bucketSize); - } else { - double rnd = Math.random() * running_total; - selectedPos = bisect(totals, rnd); - } - // add the SRVRecord that was randomly chosen on it's weight - // to the start of the result list - SRVRecord chosenSRVRecord = bucket.remove(selectedPos); - res.add(chosenSRVRecord); - } - } - - return res; - } - - // TODO this is not yet really bisection just a stupid linear search - private static int bisect(int[] array, double value) { - int pos = 0; - for (int element : array) { - if (value < element) - break; - pos++; - } - return pos; - } - } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/Function.java b/smack-core/src/main/java/org/jivesoftware/smack/util/Function.java index 760d3cf5d..2cc555208 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/Function.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/Function.java @@ -1,6 +1,6 @@ /** * - * Copyright 2019 Florian Schmaus + * Copyright 2019-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,4 +20,7 @@ public interface Function { R apply(T t); + static Function identity() { + return t -> t; + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/MultiMap.java b/smack-core/src/main/java/org/jivesoftware/smack/util/MultiMap.java index ea58e469e..daa3241a8 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/MultiMap.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/MultiMap.java @@ -123,17 +123,26 @@ public class MultiMap implements TypedCloneable> { } public boolean put(K key, V value) { + return putInternal(key, list -> list.add(value)); + } + + public boolean putFirst(K key, V value) { + return putInternal(key, list -> list.add(0, value)); + } + + private boolean putInternal(K key, Consumer> valueListConsumer) { boolean keyExisted; List list = map.get(key); if (list == null) { list = new ArrayList<>(ENTRY_LIST_SIZE); - list.add(value); map.put(key, list); keyExisted = false; } else { - list.add(value); keyExisted = true; } + + valueListConsumer.accept(list); + return keyExisted; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java index ff01779ed..b2958395b 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software, 2016-2019 Florian Schmaus. + * Copyright 2003-2007 Jive Software, 2016-2020 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -467,10 +467,24 @@ public class StringUtils { return sb; } + public static void appendTo(Collection collection, StringBuilder sb) { + appendTo(collection, ", ", sb); + } + + public static void appendTo(Collection collection, StringBuilder sb, + Consumer appendFunction) { + appendTo(collection, ", ", sb, appendFunction); + } + public static void appendTo(Collection collection, String delimiter, StringBuilder sb) { - for (Iterator it = collection.iterator(); it.hasNext();) { - Object cs = it.next(); - sb.append(cs); + appendTo(collection, delimiter, sb, o -> sb.append(o)); + } + + public static void appendTo(Collection collection, String delimiter, StringBuilder sb, + Consumer appendFunction) { + for (Iterator it = collection.iterator(); it.hasNext();) { + O cs = it.next(); + appendFunction.accept(cs); if (it.hasNext()) { sb.append(delimiter); } @@ -565,4 +579,16 @@ public class StringUtils { public static String deleteXmlWhitespace(String string) { return XML_WHITESPACE.matcher(string).replaceAll(""); } + + public static Appendable appendHeading(Appendable appendable, String heading) throws IOException { + return appendHeading(appendable, heading, '-'); + } + + public static Appendable appendHeading(Appendable appendable, String heading, char underlineChar) throws IOException { + appendable.append(heading).append('\n'); + for (int i = 0; i < heading.length(); i++) { + appendable.append(underlineChar); + } + return appendable.append('\n'); + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/DNSResolver.java b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/DNSResolver.java index 1e7efe50c..41db7b073 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/DNSResolver.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/DNSResolver.java @@ -1,6 +1,6 @@ /** * - * Copyright 2013-2018 Florian Schmaus + * Copyright 2013-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,13 +19,16 @@ package org.jivesoftware.smack.util.dns; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; +import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure; import org.minidns.dnsname.DnsName; +import org.minidns.record.SRV; /** * Implementations of this interface define a class that is capable of resolving DNS addresses. @@ -43,25 +46,25 @@ public abstract class DNSResolver { /** * Gets a list of service records for the specified service. + * * @param name The symbolic name of the service. - * @param failedAddresses list of failed addresses. + * @param lookupFailures list of exceptions that occurred during lookup. * @param dnssecMode security mode. * @return The list of SRV records mapped to the service name. */ - public final List lookupSRVRecords(DnsName name, List failedAddresses, DnssecMode dnssecMode) { + public final Collection lookupSrvRecords(DnsName name, + List lookupFailures, DnssecMode dnssecMode) { checkIfDnssecRequestedAndSupported(dnssecMode); - return lookupSRVRecords0(name, failedAddresses, dnssecMode); + return lookupSrvRecords0(name, lookupFailures, dnssecMode); } - protected abstract List lookupSRVRecords0(DnsName name, List failedAddresses, DnssecMode dnssecMode); + protected abstract Collection lookupSrvRecords0(DnsName name, + List lookupFailures, DnssecMode dnssecMode); - public final HostAddress lookupHostAddress(DnsName name, int port, List failedAddresses, DnssecMode dnssecMode) { + public final List lookupHostAddress(DnsName name, + List lookupFailures, DnssecMode dnssecMode) { checkIfDnssecRequestedAndSupported(dnssecMode); - List inetAddresses = lookupHostAddress0(name, failedAddresses, dnssecMode); - if (inetAddresses == null || inetAddresses.isEmpty()) { - return null; - } - return new HostAddress(name, port, inetAddresses); + return lookupHostAddress0(name, lookupFailures, dnssecMode); } /** @@ -74,11 +77,11 @@ public abstract class DNSResolver { *

* * @param name the DNS name to lookup - * @param failedAddresses a list with the failed addresses + * @param lookupFailures list of exceptions that occurred during lookup. * @param dnssecMode the selected DNSSEC mode * @return A list, either empty or non-empty, or null */ - protected List lookupHostAddress0(DnsName name, List failedAddresses, DnssecMode dnssecMode) { + protected List lookupHostAddress0(DnsName name, List lookupFailures, DnssecMode dnssecMode) { // Default implementation of a DNS name lookup for A/AAAA records. It is assumed that this method does never // support DNSSEC. Subclasses are free to override this method. if (dnssecMode != DnssecMode.disabled) { @@ -89,14 +92,14 @@ public abstract class DNSResolver { try { inetAddressArray = InetAddress.getAllByName(name.toString()); } catch (UnknownHostException e) { - failedAddresses.add(new HostAddress(name, e)); + lookupFailures.add(new RemoteConnectionEndpointLookupFailure.DnsLookupFailure(name, e)); return null; } return Arrays.asList(inetAddressArray); } - protected final boolean shouldContinue(CharSequence name, CharSequence hostname, List hostAddresses) { + protected static boolean shouldContinue(CharSequence name, CharSequence hostname, List hostAddresses) { if (hostAddresses == null) { return true; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/HostAddress.java b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/HostAddress.java deleted file mode 100644 index 83300bbc6..000000000 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/HostAddress.java +++ /dev/null @@ -1,182 +0,0 @@ -/** - * - * Copyright ยฉ 2013-2018 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.util.dns; - -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import org.jivesoftware.smack.SmackException.ConnectionException; - -import org.minidns.dnsname.DnsName; - -public class HostAddress { - private final DnsName fqdn; - private final int port; - private final Map exceptions = new LinkedHashMap<>(); - private final List inetAddresses; - - /** - * Creates a new HostAddress with the given FQDN. - * - * @param fqdn the optional fully qualified domain name (FQDN). - * @param port The port to connect on. - * @param inetAddresses list of addresses. - * @throws IllegalArgumentException If the port is out of valid range (0 - 65535). - */ - public HostAddress(DnsName fqdn, int port, List inetAddresses) { - if (port < 0 || port > 65535) - throw new IllegalArgumentException( - "Port must be a 16-bit unsigned integer (i.e. between 0-65535. Port was: " + port); - this.fqdn = fqdn; - this.port = port; - if (inetAddresses.isEmpty()) { - throw new IllegalArgumentException("Must provide at least one InetAddress"); - } - this.inetAddresses = inetAddresses; - } - - public HostAddress(int port, InetAddress hostAddress) { - this(null, port, Collections.singletonList(hostAddress)); - } - - /** - * Constructs a new failed HostAddress. This constructor is usually used when the DNS resolution of the domain name - * failed for some reason. - * - * @param fqdn the domain name of the host. - * @param e the exception causing the failure. - */ - public HostAddress(DnsName fqdn, Exception e) { - this.fqdn = fqdn; - this.port = 5222; - inetAddresses = Collections.emptyList(); - setException(e); - } - - public HostAddress(InetSocketAddress inetSocketAddress, Exception exception) { - String hostString = inetSocketAddress.getHostString(); - this.fqdn = DnsName.from(hostString); - this.port = inetSocketAddress.getPort(); - inetAddresses = Collections.emptyList(); - setException(exception); - } - - public String getHost() { - if (fqdn != null) { - return fqdn.toString(); - } - - // In this case, the HostAddress(int, InetAddress) constructor must been used. We have no FQDN. And - // inetAddresses.size() must be exactly one. - assert inetAddresses.size() == 1; - return inetAddresses.get(0).getHostAddress(); - } - - /** - * Return the fully qualified domain name. This may return null in case there host address is only numeric, i.e. an IP address. - * - * @return the fully qualified domain name or null - */ - public DnsName getFQDN() { - return fqdn; - } - - public int getPort() { - return port; - } - - public void setException(Exception exception) { - setException(null, exception); - } - - public void setException(InetAddress inetAddress, Exception exception) { - Exception old = exceptions.put(inetAddress, exception); - assert old == null; - } - - /** - * Retrieve the Exception that caused a connection failure to this HostAddress. Every - * HostAddress found in {@link ConnectionException} will have an Exception set, - * which can be retrieved with this method. - * - * @return the Exception causing this HostAddress to fail - */ - public Map getExceptions() { - return Collections.unmodifiableMap(exceptions); - } - - public List getInetAddresses() { - return Collections.unmodifiableList(inetAddresses); - } - - @Override - public String toString() { - return getHost() + ":" + port; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof HostAddress)) { - return false; - } - - final HostAddress address = (HostAddress) o; - - if (!getHost().equals(address.getHost())) { - return false; - } - return port == address.port; - } - - @Override - public int hashCode() { - int result = 1; - result = 37 * result + getHost().hashCode(); - return result * 37 + port; - } - - public String getErrorMessage() { - if (exceptions.isEmpty()) { - return "No error logged"; - } - StringBuilder sb = new StringBuilder(); - sb.append('\'').append(toString()).append("' failed because: "); - Iterator> iterator = exceptions.entrySet().iterator(); - while (iterator.hasNext()) { - Entry entry = iterator.next(); - InetAddress inetAddress = entry.getKey(); - if (inetAddress != null) { - sb.append(entry.getKey()).append(" exception: "); - } - sb.append(entry.getValue()); - if (iterator.hasNext()) { - sb.append(", "); - } - } - - return sb.toString(); - } -} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SRVRecord.java b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SRVRecord.java deleted file mode 100644 index e55dc08f1..000000000 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SRVRecord.java +++ /dev/null @@ -1,91 +0,0 @@ -/** - * - * Copyright 2013-2018 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.util.dns; - -import java.net.InetAddress; -import java.util.List; - -import org.jivesoftware.smack.util.StringUtils; - -import org.minidns.dnsname.DnsName; - -/** - * A DNS SRV RR. - * - * @see RFC 2782: A DNS RR for specifying the location of services (DNS - * SRV) - * @author Florian Schmaus - * - */ -public class SRVRecord extends HostAddress implements Comparable { - - private int weight; - private int priority; - - /** - * SRV Record constructor. - * - * @param fqdn Fully qualified domain name - * @param port The connection port - * @param priority Priority of the target host - * @param weight Relative weight for records with same priority - * @param inetAddresses list of addresses. - * @throws IllegalArgumentException fqdn is null or any other field is not in valid range (0-65535). - */ - public SRVRecord(DnsName fqdn, int port, int priority, int weight, List inetAddresses) { - super(fqdn, port, inetAddresses); - StringUtils.requireNotNullNorEmpty(fqdn, "The FQDN must not be null"); - if (weight < 0 || weight > 65535) - throw new IllegalArgumentException( - "DNS SRV records weight must be a 16-bit unsigned integer (i.e. between 0-65535. Weight was: " - + weight); - - if (priority < 0 || priority > 65535) - throw new IllegalArgumentException( - "DNS SRV records priority must be a 16-bit unsigned integer (i.e. between 0-65535. Priority was: " - + priority); - - this.priority = priority; - this.weight = weight; - - } - - public int getPriority() { - return priority; - } - - public int getWeight() { - return weight; - } - - @Override - public int compareTo(SRVRecord other) { - // According to RFC2782, - // "[a] client MUST attempt to contact the target host with the lowest-numbered priority it can reach". - // This means that a SRV record with a higher priority is 'less' then one with a lower. - int res = other.priority - this.priority; - if (res == 0) { - res = this.weight - other.weight; - } - return res; - } - - @Override - public String toString() { - return super.toString() + " prio:" + priority + ":w:" + weight; - } -} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/rce/RemoteConnectionEndpoint.java b/smack-core/src/main/java/org/jivesoftware/smack/util/rce/RemoteConnectionEndpoint.java new file mode 100644 index 000000000..0fc31e85f --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/rce/RemoteConnectionEndpoint.java @@ -0,0 +1,59 @@ +/** + * + * Copyright 2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.rce; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Collection; + +import org.jivesoftware.smack.datatypes.UInt16; + +public interface RemoteConnectionEndpoint { + + CharSequence getHost(); + + UInt16 getPort(); + + Collection getInetAddresses(); + + String getDescription(); + + class InetSocketAddressCoupling { + private final RCE connectionEndpoint; + private final InetSocketAddress inetSocketAddress; + + public InetSocketAddressCoupling(RCE connectionEndpoint, InetAddress inetAddress) { + this.connectionEndpoint = connectionEndpoint; + + UInt16 port = connectionEndpoint.getPort(); + inetSocketAddress = new InetSocketAddress(inetAddress, port.intValue()); + } + + public RCE getRemoteConnectionEndpoint() { + return connectionEndpoint; + } + + public InetSocketAddress getInetSocketAddress() { + return inetSocketAddress; + } + + @Override + public String toString() { + return connectionEndpoint.getDescription() + " (" + inetSocketAddress + ')'; + } + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/rce/RemoteConnectionEndpointLookupFailure.java b/smack-core/src/main/java/org/jivesoftware/smack/util/rce/RemoteConnectionEndpointLookupFailure.java new file mode 100644 index 000000000..73ce8a4b0 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/rce/RemoteConnectionEndpointLookupFailure.java @@ -0,0 +1,70 @@ +/** + * + * Copyright 2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.rce; + +import org.jivesoftware.smack.util.ToStringUtil; + +import org.minidns.dnsname.DnsName; + +public abstract class RemoteConnectionEndpointLookupFailure { + + private final String description; + private final Exception exception; + + public RemoteConnectionEndpointLookupFailure(String description, Exception exception) { + this.description = description; + this.exception = exception; + } + + public final String getDescription() { + return description; + } + + public final Exception getException() { + return exception; + } + + public String getErrorMessage() { + return description + " because: " + exception; + } + + private transient String toStringCache; + + @Override + public String toString() { + if (toStringCache == null) { + toStringCache = ToStringUtil.builderFor(RemoteConnectionEndpointLookupFailure.class) + .addValue("description", description) + .addValue("exception", exception) + .build(); + } + return toStringCache; + } + + public static class DnsLookupFailure extends RemoteConnectionEndpointLookupFailure { + private final DnsName dnsName; + + public DnsLookupFailure(DnsName dnsName, Exception exception) { + super("DNS lookup exception for " + dnsName, exception); + this.dnsName = dnsName; + } + + public DnsName getDnsName() { + return dnsName; + } + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/rce/RemoteConnectionException.java b/smack-core/src/main/java/org/jivesoftware/smack/util/rce/RemoteConnectionException.java new file mode 100644 index 000000000..581b58a28 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/rce/RemoteConnectionException.java @@ -0,0 +1,66 @@ +/** + * + * Copyright 2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.rce; + +import java.net.InetAddress; + +import org.jivesoftware.smack.util.ToStringUtil; + +public final class RemoteConnectionException { + + private final RemoteConnectionEndpoint.InetSocketAddressCoupling address; + private final Exception exception; + + public RemoteConnectionException(RCE remoteConnectionEndpoint, InetAddress inetAddress, + Exception exception) { + this(new RemoteConnectionEndpoint.InetSocketAddressCoupling<>(remoteConnectionEndpoint, inetAddress), exception); + } + + public RemoteConnectionException(RemoteConnectionEndpoint.InetSocketAddressCoupling address, Exception exception) { + this.address = address; + this.exception = exception; + } + + public RemoteConnectionEndpoint.InetSocketAddressCoupling getAddress() { + return address; + } + + public Exception getException() { + return exception; + } + + public String getErrorMessage() { + return "\'" + address + "' failed because: " + exception; + } + + private transient String toStringCache; + + @Override + public String toString() { + if (toStringCache == null) { + toStringCache = ToStringUtil.builderFor(RemoteConnectionException.class) + .addValue("address", address) + .addValue("exception", exception) + .build(); + } + return toStringCache; + } + + public static RemoteConnectionException from(SARCE remoteConnectionEndpoint, Exception exception) { + return new RemoteConnectionException(remoteConnectionEndpoint, remoteConnectionEndpoint.getInetAddress(), exception); + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/rce/SingleAddressRemoteConnectionEndpoint.java b/smack-core/src/main/java/org/jivesoftware/smack/util/rce/SingleAddressRemoteConnectionEndpoint.java new file mode 100644 index 000000000..54946d793 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/rce/SingleAddressRemoteConnectionEndpoint.java @@ -0,0 +1,31 @@ +/** + * + * Copyright 2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.rce; + +import java.net.InetAddress; +import java.util.Collection; +import java.util.Collections; + +public interface SingleAddressRemoteConnectionEndpoint extends RemoteConnectionEndpoint { + + InetAddress getInetAddress(); + + @Override + default Collection getInetAddresses() { + return Collections.singletonList(getInetAddress()); + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/rce/package-info.java b/smack-core/src/main/java/org/jivesoftware/smack/util/rce/package-info.java new file mode 100644 index 000000000..ae0ef2035 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/rce/package-info.java @@ -0,0 +1,21 @@ +/** + * + * Copyright 2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Utility classes for Remote Connection Endpoints (RCE). + */ +package org.jivesoftware.smack.util.rce; diff --git a/smack-core/src/test/java/org/jivesoftware/smack/SmackExceptionTest.java b/smack-core/src/test/java/org/jivesoftware/smack/SmackExceptionTest.java deleted file mode 100644 index 4403f7bf5..000000000 --- a/smack-core/src/test/java/org/jivesoftware/smack/SmackExceptionTest.java +++ /dev/null @@ -1,59 +0,0 @@ -/** - * - * Copyright ยฉ 2014-2018 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; - -import static org.junit.Assert.assertEquals; - -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -import org.jivesoftware.smack.SmackException.ConnectionException; -import org.jivesoftware.smack.util.dns.HostAddress; - -import org.junit.Test; -import org.minidns.dnsname.DnsName; - -public class SmackExceptionTest { - - @Test - public void testConnectionException() throws UnknownHostException { - List failedAddresses = new LinkedList(); - - DnsName host = DnsName.from("foo.bar.example"); - InetAddress inetAddress = InetAddress.getByAddress(host.toString(), new byte[] { 0, 0, 0, 0 }); - List inetAddresses = Collections.singletonList(inetAddress); - HostAddress hostAddress = new HostAddress(host, 1234, inetAddresses); - hostAddress.setException(new Exception("Failed for some reason")); - failedAddresses.add(hostAddress); - - host = DnsName.from("barz.example"); - inetAddress = InetAddress.getByAddress(host.toString(), new byte[] { 0, 0, 0, 0 }); - inetAddresses = Collections.singletonList(inetAddress); - hostAddress = new HostAddress(host, 5678, inetAddresses); - hostAddress.setException(new Exception("Failed for some other reason")); - failedAddresses.add(hostAddress); - - ConnectionException connectionException = ConnectionException.from(failedAddresses); - String message = connectionException.getMessage(); - assertEquals("The following addresses failed: 'foo.bar.example:1234' failed because: java.lang.Exception: Failed for some reason, 'barz.example:5678' failed because: java.lang.Exception: Failed for some other reason", - message); - } - -} diff --git a/smack-core/src/test/java/org/jivesoftware/smack/util/DnsUtilTest.java b/smack-core/src/test/java/org/jivesoftware/smack/util/DnsUtilTest.java index 626a3d4f4..eef18918f 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/util/DnsUtilTest.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/util/DnsUtilTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018 Florian Schmaus. + * Copyright 2018-2020 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ package org.jivesoftware.smack.util; import static org.junit.Assert.assertEquals; -import org.jivesoftware.smack.util.DNSUtil.DomainType; import org.jivesoftware.smack.util.dns.SmackDaneProvider; import org.jivesoftware.smack.util.dns.SmackDaneVerifier; @@ -26,15 +25,6 @@ import org.junit.Test; public class DnsUtilTest { - @Test - public void simpleDomainTypeTest() { - DomainType client = DomainType.client; - assertEquals(DNSUtil.XMPP_CLIENT_DNS_SRV_PREFIX, client.srvPrefix.ace); - - DomainType server = DomainType.server; - assertEquals(DNSUtil.XMPP_SERVER_DNS_SRV_PREFIX, server.srvPrefix.ace); - } - private static final SmackDaneProvider DNS_UTIL_TEST_DANE_PROVIDER = new SmackDaneProvider() { @Override public SmackDaneVerifier newInstance() { diff --git a/smack-integration-test/build.gradle b/smack-integration-test/build.gradle index f16e9b76c..f0b380cca 100644 --- a/smack-integration-test/build.gradle +++ b/smack-integration-test/build.gradle @@ -7,13 +7,7 @@ mainClassName = 'org.igniterealtime.smack.inttest.SmackIntegrationTestFramework' applicationDefaultJvmArgs = ["-enableassertions"] dependencies { - compile project(':smack-java7') - compile project(':smack-tcp') - compile project(':smack-extensions') - compile project(':smack-experimental') - compile project(':smack-omemo') - compile project(':smack-openpgp') - compile project(':smack-debug') + api project(':smack-java8-full') compile 'org.reflections:reflections:0.9.11' compile 'eu.geekplace.javapinning:java-pinning-java7:1.1.0-alpha1' compile group: 'commons-io', name: 'commons-io', version: "$commonsIoVersion" diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntTest.java index dd1a7b194..57b85f722 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntTest.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2019 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ public abstract class AbstractSmackIntTest { protected final Configuration sinttestConfiguration; - protected AbstractSmackIntTest(SmackIntegrationTestEnvironment environment) { + protected AbstractSmackIntTest(SmackIntegrationTestEnvironment environment) { this.testRunId = environment.testRunId; this.sinttestConfiguration = environment.configuration; this.timeout = environment.configuration.replyTimeout; diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntegrationTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntegrationTest.java index 5f32f9268..befb94c08 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2018 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ public abstract class AbstractSmackIntegrationTest extends AbstractSmackIntTest protected final List connections; - public AbstractSmackIntegrationTest(SmackIntegrationTestEnvironment environment) { + public AbstractSmackIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); this.connection = this.conOne = environment.conOne; this.conTwo = environment.conTwo; diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackLowLevelIntegrationTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackLowLevelIntegrationTest.java index cd0b89b48..25435627f 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackLowLevelIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackLowLevelIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2019 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ import org.jxmpp.jid.DomainBareJid; public abstract class AbstractSmackLowLevelIntegrationTest extends AbstractSmackIntTest { - private final SmackIntegrationTestEnvironment environment; + private final SmackIntegrationTestEnvironment environment; /** * The configuration @@ -40,7 +40,7 @@ public abstract class AbstractSmackLowLevelIntegrationTest extends AbstractSmack protected final DomainBareJid service; - protected AbstractSmackLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment) { + protected AbstractSmackLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); this.environment = environment; this.configuration = environment.configuration; diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackSpecificLowLevelIntegrationTest.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackSpecificLowLevelIntegrationTest.java index 6c9bbe219..492f04c7e 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackSpecificLowLevelIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/AbstractSmackSpecificLowLevelIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019 Florian Schmaus + * Copyright 2018-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,19 +28,22 @@ import org.jivesoftware.smack.XMPPException.XMPPErrorException; public abstract class AbstractSmackSpecificLowLevelIntegrationTest extends AbstractSmackLowLevelIntegrationTest { - private final SmackIntegrationTestEnvironment environment; + private final SmackIntegrationTestEnvironment environment; protected final Class connectionClass; private final XmppConnectionDescriptor> connectionDescriptor; - public AbstractSmackSpecificLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment, + public AbstractSmackSpecificLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment, Class connectionClass) { super(environment); this.environment = environment; this.connectionClass = connectionClass; connectionDescriptor = environment.connectionManager.getConnectionDescriptorFor(connectionClass); + if (connectionDescriptor == null) { + throw new IllegalStateException("No connection descriptor for " + connectionClass + " known"); + } } public Class getConnectionClass() { diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java index a6e3d6eda..9b308df3b 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/Configuration.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2018 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,9 @@ import javax.net.ssl.SSLContext; import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.debugger.ConsoleDebugger; +import org.jivesoftware.smack.util.Function; import org.jivesoftware.smack.util.Objects; +import org.jivesoftware.smack.util.ParserUtils; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smackx.debugger.EnhancedDebugger; @@ -94,80 +96,90 @@ public final class Configuration { public final Set disabledTests; + public final String defaultConnectionNickname; + + public final Set enabledConnections; + + public final Set disabledConnections; + public final Set testPackages; public final ConnectionConfigurationBuilderApplier configurationApplier; - private Configuration(DomainBareJid service, String serviceTlsPin, SecurityMode securityMode, int replyTimeout, - Debugger debugger, String accountOneUsername, String accountOnePassword, String accountTwoUsername, - String accountTwoPassword, String accountThreeUsername, String accountThreePassword, Set enabledTests, Set disabledTests, - Set testPackages, String adminAccountUsername, String adminAccountPassword) - throws KeyManagementException, NoSuchAlgorithmException { - this.service = Objects.requireNonNull(service, + public final boolean verbose; + + private Configuration(Configuration.Builder builder) throws KeyManagementException, NoSuchAlgorithmException { + service = Objects.requireNonNull(builder.service, "'service' must be set. Either via 'properties' files or via system property 'sinttest.service'."); - this.serviceTlsPin = serviceTlsPin; + serviceTlsPin = builder.serviceTlsPin; if (serviceTlsPin != null) { tlsContext = Java7Pinning.forPin(serviceTlsPin); } else { tlsContext = null; } - this.securityMode = securityMode; - if (replyTimeout > 0) { - this.replyTimeout = replyTimeout; + securityMode = builder.securityMode; + if (builder.replyTimeout > 0) { + replyTimeout = builder.replyTimeout; } else { - this.replyTimeout = 60000; + replyTimeout = 60000; } - this.debugger = debugger; - if (StringUtils.isNotEmpty(adminAccountUsername, adminAccountPassword)) { + debugger = builder.debugger; + if (StringUtils.isNotEmpty(builder.adminAccountUsername, builder.adminAccountPassword)) { accountRegistration = AccountRegistration.serviceAdministration; } - else if (StringUtils.isNotEmpty(accountOneUsername, accountOnePassword, accountTwoUsername, accountTwoPassword, - accountThreeUsername, accountThreePassword)) { + else if (StringUtils.isNotEmpty(builder.accountOneUsername, builder.accountOnePassword, + builder.accountTwoUsername, builder.accountTwoPassword, builder.accountThreeUsername, + builder.accountThreePassword)) { accountRegistration = AccountRegistration.disabled; } else { accountRegistration = AccountRegistration.inBandRegistration; } - this.adminAccountUsername = adminAccountUsername; - this.adminAccountPassword = adminAccountPassword; + this.adminAccountUsername = builder.adminAccountUsername; + this.adminAccountPassword = builder.adminAccountPassword; - boolean accountOnePasswordSet = StringUtils.isNotEmpty(accountOnePassword); - if (accountOnePasswordSet != StringUtils.isNotEmpty(accountTwoPassword) || - accountOnePasswordSet != StringUtils.isNotEmpty(accountThreePassword)) { + boolean accountOnePasswordSet = StringUtils.isNotEmpty(builder.accountOnePassword); + if (accountOnePasswordSet != StringUtils.isNotEmpty(builder.accountTwoPassword) || + accountOnePasswordSet != StringUtils.isNotEmpty(builder.accountThreePassword)) { // Ensure the invariant that either all main accounts have a password set, or none. throw new IllegalArgumentException(); } - this.accountOneUsername = accountOneUsername; - this.accountOnePassword = accountOnePassword; - this.accountTwoUsername = accountTwoUsername; - this.accountTwoPassword = accountTwoPassword; - this.accountThreeUsername = accountThreeUsername; - this.accountThreePassword = accountThreePassword; - this.enabledTests = enabledTests; - this.disabledTests = disabledTests; - this.testPackages = testPackages; + this.accountOneUsername = builder.accountOneUsername; + this.accountOnePassword = builder.accountOnePassword; + this.accountTwoUsername = builder.accountTwoUsername; + this.accountTwoPassword = builder.accountTwoPassword; + this.accountThreeUsername = builder.accountThreeUsername; + this.accountThreePassword = builder.accountThreePassword; + this.enabledTests = builder.enabledTests; + this.disabledTests = builder.disabledTests; + this.defaultConnectionNickname = builder.defaultConnectionNickname; + this.enabledConnections = builder.enabledConnections; + this.disabledConnections = builder.disabledConnections; + this.testPackages = builder.testPackages; - this.configurationApplier = builder -> { + this.configurationApplier = b -> { if (tlsContext != null) { - builder.setCustomSSLContext(tlsContext); + b.setCustomSSLContext(tlsContext); } - builder.setSecurityMode(securityMode); - builder.setXmppDomain(service); + b.setSecurityMode(securityMode); + b.setXmppDomain(service); switch (debugger) { case enhanced: - builder.setDebuggerFactory(EnhancedDebugger.Factory.INSTANCE); + b.setDebuggerFactory(EnhancedDebugger.Factory.INSTANCE); break; case console: - builder.setDebuggerFactory(ConsoleDebugger.Factory.INSTANCE); + b.setDebuggerFactory(ConsoleDebugger.Factory.INSTANCE); break; case none: // Nothing to do :). break; } }; + + this.verbose = builder.verbose; } public boolean isAccountRegistrationPossible() { @@ -210,8 +222,16 @@ public final class Configuration { private Set disabledTests; + private String defaultConnectionNickname; + + private Set enabledConnections; + + private Set disabledConnections; + private Set testPackages; + private boolean verbose; + private Builder() { } @@ -324,6 +344,21 @@ public final class Configuration { return this; } + public Builder setDefaultConnection(String defaultConnectionNickname) { + this.defaultConnectionNickname = defaultConnectionNickname; + return this; + } + + public Builder setEnabledConnections(String enabledConnectionsString) { + enabledConnections = split(enabledConnectionsString); + return this; + } + + public Builder setDisabledConnections(String disabledConnectionsString) { + disabledConnections = split(disabledConnectionsString); + return this; + } + public Builder addTestPackages(String testPackagesString) { if (testPackagesString != null) { String[] testPackagesArray = testPackagesString.split(","); @@ -350,10 +385,22 @@ public final class Configuration { return this; } + public Builder setVerbose(boolean verbose) { + this.verbose = verbose; + return this; + } + + public Builder setVerbose(String verboseBooleanString) { + if (verboseBooleanString == null) { + return this; + } + + boolean verbose = ParserUtils.parseXmlBoolean(verboseBooleanString); + return setVerbose(verbose); + } + public Configuration build() throws KeyManagementException, NoSuchAlgorithmException { - return new Configuration(service, serviceTlsPin, securityMode, replyTimeout, debugger, accountOneUsername, - accountOnePassword, accountTwoUsername, accountTwoPassword, accountThreeUsername, accountThreePassword, enabledTests, disabledTests, - testPackages, adminAccountUsername, adminAccountPassword); + return new Configuration(this); } } @@ -414,10 +461,15 @@ public final class Configuration { builder.setDebugger(properties.getProperty("debugger")); builder.setEnabledTests(properties.getProperty("enabledTests")); builder.setDisabledTests(properties.getProperty("disabledTests")); + builder.setDefaultConnection(properties.getProperty("defaultConnection")); + builder.setEnabledConnections(properties.getProperty("enabledConnections")); + builder.setDisabledConnections(properties.getProperty("disabledConnections")); builder.addTestPackages(properties.getProperty("testPackages")); builder.addTestPackages(testPackages); + builder.setVerbose(properties.getProperty("verbose")); + return builder.build(); } @@ -437,23 +489,36 @@ public final class Configuration { return null; } - private static Set getTestSetFrom(String string) { - if (string == null) { + private static Set split(String input) { + return split(input, Function.identity()); + } + + private static Set split(String input, Function transformer) { + if (input == null) { return null; } - String[] stringArray = string.split(","); - Set res = new HashSet<>(stringArray.length); - for (String s : stringArray) { - res.add(getFullTestStringFrom(s)); + + String[] inputArray = input.split(","); + Set res = new HashSet<>(inputArray.length); + for (String s : inputArray) { + s = transformer.apply(s); + boolean newElement = res.add(s); + if (!newElement) { + throw new IllegalArgumentException("The argument '" + s + "' was already provided."); + } } + return res; } - private static String getFullTestStringFrom(String string) { - string = string.trim(); - if (string.startsWith("smackx.") || string.startsWith("smack.")) { - string = "org.jivesoftware." + string; - } - return string; + private static Set getTestSetFrom(String input) { + return split(input, s -> { + s = s.trim(); + if (s.startsWith("smackx.") || s.startsWith("smack.")) { + s = "org.jivesoftware." + s; + } + return s; + }); } + } diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestEnvironment.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestEnvironment.java index 58dae7fb9..938c2f52f 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestEnvironment.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestEnvironment.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2019 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,18 +18,18 @@ package org.igniterealtime.smack.inttest; import org.jivesoftware.smack.AbstractXMPPConnection; -public class SmackIntegrationTestEnvironment { +public class SmackIntegrationTestEnvironment { - public final C conOne, conTwo, conThree; + public final AbstractXMPPConnection conOne, conTwo, conThree; public final String testRunId; public final Configuration configuration; - public final XmppConnectionManager connectionManager; + public final XmppConnectionManager connectionManager; - SmackIntegrationTestEnvironment(C conOne, C conTwo, C conThree, String testRunId, - Configuration configuration, XmppConnectionManager connectionManager) { + SmackIntegrationTestEnvironment(AbstractXMPPConnection conOne, AbstractXMPPConnection conTwo, AbstractXMPPConnection conThree, String testRunId, + Configuration configuration, XmppConnectionManager connectionManager) { this.conOne = conOne; this.conTwo = conTwo; this.conThree = conThree; diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java index 36e9afda2..cc1cf1944 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/SmackIntegrationTestFramework.java @@ -23,6 +23,7 @@ import static org.reflections.ReflectionUtils.withParametersCount; import static org.reflections.ReflectionUtils.withReturnType; import java.io.IOException; +import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -43,7 +44,6 @@ import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; @@ -53,7 +53,6 @@ import org.jivesoftware.smack.SmackConfiguration; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.tcp.XMPPTCPConnection; import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.TLSUtils; @@ -70,7 +69,7 @@ import org.reflections.scanners.MethodParameterScanner; import org.reflections.scanners.SubTypesScanner; import org.reflections.scanners.TypeAnnotationsScanner; -public class SmackIntegrationTestFramework { +public class SmackIntegrationTestFramework { static { TLSUtils.setDefaultTrustStoreTypeToJksIfRequired(); @@ -80,14 +79,12 @@ public class SmackIntegrationTestFramework { public static boolean SINTTEST_UNIT_TEST = false; - private final Class defaultConnectionClass; - protected final Configuration config; protected TestRunResult testRunResult; - private SmackIntegrationTestEnvironment environment; - protected XmppConnectionManager connectionManager; + private SmackIntegrationTestEnvironment environment; + protected XmppConnectionManager connectionManager; public enum TestType { Normal, @@ -100,7 +97,7 @@ public class SmackIntegrationTestFramework { IllegalAccessException, IllegalArgumentException, InvocationTargetException { Configuration config = Configuration.newConfiguration(args); - SmackIntegrationTestFramework sinttest = new SmackIntegrationTestFramework<>(config, XMPPTCPConnection.class); + SmackIntegrationTestFramework sinttest = new SmackIntegrationTestFramework(config); TestRunResult testRunResult = sinttest.run(); for (Entry, Throwable> entry : testRunResult.impossibleTestClasses.entrySet()) { @@ -116,11 +113,9 @@ public class SmackIntegrationTestFramework { } final int successfulTests = testRunResult.successfulIntegrationTests.size(); final int failedTests = testRunResult.failedIntegrationTests.size(); - final int totalIntegrationTests = successfulTests + failedTests; final int availableTests = testRunResult.getNumberOfAvailableTests(); - final int possibleTests = testRunResult.getNumberOfPossibleTests(); - LOGGER.info("SmackIntegrationTestFramework[" + testRunResult.testRunId + ']' + ": Finished [" - + successfulTests + '/' + totalIntegrationTests + "] (" + possibleTests + " test methods of " + availableTests + " where possible)"); + LOGGER.info("SmackIntegrationTestFramework[" + testRunResult.testRunId + ']' + " finished: " + + successfulTests + '/' + availableTests + " [" + failedTests + " failed]"); final int exitStatus; if (failedTests > 0) { @@ -146,12 +141,8 @@ public class SmackIntegrationTestFramework { System.exit(exitStatus); } - public SmackIntegrationTestFramework(Configuration configuration, Class defaultConnectionClass) - throws KeyManagementException, InstantiationException, IllegalAccessException, IllegalArgumentException, - InvocationTargetException, NoSuchAlgorithmException, SmackException, IOException, XMPPException, - InterruptedException { + public SmackIntegrationTestFramework(Configuration configuration) { this.config = configuration; - this.defaultConnectionClass = defaultConnectionClass; } public synchronized TestRunResult run() @@ -160,7 +151,7 @@ public class SmackIntegrationTestFramework { testRunResult = new TestRunResult(); // Create a connection manager *after* we created the testRunId (in testRunResult). - this.connectionManager = new XmppConnectionManager<>(this, defaultConnectionClass); + this.connectionManager = new XmppConnectionManager(this); LOGGER.info("SmackIntegrationTestFramework [" + testRunResult.testRunId + ']' + ": Starting"); if (config.debugger != Configuration.Debugger.none) { @@ -226,10 +217,13 @@ public class SmackIntegrationTestFramework { return testRunResult; } - @SuppressWarnings({"unchecked", "Finally"}) + @SuppressWarnings({"Finally"}) private void runTests(Set> classes) throws InterruptedException, InstantiationException, IllegalAccessException, IllegalArgumentException, SmackException, IOException, XMPPException { + List tests = new ArrayList<>(classes.size()); + int numberOfAvailableTests = 0; + for (Class testClass : classes) { final String testClassName = testClass.getName(); @@ -260,17 +254,19 @@ public class SmackIntegrationTestFramework { // - https://discuss.gradle.org/t/main-vs-test-compile-vs-runtime-classpaths-in-eclipse-once-and-for-all-how/17403 // - https://bugs.eclipse.org/bugs/show_bug.cgi?id=376616 (Scope of dependencies has no effect on Eclipse compilation) if (!SINTTEST_UNIT_TEST && testClassName.startsWith("org.igniterealtime.smack.inttest.unittest")) { - LOGGER.finer("Skipping integration test '" + testClassName + "' from src/test classpath"); + LOGGER.warning("Skipping integration test '" + testClassName + "' from src/test classpath (should not be in classpath)"); continue; } if (config.enabledTests != null && !isInSet(testClass, config.enabledTests)) { - LOGGER.info("Skipping test class " + testClassName + " because it is not enabled"); + DisabledTestClass disabledTestClass = new DisabledTestClass(testClass, "Skipping test class " + testClassName + " because it is not enabled"); + testRunResult.disabledTestClasses.add(disabledTestClass); continue; } if (isInSet(testClass, config.disabledTests)) { - LOGGER.info("Skipping test class " + testClassName + " because it is disalbed"); + DisabledTestClass disabledTestClass = new DisabledTestClass(testClass, "Skipping test class " + testClassName + " because it is disalbed"); + testRunResult.disabledTestClasses.add(disabledTestClass); continue; } @@ -301,8 +297,6 @@ public class SmackIntegrationTestFramework { continue; } - testRunResult.numberOfAvailableTestMethods.addAndGet(smackIntegrationTestMethods.size()); - final AbstractSmackIntTest test; try { test = cons.newInstance(environment); @@ -360,12 +354,14 @@ public class SmackIntegrationTestFramework { final String methodName = method.getName(); if (config.enabledTests != null && !(config.enabledTests.contains(methodName) || isInSet(testClass, config.enabledTests))) { - LOGGER.fine("Skipping test method " + methodName + " because it is not enabled"); + DisabledTest disabledTest = new DisabledTest(method, "Skipping test method " + methodName + " because it is not enabled"); + testRunResult.disabledTests.add(disabledTest); it.remove(); continue; } if (config.disabledTests != null && config.disabledTests.contains(methodName)) { - LOGGER.info("Skipping test method " + methodName + " because it is disabled"); + DisabledTest disabledTest = new DisabledTest(method, "Skipping test method " + methodName + " because it is disabled"); + testRunResult.disabledTests.add(disabledTest); it.remove(); continue; } @@ -376,106 +372,77 @@ public class SmackIntegrationTestFramework { continue; } - final int detectedTestMethodsCount = smackIntegrationTestMethods.size(); - testRunResult.numberOfPossibleTestMethods.addAndGet(detectedTestMethodsCount); + List concreteTests = new ArrayList<>(smackIntegrationTestMethods.size()); - try { - // Run the @BeforeClass methods (if any) - Set beforeClassMethods = getAllMethods(testClass, - withAnnotation(BeforeClass.class), withReturnType(Void.TYPE), - withParametersCount(0), withModifier(Modifier.PUBLIC - )); - - // See if there are any methods that have the @BeforeClassAnnotation but a wrong signature - Set allBeforeClassMethods = getAllMethods(testClass, withAnnotation(BeforeClass.class)); - allBeforeClassMethods.removeAll(beforeClassMethods); - if (!allBeforeClassMethods.isEmpty()) { - throw new IllegalArgumentException("@BeforeClass methods with wrong signature found"); + for (Method testMethod : smackIntegrationTestMethods) { + switch (testType) { + case Normal: { + ConcreteTest.Executor concreteTestExecutor = () -> testMethod.invoke(test); + ConcreteTest concreteTest = new ConcreteTest(testType, testMethod, concreteTestExecutor); + concreteTests.add(concreteTest); } - - if (beforeClassMethods.size() == 1) { - Method beforeClassMethod = beforeClassMethods.iterator().next(); - LOGGER.info("Executing @BeforeClass method of " + testClass); - try { - beforeClassMethod.invoke(test); - } - catch (InvocationTargetException | IllegalAccessException e) { - LOGGER.log(Level.SEVERE, "Exception executing @BeforeClass method", e); - } - catch (IllegalArgumentException e) { - throw new AssertionError(e); - } - } - else if (beforeClassMethods.size() > 1) { - throw new IllegalArgumentException("Only one @BeforeClass method allowed"); - } - - for (Method testMethod : smackIntegrationTestMethods) { - List concreteTests = null; + break; + case LowLevel: + case SpecificLowLevel: + LowLevelTestMethod lowLevelTestMethod = new LowLevelTestMethod(testMethod); switch (testType) { - case Normal: { - ConcreteTest.Executor concreteTestExecutor = () -> testMethod.invoke(test); - ConcreteTest concreteTest = new ConcreteTest(testType, testMethod, concreteTestExecutor); - concreteTests = Collections.singletonList(concreteTest); - } - break; case LowLevel: - case SpecificLowLevel: - LowLevelTestMethod lowLevelTestMethod = new LowLevelTestMethod(testMethod); - switch (testType) { - case LowLevel: - concreteTests = invokeLowLevel(lowLevelTestMethod, (AbstractSmackLowLevelIntegrationTest) test); - break; - case SpecificLowLevel: { - ConcreteTest.Executor concreteTestExecutor = () -> invokeSpecificLowLevel( - lowLevelTestMethod, (AbstractSmackSpecificLowLevelIntegrationTest) test); - ConcreteTest concreteTest = new ConcreteTest(testType, testMethod, concreteTestExecutor); - concreteTests = Collections.singletonList(concreteTest); - break; - } - default: - throw new AssertionError(); - } + List concreteLowLevelTests = invokeLowLevel(lowLevelTestMethod, (AbstractSmackLowLevelIntegrationTest) test); + concreteTests.addAll(concreteLowLevelTests); + break; + case SpecificLowLevel: { + ConcreteTest.Executor concreteTestExecutor = () -> invokeSpecificLowLevel( + lowLevelTestMethod, (AbstractSmackSpecificLowLevelIntegrationTest) test); + ConcreteTest concreteTest = new ConcreteTest(testType, testMethod, concreteTestExecutor); + concreteTests.add(concreteTest); break; } - - for (ConcreteTest concreteTest : concreteTests) { - runConcreteTest(concreteTest); + default: + throw new AssertionError(); } + break; } } - finally { - // Run the @AfterClass method (if any) - Set afterClassMethods = getAllMethods(testClass, - withAnnotation(AfterClass.class), withReturnType(Void.TYPE), - withParametersCount(0), withModifier(Modifier.PUBLIC - )); - // See if there are any methods that have the @AfterClassAnnotation but a wrong signature - Set allAfterClassMethods = getAllMethods(testClass, withAnnotation(AfterClass.class)); - allAfterClassMethods.removeAll(afterClassMethods); - if (!allAfterClassMethods.isEmpty()) { - throw new IllegalArgumentException("@AfterClass methods with wrong signature found"); - } + // Instantiate the prepared test early as this will check the before and after class annotations. + PreparedTest preparedTest = new PreparedTest(test, concreteTests); + tests.add(preparedTest); - if (afterClassMethods.size() == 1) { - Method afterClassMethod = afterClassMethods.iterator().next(); - LOGGER.info("Executing @AfterClass method of " + testClass); - try { - afterClassMethod.invoke(test); - } - catch (InvocationTargetException | IllegalAccessException e) { - LOGGER.log(Level.SEVERE, "Exception executing @AfterClass method", e); - } - catch (IllegalArgumentException e) { - throw new AssertionError(e); - } - } - else if (afterClassMethods.size() > 1) { - throw new IllegalArgumentException("Only one @AfterClass method allowed"); - } - } + numberOfAvailableTests += concreteTests.size(); } + + // Print status information. + StringBuilder sb = new StringBuilder(1024); + sb.append("Smack Integration Test Framework\n"); + sb.append("################################\n"); + if (config.verbose) { + sb.append('\n'); + if (!testRunResult.disabledTestClasses.isEmpty()) { + sb.append("The following test classes are disabled:\n"); + for (DisabledTestClass disabledTestClass : testRunResult.disabledTestClasses) { + disabledTestClass.appendTo(sb).append('\n'); + } + } + if (!testRunResult.disabledTests.isEmpty()) { + sb.append("The following tests are disabled:\n"); + for (DisabledTest disabledTest : testRunResult.disabledTests) { + disabledTest.appendTo(sb).append('\n'); + } + } + sb.append('\n'); + } + sb.append("Available tests: ").append(numberOfAvailableTests) + .append("(#-classes: ").append(testRunResult.disabledTestClasses.size()) + .append(", #-tests: ").append(testRunResult.disabledTests.size()) + .append(")\n"); + LOGGER.info(sb.toString()); + + for (PreparedTest test : tests) { + test.run(); + } + + // Assert that all tests in the 'tests' list produced a result. + assert numberOfAvailableTests == testRunResult.getNumberOfAvailableTests(); } private void runConcreteTest(ConcreteTest concreteTest) @@ -524,17 +491,35 @@ public class SmackIntegrationTestFramework { } private List invokeLowLevel(LowLevelTestMethod lowLevelTestMethod, AbstractSmackLowLevelIntegrationTest test) { - Set> connectionClasses; + Collection> connectionDescriptors; if (lowLevelTestMethod.smackIntegrationTestAnnotation.onlyDefaultConnectionType()) { - Class defaultConnectionClass = connectionManager.getDefaultConnectionClass(); - connectionClasses = Collections.singleton(defaultConnectionClass); + XmppConnectionDescriptor defaultConnectionDescriptor = connectionManager.getDefaultConnectionDescriptor(); + connectionDescriptors = Collections.singleton(defaultConnectionDescriptor); } else { - connectionClasses = connectionManager.getConnectionClasses(); + connectionDescriptors = connectionManager.getConnectionDescriptors(); } - List resultingConcreteTests = new ArrayList<>(connectionClasses.size()); + List resultingConcreteTests = new ArrayList<>(connectionDescriptors.size()); + + for (XmppConnectionDescriptor connectionDescriptor : connectionDescriptors) { + String connectionNick = connectionDescriptor.getNickname(); + + if (config.enabledConnections != null && !config.enabledConnections.contains(connectionNick)) { + DisabledTest disabledTest = new DisabledTest(lowLevelTestMethod.testMethod, "Not creating test for " + lowLevelTestMethod + " with connection '" + connectionNick + + "', as this connection type is not enabled"); + testRunResult.disabledTests.add(disabledTest); + continue; + } + + if (config.disabledConnections != null && config.disabledConnections.contains(connectionNick)) { + DisabledTest disabledTest = new DisabledTest(lowLevelTestMethod.testMethod, "Not creating test for " + lowLevelTestMethod + " with connection '" + connectionNick + + ", as this connection type is disabled"); + testRunResult.disabledTests.add(disabledTest); + continue; + } + + Class connectionClass = connectionDescriptor.getConnectionClass(); - for (Class connectionClass : connectionClasses) { ConcreteTest.Executor executor = () -> lowLevelTestMethod.invoke(test, connectionClass); ConcreteTest concreteTest = new ConcreteTest(TestType.LowLevel, lowLevelTestMethod.testMethod, executor, connectionClass.getSimpleName()); resultingConcreteTests.add(concreteTest); @@ -543,7 +528,7 @@ public class SmackIntegrationTestFramework { return resultingConcreteTests; } - private void invokeSpecificLowLevel(LowLevelTestMethod testMethod, + private static void invokeSpecificLowLevel(LowLevelTestMethod testMethod, AbstractSmackSpecificLowLevelIntegrationTest test) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, InterruptedException, SmackException, IOException, XMPPException { @@ -554,7 +539,7 @@ public class SmackIntegrationTestFramework { testMethod.invoke(test, connectionClass); } - protected SmackIntegrationTestEnvironment prepareEnvironment() throws SmackException, + protected SmackIntegrationTestEnvironment prepareEnvironment() throws SmackException, IOException, XMPPException, InterruptedException, KeyManagementException, NoSuchAlgorithmException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { return connectionManager.prepareEnvironment(); @@ -576,9 +561,6 @@ public class SmackIntegrationTestFramework { private static Exception throwFatalException(Throwable e) throws Error, NoResponseException, InterruptedException { - if (e instanceof NoResponseException) { - throw (NoResponseException) e; - } if (e instanceof InterruptedException) { throw (InterruptedException) e; } @@ -612,9 +594,13 @@ public class SmackIntegrationTestFramework { private final List successfulIntegrationTests = Collections.synchronizedList(new LinkedList()); private final List failedIntegrationTests = Collections.synchronizedList(new LinkedList()); private final List impossibleIntegrationTests = Collections.synchronizedList(new LinkedList()); + + // TODO: Ideally three would only be a list of disabledTests, but since we do not process a disabled test class + // any further, we can not determine the concrete disabled tests. + private final List disabledTestClasses = Collections.synchronizedList(new ArrayList<>()); + private final List disabledTests = Collections.synchronizedList(new ArrayList<>()); + private final Map, Throwable> impossibleTestClasses = new HashMap<>(); - private final AtomicInteger numberOfAvailableTestMethods = new AtomicInteger(); - private final AtomicInteger numberOfPossibleTestMethods = new AtomicInteger(); TestRunResult() { } @@ -624,11 +610,7 @@ public class SmackIntegrationTestFramework { } public int getNumberOfAvailableTests() { - return numberOfAvailableTestMethods.get(); - } - - public int getNumberOfPossibleTests() { - return numberOfPossibleTestMethods.get(); + return successfulIntegrationTests.size() + failedIntegrationTests.size() + impossibleIntegrationTests.size(); } public List getSuccessfulTests() { @@ -648,6 +630,77 @@ public class SmackIntegrationTestFramework { } } + final class PreparedTest { + private final AbstractSmackIntTest test; + private final List concreteTests; + + private final Method beforeClassMethod; + private final Method afterClassMethod; + + private PreparedTest(AbstractSmackIntTest test, List concreteTests) { + this.test = test; + this.concreteTests = concreteTests; + Class testClass = test.getClass(); + + beforeClassMethod = getSinttestSpecialMethod(testClass, BeforeClass.class); + afterClassMethod = getSinttestSpecialMethod(testClass, AfterClass.class); + } + + public void run() throws InterruptedException, XMPPException, IOException, SmackException { + try { + // Run the @BeforeClass methods (if any) + executeSinttestSpecialMethod(beforeClassMethod); + + for (ConcreteTest concreteTest : concreteTests) { + runConcreteTest(concreteTest); + } + } + finally { + executeSinttestSpecialMethod(afterClassMethod); + } + } + + private void executeSinttestSpecialMethod(Method method) { + if (method == null) { + return; + } + + try { + method.invoke(test); + } + catch (InvocationTargetException | IllegalAccessException e) { + LOGGER.log(Level.SEVERE, "Exception executing " + method, e); + } + catch (IllegalArgumentException e) { + throw new AssertionError(e); + } + } + } + + @SuppressWarnings("unchecked") + private static Method getSinttestSpecialMethod(Class testClass, Class annotation) { + Set specialClassMethods = getAllMethods(testClass, + withAnnotation(annotation), withReturnType(Void.TYPE), + withParametersCount(0), withModifier(Modifier.PUBLIC + )); + + // See if there are any methods that have a special but a wrong signature + Set allSpecialClassMethods = getAllMethods(testClass, withAnnotation(annotation)); + allSpecialClassMethods.removeAll(specialClassMethods); + if (!allSpecialClassMethods.isEmpty()) { + throw new IllegalArgumentException(annotation + " methods with wrong signature found"); + } + + if (specialClassMethods.size() == 1) { + return specialClassMethods.iterator().next(); + } + else if (specialClassMethods.size() > 1) { + throw new IllegalArgumentException("Only one @BeforeClass method allowed"); + } + + return null; + } + static final class ConcreteTest { private final TestType testType; private final Method method; @@ -705,6 +758,50 @@ public class SmackIntegrationTestFramework { } } + public static final class DisabledTestClass { + private final Class testClass; + private final String reason; + + private DisabledTestClass(Class testClass, String reason) { + this.testClass = testClass; + this.reason = reason; + } + + public Class getTestClass() { + return testClass; + } + + public String getReason() { + return reason; + } + + public StringBuilder appendTo(StringBuilder sb) { + return sb.append("Disabled ").append(testClass).append(" because ").append(reason); + } + } + + public static final class DisabledTest { + private final Method method; + private final String reason; + + private DisabledTest(Method method, String reason) { + this.method = method; + this.reason = reason; + } + + public Method getMethod() { + return method; + } + + public String getReason() { + return reason; + } + + public StringBuilder appendTo(StringBuilder sb) { + return sb.append("Disabled ").append(method).append(" because ").append(reason); + } + } + private final class LowLevelTestMethod { private final Method testMethod; private final SmackIntegrationTest smackIntegrationTestAnnotation; @@ -718,6 +815,7 @@ public class SmackIntegrationTestFramework { parameterListOfConnections = testMethodParametersIsListOfConnections(testMethod); } + // TODO: The second parameter should probably be a connection descriptor? private void invoke(AbstractSmackLowLevelIntegrationTest test, Class connectionClass) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, @@ -746,6 +844,11 @@ public class SmackIntegrationTestFramework { testMethod.invoke(test, connectionsArray); } } + + @Override + public String toString() { + return testMethod.toString(); + } } private static boolean testMethodParametersIsListOfConnections(Method testMethod) { diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionDescriptor.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionDescriptor.java index 29a24aecc..6eaec61e0 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionDescriptor.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionDescriptor.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019 Florian Schmaus + * Copyright 2018-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,8 +29,13 @@ import java.util.List; import org.jivesoftware.smack.AbstractXMPPConnection; import org.jivesoftware.smack.ConnectionConfiguration; import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.util.Consumer; -public class XmppConnectionDescriptor> { +public final class XmppConnectionDescriptor< + C extends AbstractXMPPConnection, + CC extends ConnectionConfiguration, + CCB extends ConnectionConfiguration.Builder +> { private final Class connectionClass; private final Class connectionConfigurationClass; @@ -38,13 +43,18 @@ public class XmppConnectionDescriptor connectionConstructor; private final Method builderMethod; - public XmppConnectionDescriptor(Class connectionClass, Class connectionConfigurationClass) - throws ClassNotFoundException, NoSuchMethodException, SecurityException { - this.connectionClass = connectionClass; - this.connectionConfigurationClass = connectionConfigurationClass; + private final Consumer extraBuilder; - this.connectionConstructor = getConstructor(connectionClass, connectionConfigurationClass); - this.builderMethod = getBuilderMethod(connectionConfigurationClass); + private final String nickname; + + private XmppConnectionDescriptor(Builder builder) throws NoSuchMethodException, SecurityException { + connectionClass = builder.connectionClass; + connectionConfigurationClass = builder.connectionConfigurationClass; + extraBuilder = builder.extraBuilder; + nickname = builder.nickname; + + connectionConstructor = getConstructor(connectionClass, connectionConfigurationClass); + builderMethod = getBuilderMethod(connectionConfigurationClass); } public C construct(Configuration sinttestConfiguration) @@ -65,6 +75,9 @@ public class XmppConnectionDescriptor customConnectionConfigurationAppliers) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { CCB connectionConfigurationBuilder = getNewBuilder(); + if (extraBuilder != null) { + extraBuilder.accept(connectionConfigurationBuilder); + } for (ConnectionConfigurationBuilderApplier customConnectionConfigurationApplier : customConnectionConfigurationAppliers) { customConnectionConfigurationApplier.applyConfigurationTo(connectionConfigurationBuilder); } @@ -88,6 +101,10 @@ public class XmppConnectionDescriptor Constructor getConstructor(Class connectionClass, Class connectionConfigurationClass) throws NoSuchMethodException, SecurityException { @@ -106,4 +123,47 @@ public class XmppConnectionDescriptor> + Builder buildWith(Class connectionClass, Class connectionConfigurationClass) { + return buildWith(connectionClass, connectionConfigurationClass, null); + } + + public static > + Builder buildWith(Class connectionClass, Class connectionConfigurationClass, Class connectionConfigurationBuilderClass) { + return new Builder<>(connectionClass, connectionConfigurationClass, connectionConfigurationBuilderClass); + } + + public static final class Builder> { + private final Class connectionClass; + private final Class connectionConfigurationClass; + + private Consumer extraBuilder; + + private String nickname; + + // The connectionConfigurationBuilderClass merely exists for type-checking purposes. + @SuppressWarnings("UnusedVariable") + private Builder(Class connectionClass, Class connectionConfigurationClass, + Class connectionConfigurationBuilderClass) { + this.connectionClass = connectionClass; + this.connectionConfigurationClass = connectionConfigurationClass; + + nickname = connectionClass.getSimpleName(); + } + + public Builder applyExtraConfguration(Consumer extraBuilder) { + this.extraBuilder = extraBuilder; + return this; + } + + public Builder withNickname(String nickname) { + this.nickname = nickname; + return this; + } + + public XmppConnectionDescriptor build() throws NoSuchMethodException, SecurityException { + return new XmppConnectionDescriptor<>(this); + } + } } diff --git a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java index c576a1948..207ab1c02 100644 --- a/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java +++ b/smack-integration-test/src/main/java/org/igniterealtime/smack/inttest/XmppConnectionManager.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019 Florian Schmaus + * Copyright 2018-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -38,9 +37,11 @@ import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; +import org.jivesoftware.smack.compression.CompressionModuleDescriptor; import org.jivesoftware.smack.tcp.XMPPTCPConnection; import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; -import org.jivesoftware.smack.tcp.XmppNioTcpConnection; import org.jivesoftware.smack.util.MultiMap; import org.jivesoftware.smack.util.StringUtils; @@ -54,55 +55,92 @@ import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Localpart; import org.jxmpp.stringprep.XmppStringprepException; -public class XmppConnectionManager { +public class XmppConnectionManager { private static final Logger LOGGER = Logger.getLogger(XmppConnectionManager.class.getName()); - private static final Map, XmppConnectionDescriptor>> CONNECTION_DESCRIPTORS = new ConcurrentHashMap<>(); + private static final XmppConnectionDescriptor> DEFAULT_CONNECTION_DESCRIPTOR; + + private static final Map>> NICKNAME_CONNECTION_DESCRIPTORS = new HashMap<>(); + + private static final MultiMap< + Class, + XmppConnectionDescriptor> + > CONNECTION_DESCRIPTORS = new MultiMap<>(); static { try { - addConnectionDescriptor(XmppNioTcpConnection.class, XMPPTCPConnectionConfiguration.class); - addConnectionDescriptor(XMPPTCPConnection.class, XMPPTCPConnectionConfiguration.class); - } catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) { + DEFAULT_CONNECTION_DESCRIPTOR = XmppConnectionDescriptor.buildWith(XMPPTCPConnection.class, XMPPTCPConnectionConfiguration.class) + .withNickname("tcp") + .build(); + addConnectionDescriptor(DEFAULT_CONNECTION_DESCRIPTOR); + + addConnectionDescriptor( + XmppConnectionDescriptor.buildWith(ModularXmppClientToServerConnection.class, ModularXmppClientToServerConnectionConfiguration.class) + .withNickname("modular") + .build() + ); + addConnectionDescriptor( + XmppConnectionDescriptor.buildWith(ModularXmppClientToServerConnection.class, ModularXmppClientToServerConnectionConfiguration.class, ModularXmppClientToServerConnectionConfiguration.Builder.class) + .withNickname("modular-nocompress") + .applyExtraConfguration(cb -> cb.removeModule(CompressionModuleDescriptor.class)) + .build() + ); + } catch (NoSuchMethodException | SecurityException e) { throw new AssertionError(e); } } - public static void addConnectionDescriptor(Class connectionClass, - Class connectionConfigurationClass) throws ClassNotFoundException, NoSuchMethodException, SecurityException { - XmppConnectionDescriptor> connectionDescriptor = new XmppConnectionDescriptor<>( - connectionClass, connectionConfigurationClass); - addConnectionDescriptor(connectionDescriptor); - } - - public static void addConnectionDescriptor( + public static boolean addConnectionDescriptor( XmppConnectionDescriptor> connectionDescriptor) { + String nickname = connectionDescriptor.getNickname(); Class connectionClass = connectionDescriptor.getConnectionClass(); - CONNECTION_DESCRIPTORS.put(connectionClass, connectionDescriptor); + + boolean alreadyExisted; + synchronized (CONNECTION_DESCRIPTORS) { + alreadyExisted = removeConnectionDescriptor(nickname); + + CONNECTION_DESCRIPTORS.put(connectionClass, connectionDescriptor); + NICKNAME_CONNECTION_DESCRIPTORS.put(connectionDescriptor.getNickname(), connectionDescriptor); + } + return alreadyExisted; } - public static void removeConnectionDescriptor(Class connectionClass) { - CONNECTION_DESCRIPTORS.remove(connectionClass); + public static boolean removeConnectionDescriptor(String nickname) { + synchronized (CONNECTION_DESCRIPTORS) { + XmppConnectionDescriptor> connectionDescriptor = NICKNAME_CONNECTION_DESCRIPTORS.remove(nickname); + if (connectionDescriptor == null) { + return false; + } + + boolean removed = CONNECTION_DESCRIPTORS.removeOne(connectionDescriptor.getConnectionClass(), connectionDescriptor); + assert removed; + } + + return true; } - private final XmppConnectionDescriptor> defaultConnectionDescriptor; + private final XmppConnectionDescriptor> defaultConnectionDescriptor; - private final Map, XmppConnectionDescriptor>> connectionDescriptors = new HashMap<>( - CONNECTION_DESCRIPTORS.size()); + private final Map>> nicknameConnectionDescriptors; - private final SmackIntegrationTestFramework sinttestFramework; + private final MultiMap< + Class, + XmppConnectionDescriptor> + > connectionDescriptors; + + private final SmackIntegrationTestFramework sinttestFramework; private final Configuration sinttestConfiguration; private final String testRunId; - private final DC accountRegistrationConnection; + private final AbstractXMPPConnection accountRegistrationConnection; private final ServiceAdministrationManager adminManager; private final AccountManager accountManager; /** * One of the three main connections. The type of the main connections is the default connection type. */ - DC conOne, conTwo, conThree; + AbstractXMPPConnection conOne, conTwo, conThree; /** * A pool of authenticated and free to use connections. @@ -114,20 +152,25 @@ public class XmppConnectionManager { */ private final List connections = new ArrayList<>(); - @SuppressWarnings("unchecked") - XmppConnectionManager(SmackIntegrationTestFramework sinttestFramework, - Class defaultConnectionClass) + XmppConnectionManager(SmackIntegrationTestFramework sinttestFramework) throws SmackException, IOException, XMPPException, InterruptedException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + synchronized (CONNECTION_DESCRIPTORS) { + connectionDescriptors = CONNECTION_DESCRIPTORS.clone(); + nicknameConnectionDescriptors = new HashMap<>(NICKNAME_CONNECTION_DESCRIPTORS); + } + this.sinttestFramework = sinttestFramework; this.sinttestConfiguration = sinttestFramework.config; this.testRunId = sinttestFramework.testRunResult.testRunId; - connectionDescriptors.putAll(CONNECTION_DESCRIPTORS); - - defaultConnectionDescriptor = (XmppConnectionDescriptor>) connectionDescriptors.get( - defaultConnectionClass); - if (defaultConnectionDescriptor == null) { - throw new IllegalArgumentException("Could not find a connection descriptor for " + defaultConnectionClass); + String configuredDefaultConnectionNickname = sinttestConfiguration.defaultConnectionNickname; + if (configuredDefaultConnectionNickname != null) { + defaultConnectionDescriptor = nicknameConnectionDescriptors.get(configuredDefaultConnectionNickname); + if (defaultConnectionDescriptor == null) { + throw new IllegalArgumentException("Could not find a connection descriptor for connection nickname '" + configuredDefaultConnectionNickname + "'"); + } + } else { + defaultConnectionDescriptor = DEFAULT_CONNECTION_DESCRIPTOR; } switch (sinttestConfiguration.accountRegistration) { @@ -157,11 +200,11 @@ public class XmppConnectionManager { } } - SmackIntegrationTestEnvironment prepareEnvironment() throws KeyManagementException, NoSuchAlgorithmException, + SmackIntegrationTestEnvironment prepareEnvironment() throws KeyManagementException, NoSuchAlgorithmException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, SmackException, IOException, XMPPException, InterruptedException { prepareMainConnections(); - return new SmackIntegrationTestEnvironment(conOne, conTwo, conThree, + return new SmackIntegrationTestEnvironment(conOne, conTwo, conThree, sinttestFramework.testRunResult.testRunId, sinttestConfiguration, this); } @@ -169,9 +212,9 @@ public class XmppConnectionManager { IllegalAccessException, IllegalArgumentException, InvocationTargetException, SmackException, IOException, XMPPException, InterruptedException { final int mainAccountCount = AccountNum.values().length; - List connections = new ArrayList<>(mainAccountCount); + List connections = new ArrayList<>(mainAccountCount); for (AccountNum mainAccountNum : AccountNum.values()) { - DC mainConnection = getConnectedMainConnectionFor(mainAccountNum); + AbstractXMPPConnection mainConnection = getConnectedMainConnectionFor(mainAccountNum); connections.add(mainConnection); } conOne = connections.get(0); @@ -179,18 +222,18 @@ public class XmppConnectionManager { conThree = connections.get(2); } - public Class getDefaultConnectionClass() { - return defaultConnectionDescriptor.getConnectionClass(); + public XmppConnectionDescriptor> getDefaultConnectionDescriptor() { + return defaultConnectionDescriptor; } - public Set> getConnectionClasses() { - return Collections.unmodifiableSet(connectionDescriptors.keySet()); + public Collection>> getConnectionDescriptors() { + return Collections.unmodifiableCollection(nicknameConnectionDescriptors.values()); } @SuppressWarnings("unchecked") public XmppConnectionDescriptor> getConnectionDescriptorFor( Class connectionClass) { - return (XmppConnectionDescriptor>) connectionDescriptors.get( + return (XmppConnectionDescriptor>) connectionDescriptors.getFirst( connectionClass); } @@ -249,7 +292,7 @@ public class XmppConnectionManager { private static final String USERNAME_PREFIX = "smack-inttest"; - private DC getConnectedMainConnectionFor(AccountNum accountNum) throws SmackException, IOException, XMPPException, + private AbstractXMPPConnection getConnectedMainConnectionFor(AccountNum accountNum) throws SmackException, IOException, XMPPException, InterruptedException, KeyManagementException, NoSuchAlgorithmException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { String middlefix; @@ -283,7 +326,7 @@ public class XmppConnectionManager { registerAccount(finalAccountUsername, finalAccountPassword); } - DC mainConnection = defaultConnectionDescriptor.construct(sinttestConfiguration, builder -> { + AbstractXMPPConnection mainConnection = defaultConnectionDescriptor.construct(sinttestConfiguration, builder -> { try { builder.setUsernameAndPassword(finalAccountUsername, finalAccountPassword) .setResource(middlefix + '-' + testRunId); @@ -347,7 +390,7 @@ public class XmppConnectionManager { @SuppressWarnings("unchecked") XmppConnectionDescriptor> connectionDescriptor = (XmppConnectionDescriptor>) connectionDescriptors - .get(connectionClass); + .getFirst(connectionClass); for (int i = 0; i < count; i++) { C connection = constructConnectedConnection(connectionDescriptor); connections.add(connection); @@ -367,7 +410,7 @@ public class XmppConnectionManager { return connection; } - DC constructConnection() + AbstractXMPPConnection constructConnection() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { return constructConnection(defaultConnectionDescriptor); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/ChatTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/ChatTest.java index ec21761a5..ff058b8e8 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/ChatTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/ChatTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2019 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,7 @@ public class ChatTest extends AbstractSmackIntegrationTest { private boolean invoked; @SuppressWarnings("deprecation") - public ChatTest(SmackIntegrationTestEnvironment environment) { + public ChatTest(SmackIntegrationTestEnvironment environment) { super(environment); chatManagerOne = org.jivesoftware.smack.chat.ChatManager.getInstanceFor(conOne); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/LoginIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/LoginIntegrationTest.java index 07a239b00..f43f18f2b 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/LoginIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/LoginIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2019 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; public class LoginIntegrationTest extends AbstractSmackLowLevelIntegrationTest { - public LoginIntegrationTest(SmackIntegrationTestEnvironment environment) { + public LoginIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/StreamManagementTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/StreamManagementTest.java index a26cf8e17..e5e77e4b0 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/StreamManagementTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/StreamManagementTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2019 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ import org.igniterealtime.smack.inttest.TestNotPossibleException; public class StreamManagementTest extends AbstractSmackSpecificLowLevelIntegrationTest { - public StreamManagementTest(SmackIntegrationTestEnvironment environment) throws Exception { + public StreamManagementTest(SmackIntegrationTestEnvironment environment) throws Exception { super(environment, XMPPTCPConnection.class); XMPPTCPConnection connection = getSpecificUnconnectedConnection(); connection.connect().login(); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/WaitForClosingStreamElementTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/WaitForClosingStreamElementTest.java index 8aa741ecb..50ddf0123 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/WaitForClosingStreamElementTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/WaitForClosingStreamElementTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2019 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; public class WaitForClosingStreamElementTest extends AbstractSmackLowLevelIntegrationTest { - public WaitForClosingStreamElementTest(SmackIntegrationTestEnvironment environment) { + public WaitForClosingStreamElementTest(SmackIntegrationTestEnvironment environment) { super(environment); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/XmppConnectionIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/XmppConnectionIntegrationTest.java index 8ca3c8498..29dc527e6 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/XmppConnectionIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/XmppConnectionIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019 Florian Schmaus + * Copyright 2018-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ package org.jivesoftware.smack; import java.util.List; import java.util.logging.Level; -import org.jivesoftware.smack.tcp.XmppNioTcpConnection; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; import org.igniterealtime.smack.XmppConnectionStressTest; import org.igniterealtime.smack.XmppConnectionStressTest.StressTestFailedException.ErrorsWhileSendingOrReceivingException; @@ -30,7 +30,7 @@ import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; public class XmppConnectionIntegrationTest extends AbstractSmackLowLevelIntegrationTest { - public XmppConnectionIntegrationTest(SmackIntegrationTestEnvironment environment) { + public XmppConnectionIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); } @@ -52,12 +52,12 @@ public class XmppConnectionIntegrationTest extends AbstractSmackLowLevelIntegrat final Level connectionStatsLogLevel = Level.FINE; if (LOGGER.isLoggable(connectionStatsLogLevel)) { - if (connections.get(0) instanceof XmppNioTcpConnection) { + if (connections.get(0) instanceof ModularXmppClientToServerConnection) { for (XMPPConnection connection : connections) { - XmppNioTcpConnection xmppNioTcpConnection = (XmppNioTcpConnection) connection; - XmppNioTcpConnection.Stats stats = xmppNioTcpConnection.getStats(); + ModularXmppClientToServerConnection xmppC2sConnection = (ModularXmppClientToServerConnection) connection; + ModularXmppClientToServerConnection.Stats stats = xmppC2sConnection.getStats(); LOGGER.log(connectionStatsLogLevel, - "Connections stats for " + xmppNioTcpConnection + ":\n{}", + "Connections stats for " + xmppC2sConnection + ":\n{}", stats); } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/AbstractChatIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/AbstractChatIntegrationTest.java index 68db46c5a..3ebf4e852 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/AbstractChatIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/AbstractChatIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018 Florian Schmaus + * Copyright 2018-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ public abstract class AbstractChatIntegrationTest extends AbstractSmackIntegrati protected final ChatManager chatManagerTwo; protected final ChatManager chatManagerThree; - protected AbstractChatIntegrationTest(SmackIntegrationTestEnvironment environment) { + protected AbstractChatIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); chatManagerOne = ChatManager.getInstanceFor(conOne); chatManagerTwo = ChatManager.getInstanceFor(conTwo); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/IncomingMessageListenerIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/IncomingMessageListenerIntegrationTest.java index b031be9d2..ca76ab280 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/IncomingMessageListenerIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/IncomingMessageListenerIntegrationTest.java @@ -26,7 +26,7 @@ import org.jxmpp.jid.EntityBareJid; public class IncomingMessageListenerIntegrationTest extends AbstractChatIntegrationTest { - public IncomingMessageListenerIntegrationTest(SmackIntegrationTestEnvironment environment) { + public IncomingMessageListenerIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/OutgoingMessageListenerIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/OutgoingMessageListenerIntegrationTest.java index c284495fa..6118d2ed2 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/OutgoingMessageListenerIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/chat2/OutgoingMessageListenerIntegrationTest.java @@ -27,7 +27,7 @@ import org.jxmpp.jid.EntityBareJid; public class OutgoingMessageListenerIntegrationTest extends AbstractChatIntegrationTest { - public OutgoingMessageListenerIntegrationTest(SmackIntegrationTestEnvironment environment) { + public OutgoingMessageListenerIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/LowLevelRosterIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/LowLevelRosterIntegrationTest.java index 242491c96..8b73081be 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/LowLevelRosterIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/LowLevelRosterIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2019 Florian Schmaus + * Copyright 2016-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ import org.jxmpp.jid.FullJid; public class LowLevelRosterIntegrationTest extends AbstractSmackLowLevelIntegrationTest { - public LowLevelRosterIntegrationTest(SmackIntegrationTestEnvironment environment) { + public LowLevelRosterIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/RosterIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/RosterIntegrationTest.java index beff7eb1b..6f2d0a8fb 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/RosterIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/roster/RosterIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2019 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ public class RosterIntegrationTest extends AbstractSmackIntegrationTest { private final Roster rosterOne; private final Roster rosterTwo; - public RosterIntegrationTest(SmackIntegrationTestEnvironment environment) { + public RosterIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); rosterOne = Roster.getInstanceFor(conOne); rosterTwo = Roster.getInstanceFor(conTwo); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnectionLowLevelIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnectionLowLevelIntegrationTest.java index f42f04ced..5971ceda6 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnectionLowLevelIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnectionLowLevelIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019 Florian Schmaus + * Copyright 2018-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,21 +22,22 @@ import java.security.NoSuchAlgorithmException; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; import org.igniterealtime.smack.inttest.AbstractSmackSpecificLowLevelIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; -public class XmppNioTcpConnectionLowLevelIntegrationTest extends AbstractSmackSpecificLowLevelIntegrationTest { +public class XmppNioTcpConnectionLowLevelIntegrationTest extends AbstractSmackSpecificLowLevelIntegrationTest { - public XmppNioTcpConnectionLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment) { - super(environment, XmppNioTcpConnection.class); + public XmppNioTcpConnectionLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment) { + super(environment, ModularXmppClientToServerConnection.class); } @SmackIntegrationTest public void testDisconnectAfterConnect() throws KeyManagementException, NoSuchAlgorithmException, SmackException, IOException, XMPPException, InterruptedException { - XmppNioTcpConnection connection = getSpecificUnconnectedConnection(); + ModularXmppClientToServerConnection connection = getSpecificUnconnectedConnection(); connection.connect(); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/caps/EntityCapsTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/caps/EntityCapsTest.java index c3aa5ede0..8a4946e51 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/caps/EntityCapsTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/caps/EntityCapsTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2013-2019 Florian Schmaus + * Copyright 2013-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ public class EntityCapsTest extends AbstractSmackIntegrationTest { private final ServiceDiscoveryManager sdmOne; private final ServiceDiscoveryManager sdmTwo; - public EntityCapsTest(SmackIntegrationTestEnvironment environment) { + public EntityCapsTest(SmackIntegrationTestEnvironment environment) { super(environment); ecmTwo = EntityCapsManager.getInstanceFor(environment.conTwo); sdmOne = ServiceDiscoveryManager.getInstanceFor(environment.conOne); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/chatstate/ChatStateIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/chatstate/ChatStateIntegrationTest.java index c236d8aa4..539bdb03d 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/chatstate/ChatStateIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/chatstate/ChatStateIntegrationTest.java @@ -55,7 +55,7 @@ public class ChatStateIntegrationTest extends AbstractSmackIntegrationTest { }; - public ChatStateIntegrationTest(SmackIntegrationTestEnvironment environment) { + public ChatStateIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/filetransfer/FileTransferIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/filetransfer/FileTransferIntegrationTest.java index 651137694..4a0af1d50 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/filetransfer/FileTransferIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/filetransfer/FileTransferIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2019 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ public class FileTransferIntegrationTest extends AbstractSmackIntegrationTest { private final FileTransferManager ftManagerOne; private final FileTransferManager ftManagerTwo; - public FileTransferIntegrationTest(SmackIntegrationTestEnvironment environment) { + public FileTransferIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); ftManagerOne = FileTransferManager.getInstanceFor(conOne); ftManagerTwo = FileTransferManager.getInstanceFor(conTwo); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java index 6390a2e94..1d2e06914 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/httpfileupload/HttpFileUploadIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017-2019 Florian Schmaus + * Copyright 2017-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ public class HttpFileUploadIntegrationTest extends AbstractSmackIntegrationTest private final HttpFileUploadManager hfumOne; - public HttpFileUploadIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPErrorException, + public HttpFileUploadIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPErrorException, NotConnectedException, NoResponseException, InterruptedException, TestNotPossibleException { super(environment); hfumOne = HttpFileUploadManager.getInstanceFor(conOne); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTControlIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTControlIntegrationTest.java index c6f8b275d..4a346479b 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTControlIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTControlIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2019 Florian Schmaus + * Copyright 2016-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ public class IoTControlIntegrationTest extends AbstractSmackIntegrationTest { private final IoTControlManager IoTControlManagerTwo; - public IoTControlIntegrationTest(SmackIntegrationTestEnvironment environment) { + public IoTControlIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); IoTControlManagerOne = IoTControlManager.getInstanceFor(conOne); IoTControlManagerTwo = IoTControlManager.getInstanceFor(conTwo); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDataIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDataIntegrationTest.java index 1fec14577..dc6fc538e 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDataIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDataIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2019 Florian Schmaus + * Copyright 2016-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ public class IoTDataIntegrationTest extends AbstractSmackIntegrationTest { private final IoTDataManager iotDataManagerTwo; - public IoTDataIntegrationTest(SmackIntegrationTestEnvironment environment) { + public IoTDataIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); iotDataManagerOne = IoTDataManager.getInstanceFor(conOne); iotDataManagerTwo = IoTDataManager.getInstanceFor(conTwo); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDiscoveryIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDiscoveryIntegrationTest.java index 1c1c8071d..92c950a86 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDiscoveryIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iot/IoTDiscoveryIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2019 Florian Schmaus + * Copyright 2016-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ public class IoTDiscoveryIntegrationTest extends AbstractSmackIntegrationTest { private final IoTDiscoveryManager discoveryManagerOne; private final IoTDiscoveryManager discoveryManagerTwo; - public IoTDiscoveryIntegrationTest(SmackIntegrationTestEnvironment environment) throws NoResponseException, + public IoTDiscoveryIntegrationTest(SmackIntegrationTestEnvironment environment) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, TestNotPossibleException { super(environment); discoveryManagerOne = IoTDiscoveryManager.getInstanceFor(conOne); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/VersionIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/VersionIntegrationTest.java index f22194452..577819e11 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/VersionIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/iqversion/VersionIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2019 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; public class VersionIntegrationTest extends AbstractSmackIntegrationTest { - public VersionIntegrationTest(SmackIntegrationTestEnvironment environment) { + public VersionIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java index 0ee3d295b..b0cc99584 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016 Fernando Ramirez, 2018-2019 Florian Schmaus + * Copyright 2016 Fernando Ramirez, 2018-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ public class MamIntegrationTest extends AbstractSmackIntegrationTest { private final MamManager mamManagerConTwo; - public MamIntegrationTest(SmackIntegrationTestEnvironment environment) throws NoResponseException, + public MamIntegrationTest(SmackIntegrationTestEnvironment environment) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, TestNotPossibleException, NotLoggedInException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java index 902c2b97f..afac2dd12 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mood/MoodIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018 Paul Schaub, 2019 Florian Schmaus. + * Copyright 2018 Paul Schaub, 2019-2020 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ public class MoodIntegrationTest extends AbstractSmackIntegrationTest { private final MoodManager mm1; private final MoodManager mm2; - public MoodIntegrationTest(SmackIntegrationTestEnvironment environment) { + public MoodIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); mm1 = MoodManager.getInstanceFor(conOne); mm2 = MoodManager.getInstanceFor(conTwo); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java index c7651a052..aee855f6a 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2019 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ public class MultiUserChatIntegrationTest extends AbstractSmackIntegrationTest { private final MultiUserChatManager mucManagerTwo; private final DomainBareJid mucService; - public MultiUserChatIntegrationTest(SmackIntegrationTestEnvironment environment) + public MultiUserChatIntegrationTest(SmackIntegrationTestEnvironment environment) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, TestNotPossibleException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatLowLevelIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatLowLevelIntegrationTest.java index a47eef25f..ba4abd160 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatLowLevelIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatLowLevelIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2019 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ import org.jxmpp.jid.parts.Resourcepart; public class MultiUserChatLowLevelIntegrationTest extends AbstractSmackLowLevelIntegrationTest { - public MultiUserChatLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment) throws Exception { + public MultiUserChatLowLevelIntegrationTest(SmackIntegrationTestEnvironment environment) throws Exception { super(environment); AbstractXMPPConnection connection = getConnectedConnection(); try { diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoIntegrationTest.java index f16ca3d88..4f944d3fa 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractOmemoIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017-2018 Florian Schmaus, Paul Schaub + * Copyright 2017-2020 Florian Schmaus, Paul Schaub * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import org.igniterealtime.smack.inttest.TestNotPossibleException; */ public abstract class AbstractOmemoIntegrationTest extends AbstractSmackIntegrationTest { - public AbstractOmemoIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { + public AbstractOmemoIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { super(environment); // Test for server support diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java index 68cd5f30c..1e324b77c 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/AbstractTwoUsersOmemoIntegrationTest.java @@ -40,7 +40,7 @@ public abstract class AbstractTwoUsersOmemoIntegrationTest extends AbstractOmemo protected OmemoManager alice, bob; - public AbstractTwoUsersOmemoIntegrationTest(SmackIntegrationTestEnvironment environment) + public AbstractTwoUsersOmemoIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java index 938696441..26ba5ab08 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/MessageEncryptionIntegrationTest.java @@ -37,7 +37,7 @@ import org.igniterealtime.smack.inttest.TestNotPossibleException; */ public class MessageEncryptionIntegrationTest extends AbstractTwoUsersOmemoIntegrationTest { - public MessageEncryptionIntegrationTest(SmackIntegrationTestEnvironment environment) + public MessageEncryptionIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMamDecryptionTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMamDecryptionTest.java index 8a36a2f22..ee39eec90 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMamDecryptionTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/OmemoMamDecryptionTest.java @@ -41,7 +41,7 @@ import org.igniterealtime.smack.inttest.TestNotPossibleException; * Then Bob fetches his Mam archive and decrypts the result. */ public class OmemoMamDecryptionTest extends AbstractTwoUsersOmemoIntegrationTest { - public OmemoMamDecryptionTest(SmackIntegrationTestEnvironment environment) + public OmemoMamDecryptionTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/ReadOnlyDeviceIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/ReadOnlyDeviceIntegrationTest.java index dc13bdb8f..bed2b014d 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/ReadOnlyDeviceIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/ReadOnlyDeviceIntegrationTest.java @@ -35,7 +35,7 @@ import org.igniterealtime.smack.inttest.TestNotPossibleException; public class ReadOnlyDeviceIntegrationTest extends AbstractTwoUsersOmemoIntegrationTest { - public ReadOnlyDeviceIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { + public ReadOnlyDeviceIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { super(environment); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/SessionRenegotiationIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/SessionRenegotiationIntegrationTest.java index fa863f1be..3b4391ba3 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/SessionRenegotiationIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/omemo/SessionRenegotiationIntegrationTest.java @@ -27,7 +27,7 @@ import org.igniterealtime.smack.inttest.TestNotPossibleException; public class SessionRenegotiationIntegrationTest extends AbstractTwoUsersOmemoIntegrationTest { - public SessionRenegotiationIntegrationTest(SmackIntegrationTestEnvironment environment) + public SessionRenegotiationIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, TestNotPossibleException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/AbstractOpenPgpIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/AbstractOpenPgpIntegrationTest.java index d02d4f3f5..31d613c85 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/AbstractOpenPgpIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/AbstractOpenPgpIntegrationTest.java @@ -42,7 +42,7 @@ public abstract class AbstractOpenPgpIntegrationTest extends AbstractSmackIntegr protected final PepManager bobPepManager; protected final PepManager chloePepManager; - protected AbstractOpenPgpIntegrationTest(SmackIntegrationTestEnvironment environment) + protected AbstractOpenPgpIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, TestNotPossibleException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/OXSecretKeyBackupIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/OXSecretKeyBackupIntegrationTest.java index 897dda339..e5e33f39b 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/OXSecretKeyBackupIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox/OXSecretKeyBackupIntegrationTest.java @@ -98,7 +98,7 @@ public class OXSecretKeyBackupIntegrationTest extends AbstractOpenPgpIntegration * @throws InterruptedException if the calling thread was interrupted. * @throws SmackException.NoResponseException if there was no response from the remote entity. */ - public OXSecretKeyBackupIntegrationTest(SmackIntegrationTestEnvironment environment) + public OXSecretKeyBackupIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, TestNotPossibleException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingIntegrationTest.java index 1f68eeb4d..4ca076c2b 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingIntegrationTest.java @@ -89,7 +89,7 @@ public class OXInstantMessagingIntegrationTest extends AbstractOpenPgpIntegratio * @throws TestNotPossibleException if the test is not possible due to lacking server support for PEP. * @throws SmackException.NoResponseException if there was no response from the remote entity. */ - public OXInstantMessagingIntegrationTest(SmackIntegrationTestEnvironment environment) + public OXInstantMessagingIntegrationTest(SmackIntegrationTestEnvironment environment) throws XMPPException.XMPPErrorException, InterruptedException, SmackException.NotConnectedException, TestNotPossibleException, SmackException.NoResponseException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/PingIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/PingIntegrationTest.java index 1d19c5b4f..6bb73b8c1 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/PingIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/ping/PingIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2019 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ import org.jxmpp.jid.Jid; public class PingIntegrationTest extends AbstractSmackIntegrationTest { - public PingIntegrationTest(SmackIntegrationTestEnvironment environment) { + public PingIntegrationTest(SmackIntegrationTestEnvironment environment) { super(environment); } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java index cc5811ad0..a0e0bff6c 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/pubsub/PubSubIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2019 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ public class PubSubIntegrationTest extends AbstractSmackIntegrationTest { private final PubSubManager pubSubManagerOne; - public PubSubIntegrationTest(SmackIntegrationTestEnvironment environment) + public PubSubIntegrationTest(SmackIntegrationTestEnvironment environment) throws TestNotPossibleException, NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { super(environment); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java index c766bc6e5..b8492529b 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java @@ -38,7 +38,7 @@ public class UserTuneIntegrationTest extends AbstractSmackIntegrationTest { private final UserTuneManager utm1; private final UserTuneManager utm2; - public UserTuneIntegrationTest(SmackIntegrationTestEnvironment environment) throws NotLoggedInException { + public UserTuneIntegrationTest(SmackIntegrationTestEnvironment environment) throws NotLoggedInException { super(environment); utm1 = UserTuneManager.getInstanceFor(conOne); utm2 = UserTuneManager.getInstanceFor(conTwo); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/xdata/FormTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/xdata/FormTest.java index cf89a68f2..af49c251a 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/xdata/FormTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/xdata/FormTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2004 Jive Software, 2017-2019 Florian Schmaus. + * Copyright 2004 Jive Software, 2017-2020 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; */ public class FormTest extends AbstractSmackIntegrationTest { - public FormTest(SmackIntegrationTestEnvironment environment) { + public FormTest(SmackIntegrationTestEnvironment environment) { super(environment); } diff --git a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/DummySmackIntegrationTestFramework.java b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/DummySmackIntegrationTestFramework.java index 6617f9b0a..f16e2ba54 100644 --- a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/DummySmackIntegrationTestFramework.java +++ b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/DummySmackIntegrationTestFramework.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2019 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,12 +26,19 @@ import org.jivesoftware.smack.DummyConnection.DummyConnectionConfiguration; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPException; -public class DummySmackIntegrationTestFramework extends SmackIntegrationTestFramework { +public class DummySmackIntegrationTestFramework extends SmackIntegrationTestFramework { + + public static final String DUMMY_CONNECTION_NICKNAME = "dummy"; static { try { - XmppConnectionManager.addConnectionDescriptor(DummyConnection.class, DummyConnectionConfiguration.class); - } catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) { + XmppConnectionManager.addConnectionDescriptor( + XmppConnectionDescriptor + .buildWith(DummyConnection.class, DummyConnectionConfiguration.class) + .withNickname(DUMMY_CONNECTION_NICKNAME) + .build() + ); + } catch (NoSuchMethodException | SecurityException e) { throw new AssertionError(e); } } @@ -39,15 +46,15 @@ public class DummySmackIntegrationTestFramework extends SmackIntegrationTestFram public DummySmackIntegrationTestFramework(Configuration configuration) throws KeyManagementException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchAlgorithmException, SmackException, IOException, XMPPException, InterruptedException { - super(configuration, DummyConnection.class); + super(configuration); testRunResult = new TestRunResult(); } @Override - protected SmackIntegrationTestEnvironment prepareEnvironment() { + protected SmackIntegrationTestEnvironment prepareEnvironment() { DummyConnection dummyConnection = new DummyConnection(); connectionManager.conOne = connectionManager.conTwo = connectionManager.conThree = dummyConnection; - return new SmackIntegrationTestEnvironment(dummyConnection, dummyConnection, dummyConnection, + return new SmackIntegrationTestEnvironment(dummyConnection, dummyConnection, dummyConnection, testRunResult.getTestRunId(), config, null); } diff --git a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestUnitTestUtil.java b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestUnitTestUtil.java index de095b69a..c3cd0a0ab 100644 --- a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestUnitTestUtil.java +++ b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestUnitTestUtil.java @@ -1,6 +1,6 @@ /** * - * Copyright 2015-2017 Florian Schmaus + * Copyright 2015-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,9 @@ public class SmackIntegrationTestUnitTestUtil { Configuration configuration = Configuration.builder() .setService(JidTestUtil.DOMAIN_BARE_JID_1) .setUsernamesAndPassword("dummy1", "dummy1pass", "dummy2", "dummy2pass", "dummy3", "dummy3pass") - .addEnabledTest(unitTest).build(); + .setDefaultConnection(DummySmackIntegrationTestFramework.DUMMY_CONNECTION_NICKNAME) + .addEnabledTest(unitTest) + .build(); // @formatter:on try { return new DummySmackIntegrationTestFramework(configuration); diff --git a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestXmppConnectionManagerTest.java b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestXmppConnectionManagerTest.java index 6cd7d0d1c..1709f2a58 100644 --- a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestXmppConnectionManagerTest.java +++ b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/SmackIntegrationTestXmppConnectionManagerTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019 Florian Schmaus + * Copyright 2018-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,9 @@ import java.lang.reflect.InvocationTargetException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; -import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; -import org.jivesoftware.smack.tcp.XmppNioTcpConnection; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; +import org.jivesoftware.smack.tcp.XmppTcpTransportModuleDescriptor; import org.junit.Test; import org.jxmpp.stringprep.XmppStringprepException; @@ -34,11 +35,19 @@ public class SmackIntegrationTestXmppConnectionManagerTest { public void simpleXmppConnectionDescriptorTest() throws ClassNotFoundException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, KeyManagementException, NoSuchAlgorithmException, XmppStringprepException, InstantiationException { - XmppConnectionDescriptor descriptor - = new XmppConnectionDescriptor<>(XmppNioTcpConnection.class, XMPPTCPConnectionConfiguration.class); + XmppConnectionDescriptor< + ModularXmppClientToServerConnection, + ModularXmppClientToServerConnectionConfiguration, + ModularXmppClientToServerConnectionConfiguration.Builder + > descriptor = XmppConnectionDescriptor.buildWith( + ModularXmppClientToServerConnection.class, + ModularXmppClientToServerConnectionConfiguration.class, + ModularXmppClientToServerConnectionConfiguration.Builder.class) + .applyExtraConfguration(b -> b.removeAllModules().addModule(XmppTcpTransportModuleDescriptor.class)) + .build(); Configuration sinttestConfiguration = Configuration.builder().setService("example.org").build(); - XmppNioTcpConnection connection = descriptor.construct(sinttestConfiguration); + ModularXmppClientToServerConnection connection = descriptor.construct(sinttestConfiguration); assertEquals("example.org", connection.getXMPPServiceDomain().toString()); } diff --git a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/unittest/SmackIntegrationTestFrameworkUnitTest.java b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/unittest/SmackIntegrationTestFrameworkUnitTest.java index a73b432df..9df166d51 100644 --- a/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/unittest/SmackIntegrationTestFrameworkUnitTest.java +++ b/smack-integration-test/src/test/java/org/igniterealtime/smack/inttest/unittest/SmackIntegrationTestFrameworkUnitTest.java @@ -72,7 +72,7 @@ public class SmackIntegrationTestFrameworkUnitTest { public static class ThrowsRuntimeExceptionDummyTest extends AbstractSmackIntegrationTest { - public ThrowsRuntimeExceptionDummyTest(SmackIntegrationTestEnvironment environment) { + public ThrowsRuntimeExceptionDummyTest(SmackIntegrationTestEnvironment environment) { super(environment); } @@ -103,7 +103,7 @@ public class SmackIntegrationTestFrameworkUnitTest { public static final String DESCRIPTIVE_TEXT = "I'm not fatal"; - public ThrowsNonFatalExceptionDummyTest(SmackIntegrationTestEnvironment environment) { + public ThrowsNonFatalExceptionDummyTest(SmackIntegrationTestEnvironment environment) { super(environment); } @@ -131,7 +131,7 @@ public class SmackIntegrationTestFrameworkUnitTest { public static class BeforeAfterClassTest extends AbstractSmackIntegrationTest { - public BeforeAfterClassTest(SmackIntegrationTestEnvironment environment) { + public BeforeAfterClassTest(SmackIntegrationTestEnvironment environment) { super(environment); } diff --git a/smack-java8-full/build.gradle b/smack-java8-full/build.gradle new file mode 100644 index 000000000..8bf5dfe6a --- /dev/null +++ b/smack-java8-full/build.gradle @@ -0,0 +1,48 @@ +description = """\ +Full Smack library for Java SE.""" + +dependencies { + api project(':smack-bosh') + api project(':smack-debug') + api project(':smack-experimental') + api project(':smack-extensions') + api project(':smack-java7') + api project(':smack-legacy') + api project(':smack-omemo') + api project(':smack-omemo-signal') + api project(':smack-openpgp') + api project(':smack-resolver-minidns') + api project(':smack-resolver-minidns-dox') + api project(':smack-tcp') + + testImplementation project(path: ":smack-core", configuration: "testRuntime") +} + +task printModularXmppClientToServerConnectionStateGraph(type: JavaExec) { + classpath sourceSets.main.runtimeClasspath + main 'org.jivesoftware.smack.full.ModularXmppClientToServerConnectionTool' +} + +task generateModularXmppClientToServerConnectionStateGraph(type: JavaExec) { + // TODO: Filter out all files which do not contain the String + // StateDescriptor. + inputs.files file('..').listFiles().findAll {it.name.endsWith('.java')} + outputs.files 'src/javadoc/org/jivesoftware/smack/full/doc-files/ModularXmppClientToServerConnectionStateGraph.dot' + classpath sourceSets.main.runtimeClasspath + main 'org.jivesoftware.smack.full.ModularXmppClientToServerConnectionTool' + args outputs.files +} + +task convertModularXmppClientToServerConnectionStateGraphDotToPng(type: Exec) { + dependsOn generateModularXmppClientToServerConnectionStateGraph + inputs.files 'src/javadoc/org/jivesoftware/smack/full/doc-files/ModularXmppClientToServerConnectionStateGraph.dot' + outputs.files 'src/javadoc/org/jivesoftware/smack/full/doc-files/ModularXmppClientToServerConnectionStateGraph.png' + + executable 'dot' + args "-Tpng", "-o", "${outputs.files.first()}", "${inputs.files.first()}" +} + +task cleanGenerateFiles(type: Delete) { + delete 'src/javadoc/org/jivesoftware/smack/full/doc-files/ModularXmppClientToServerConnectionStateGraph.dot', 'src/javadoc/org/jivesoftware/smack/full/doc-files/ModularXmppClientToServerConnectionStateGraph.png' +} +clean.dependsOn cleanGenerateFiles diff --git a/smack-java8-full/src/javadoc/org/jivesoftware/smack/full/doc-files/.gitignore b/smack-java8-full/src/javadoc/org/jivesoftware/smack/full/doc-files/.gitignore new file mode 100644 index 000000000..59fc6dc8e --- /dev/null +++ b/smack-java8-full/src/javadoc/org/jivesoftware/smack/full/doc-files/.gitignore @@ -0,0 +1,2 @@ +/ModularXmppClientToServerConnectionStateGraph.dot +/ModularXmppClientToServerConnectionStateGraph.png diff --git a/smack-java8-full/src/main/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionTool.java b/smack-java8-full/src/main/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionTool.java new file mode 100644 index 000000000..9455d9e35 --- /dev/null +++ b/smack-java8-full/src/main/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionTool.java @@ -0,0 +1,71 @@ +/** + * + * Copyright 2018-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.full; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintWriter; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; + +import org.jxmpp.stringprep.XmppStringprepException; + +public class ModularXmppClientToServerConnectionTool { + + @SuppressWarnings("DefaultCharset") + public static void main(String[] args) throws XmppStringprepException, FileNotFoundException { + + final PrintWriter pw; + final boolean breakStateName; + + switch (args.length) { + case 0: + pw = new PrintWriter(System.out); + breakStateName = false; + break; + case 1: + Path outputFilePath = Paths.get(args[0]); + File outputFile = outputFilePath.toFile(); + if (outputFile.exists()) { + outputFile.delete(); + } + pw = new PrintWriter(outputFile); + breakStateName = true; + break; + default: + throw new IllegalArgumentException("At most one argument allowed"); + } + + printStateGraph(pw, breakStateName); + pw.flush(); + } + + public static void printStateGraph(PrintWriter pw, boolean breakStateName) throws XmppStringprepException { + ModularXmppClientToServerConnectionConfiguration.Builder configurationBuilder = ModularXmppClientToServerConnectionConfiguration.builder() + .setUsernameAndPassword("user", "password") + .setXmppDomain("example.org"); + + ModularXmppClientToServerConnectionConfiguration configuration = configurationBuilder.build(); + + configuration.printStateGraphInDotFormat(pw, breakStateName); + + pw.flush(); + } + +} diff --git a/smack-java8-full/src/main/java/org/jivesoftware/smack/full/package-info.java b/smack-java8-full/src/main/java/org/jivesoftware/smack/full/package-info.java new file mode 100644 index 000000000..e316eabb0 --- /dev/null +++ b/smack-java8-full/src/main/java/org/jivesoftware/smack/full/package-info.java @@ -0,0 +1,28 @@ +/** + * + * Copyright 2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The Smack XMPP client library. + + *

Modular XMPP c2s connection

+ *

+ * The graph below shows the current states of the modular xmpp client connection. Only some states are final states, + * most states are intermediate states in order to reach a final state. + *

+ * The state graph of XmppNioTcpConnection + */ +package org.jivesoftware.smack.full; diff --git a/smack-java8-full/src/test/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionStateGraphTest.java b/smack-java8-full/src/test/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionStateGraphTest.java new file mode 100644 index 000000000..624679481 --- /dev/null +++ b/smack-java8-full/src/test/java/org/jivesoftware/smack/full/ModularXmppClientToServerConnectionStateGraphTest.java @@ -0,0 +1,83 @@ +/** + * + * Copyright 2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.full; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringReader; +import java.io.StringWriter; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +import org.jivesoftware.smack.util.EqualsUtil; +import org.jivesoftware.smack.util.HashCode; + +import com.google.common.io.Resources; +import org.jgrapht.graph.DefaultEdge; +import org.jgrapht.graph.DirectedPseudograph; +import org.jgrapht.io.DOTImporter; +import org.jgrapht.io.ImportException; +import org.junit.jupiter.api.Test; + +public class ModularXmppClientToServerConnectionStateGraphTest { + + @Test + public void testStateGraphDotOutput() throws IOException, ImportException { + URL stateGraphDotFileUrl = Resources.getResource("state-graph.dot"); + String expectedStateGraphDot = Resources.toString(stateGraphDotFileUrl, StandardCharsets.UTF_8); + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + ModularXmppClientToServerConnectionTool.printStateGraph(pw, false); + String currentStateGraphDot = sw.toString(); + + @SuppressWarnings("serial") + DOTImporter dotImporter = new DOTImporter<>( + (id, attributes) -> id, + (from, to, label, attributes) -> { + return new DefaultEdge() { + @Override + public int hashCode() { + return HashCode.builder() + .append(getSource()) + .append(getTarget()) + .build(); + } + + @Override + public boolean equals(Object other) { + return EqualsUtil.equals(this, other, (b, o) -> + b.append(getSource(), o.getSource()) + .append(getTarget(), o.getTarget()) + ); + } + }; + } + ); + + DirectedPseudograph currentStateGraph = new DirectedPseudograph<>(DefaultEdge.class); + DirectedPseudograph expectedStateGraph = new DirectedPseudograph<>(DefaultEdge.class); + + dotImporter.importGraph(expectedStateGraph, new StringReader(expectedStateGraphDot)); + dotImporter.importGraph(currentStateGraph, new StringReader(currentStateGraphDot)); + + assertEquals(expectedStateGraph, currentStateGraph); + } + +} diff --git a/smack-java8-full/src/test/resources/state-graph.dot b/smack-java8-full/src/test/resources/state-graph.dot new file mode 100644 index 000000000..650f3bff6 --- /dev/null +++ b/smack-java8-full/src/test/resources/state-graph.dot @@ -0,0 +1,36 @@ +digraph { + "Disconnected" -> "LookupRemoteConnectionEndpoints"; + "LookupRemoteConnectionEndpoints" -> "EstablishingTcpConnection"; + "EstablishingTcpConnection" -> "EstablishTls (RFC 6120 ยง 5)" [xlabel="1"]; + "EstablishTls (RFC 6120 ยง 5)" -> "ConnectedButUnauthenticated"; + "ConnectedButUnauthenticated" -> "Bind2 (XEP-0386)" [xlabel="1"]; + "Bind2 (XEP-0386)" -> "AuthenticatedAndResourceBound"; + "AuthenticatedAndResourceBound" -> "Shutdown" [xlabel="1"]; + "Shutdown" -> "CloseConnection"; + "CloseConnection" -> "Disconnected"; + "AuthenticatedAndResourceBound" -> "InstantShutdown" [xlabel="2"]; + "InstantShutdown" -> "CloseConnection"; +"AuthenticatedAndResourceBound" [ style=filled ] +"Bind2 (XEP-0386)" [ style=dashed ] + "ConnectedButUnauthenticated" -> "InstantStreamResumption (XEP-0397)" [xlabel="2"]; + "InstantStreamResumption (XEP-0397)" -> "AuthenticatedAndResourceBound"; +"InstantStreamResumption (XEP-0397)" [ style=dashed ] + "ConnectedButUnauthenticated" -> "SaslAuthentication (RFC 6120 ยง 6)" [xlabel="3"]; + "SaslAuthentication (RFC 6120 ยง 6)" -> "AuthenticatedButUnbound"; + "AuthenticatedButUnbound" -> "Compression (XEP-0138)" [xlabel="1"]; + "Compression (XEP-0138)" -> "AuthenticatedButUnbound"; + "AuthenticatedButUnbound" -> "ResumeStream (XEP-0198)" [xlabel="2"]; + "ResumeStream (XEP-0198)" -> "AuthenticatedAndResourceBound"; +"ResumeStream (XEP-0198)" [ style=dashed ] + "AuthenticatedButUnbound" -> "ResourceBinding (RFC 6120 ยง 7)" [xlabel="3"]; + "ResourceBinding (RFC 6120 ยง 7)" -> "EnableStreamManagement (XEP-0198)" [xlabel="1"]; + "EnableStreamManagement (XEP-0198)" -> "AuthenticatedAndResourceBound"; +"EnableStreamManagement (XEP-0198)" [ style=dashed ] + "ResourceBinding (RFC 6120 ยง 7)" -> "AuthenticatedAndResourceBound" [xlabel="2"]; +"AuthenticatedButUnbound" [ style=bold ] + "ConnectedButUnauthenticated" -> "Shutdown" [xlabel="4"]; + "ConnectedButUnauthenticated" -> "InstantShutdown" [xlabel="5"]; +"ConnectedButUnauthenticated" [ style=filled ] + "EstablishingTcpConnection" -> "ConnectedButUnauthenticated" [xlabel="2"]; +"Disconnected" [ style=filled ] +} diff --git a/smack-repl/build.gradle b/smack-repl/build.gradle index f7f246d6d..dc691a89c 100644 --- a/smack-repl/build.gradle +++ b/smack-repl/build.gradle @@ -10,18 +10,14 @@ ext { } dependencies { - compile project(':smack-tcp') - compile project(':smack-bosh') - compile project(':smack-java7') - compile project(':smack-resolver-minidns') - compile project(':smack-resolver-minidns-dox') - compile project(':smack-extensions') - compile project(':smack-experimental') - compile project(':smack-legacy') - compile project(':smack-integration-test') - compile project(':smack-omemo-signal') + // Smack's integration test framework (sintest) depends on + // smack-java*-full and since we may want to use parts of sinttest + // in the REPL, we simply depend sinttest. + api project(':smack-integration-test') + compile "org.scala-lang:scala-library:$scalaVersion" compile "com.lihaoyi:ammonite_$scalaVersion:1.3.2" + testCompile project(path: ":smack-core", configuration: "testRuntime") } @@ -38,8 +34,3 @@ task printClasspath(dependsOn: assemble) { println sourceSets.main.runtimeClasspath.asPath } } - -task printXmppNioTcpConnectionStateGraph(type: JavaExec) { - classpath sourceSets.main.runtimeClasspath - main 'org.igniterealtime.smack.smackrepl.StateGraph' -} diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/Nio.java b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/Nio.java index 9f73c42c5..d9d766447 100644 --- a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/Nio.java +++ b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/Nio.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018 Florian Schmaus + * Copyright 2018-2020 Florian Schmaus * * This file is part of smack-repl. * @@ -20,20 +20,27 @@ */ package org.igniterealtime.smack.smackrepl; +import java.io.BufferedWriter; 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; import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; +import org.jivesoftware.smack.compression.CompressionModuleDescriptor; 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.tcp.XMPPTCPConnectionConfiguration; -import org.jivesoftware.smack.tcp.XmppNioTcpConnection; +import org.jivesoftware.smack.sm.StreamManagementModuleDescriptor; +import org.jivesoftware.smack.tcp.XmppTcpTransportModuleDescriptor; import org.jxmpp.util.XmppDateTime; @@ -48,10 +55,11 @@ public class Nio { public static void doNio(String username, String password, String service) throws SmackException, IOException, XMPPException, InterruptedException { boolean useTls = true; + boolean useStreamMangement = false; boolean useCompression = true; boolean useFullFlush = true; boolean javaNetDebug = false; - boolean smackDebug = false; + boolean smackDebug = true; if (useFullFlush) { XMPPInputOutputStream.setFlushMethod(FlushMethod.FULL_FLUSH); @@ -75,15 +83,29 @@ public class Nio { smackDebuggerFactory = null; } - XMPPTCPConnectionConfiguration configuration = XMPPTCPConnectionConfiguration.builder() + ModularXmppClientToServerConnectionConfiguration.Builder configurationBuilder = ModularXmppClientToServerConnectionConfiguration.builder() .setUsernameAndPassword(username, password) .setXmppDomain(service) .setDebuggerFactory(smackDebuggerFactory) - .setCompressionEnabled(useCompression) .setSecurityMode(securityMode) - .build(); + .removeAllModules() + .addModule(XmppTcpTransportModuleDescriptor.class); - XmppNioTcpConnection connection = new XmppNioTcpConnection(configuration); + if (useCompression) { + configurationBuilder.addModule(CompressionModuleDescriptor.class); + configurationBuilder.setCompressionEnabled(true); + } + if (useStreamMangement) { + configurationBuilder.addModule(StreamManagementModuleDescriptor.class); + } + + ModularXmppClientToServerConnectionConfiguration configuration = configurationBuilder.build(); + + PrintWriter printWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8))); + configuration.printStateGraphInDotFormat(printWriter, false); + printWriter.flush(); + + ModularXmppClientToServerConnection connection = new ModularXmppClientToServerConnection(configuration); connection.setReplyTimeout(5 * 60 * 1000); @@ -105,7 +127,7 @@ public class Nio { connection.disconnect(); - XmppNioTcpConnection.Stats connectionStats = connection.getStats(); + ModularXmppClientToServerConnection.Stats connectionStats = connection.getStats(); // CHECKSTYLE:OFF System.out.println("NIO successfully finished, yeah!\n" + connectionStats); diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/StateGraph.java b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/StateGraph.java deleted file mode 100644 index 2908c087f..000000000 --- a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/StateGraph.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * - * Copyright 2018 Florian Schmaus - * - * This file is part of smack-repl. - * - * smack-repl is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ -package org.igniterealtime.smack.smackrepl; - -import java.io.PrintWriter; -import java.lang.reflect.InvocationTargetException; -import java.util.Collections; - -import org.jivesoftware.smack.fsm.StateDescriptor; -import org.jivesoftware.smack.fsm.StateDescriptorGraph; -import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex; -import org.jivesoftware.smack.tcp.XmppNioTcpConnection; - -public class StateGraph { - - @SuppressWarnings("DefaultCharset") - public static void main(String[] args) throws InstantiationException, IllegalAccessException, - IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { - GraphVertex stateGraph = StateDescriptorGraph.constructStateDescriptorGraph(XmppNioTcpConnection.getBackwardEdgesStateDescriptors()); - - PrintWriter pw = new PrintWriter(System.out); - - boolean breakStateName = args.length == 0; - - StateDescriptorGraph.stateDescriptorGraphToDot(Collections.singleton(stateGraph), pw, breakStateName); - - pw.flush(); - } - -} diff --git a/smack-resolver-dnsjava/src/main/java/org/jivesoftware/smack/util/dns/dnsjava/DNSJavaResolver.java b/smack-resolver-dnsjava/src/main/java/org/jivesoftware/smack/util/dns/dnsjava/DNSJavaResolver.java index afa4dbd72..dc5e096cf 100644 --- a/smack-resolver-dnsjava/src/main/java/org/jivesoftware/smack/util/dns/dnsjava/DNSJavaResolver.java +++ b/smack-resolver-dnsjava/src/main/java/org/jivesoftware/smack/util/dns/dnsjava/DNSJavaResolver.java @@ -1,6 +1,6 @@ /** * - * Copyright 2013-2018 Florian Schmaus + * Copyright 2013-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ */ package org.jivesoftware.smack.util.dns.dnsjava; -import java.net.InetAddress; import java.util.ArrayList; import java.util.List; @@ -24,10 +23,10 @@ import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; import org.jivesoftware.smack.initializer.SmackInitializer; import org.jivesoftware.smack.util.DNSUtil; import org.jivesoftware.smack.util.dns.DNSResolver; -import org.jivesoftware.smack.util.dns.HostAddress; -import org.jivesoftware.smack.util.dns.SRVRecord; +import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure; import org.minidns.dnsname.DnsName; +import org.minidns.record.SRV; import org.xbill.DNS.Lookup; import org.xbill.DNS.Record; import org.xbill.DNS.TextParseException; @@ -50,21 +49,26 @@ public class DNSJavaResolver extends DNSResolver implements SmackInitializer { } @Override - protected List lookupSRVRecords0(DnsName name, List failedAddresses, DnssecMode dnssecMode) { - List res = new ArrayList<>(); - + protected List lookupSrvRecords0(DnsName name, List lookupFailures, + DnssecMode dnssecMode) { Lookup lookup; try { lookup = new Lookup(name.ace, Type.SRV); } catch (TextParseException e) { - throw new IllegalStateException(e); + RemoteConnectionEndpointLookupFailure failure = new RemoteConnectionEndpointLookupFailure.DnsLookupFailure( + name, e); + lookupFailures.add(failure); + return null; } Record[] recs = lookup.run(); - if (recs == null) - return res; + if (recs == null) { + // TODO: When does this happen? Do we want/need to record a lookup failure? + return null; + } + List res = new ArrayList<>(); for (Record record : recs) { org.xbill.DNS.SRVRecord srvRecord = (org.xbill.DNS.SRVRecord) record; if (srvRecord != null && srvRecord.getTarget() != null) { @@ -73,12 +77,7 @@ public class DNSJavaResolver extends DNSResolver implements SmackInitializer { int priority = srvRecord.getPriority(); int weight = srvRecord.getWeight(); - List hostAddresses = lookupHostAddress0(host, failedAddresses, dnssecMode); - if (shouldContinue(name, host, hostAddresses)) { - continue; - } - - SRVRecord r = new SRVRecord(host, port, priority, weight, hostAddresses); + SRV r = new SRV(priority, weight, port, host); res.add(r); } } diff --git a/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java b/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java index 957d8006c..ea508f0fe 100644 --- a/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java +++ b/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java @@ -1,6 +1,6 @@ /** * - * Copyright 2013-2019 Florian Schmaus + * Copyright 2013-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ */ package org.jivesoftware.smack.util.dns.javax; -import java.net.InetAddress; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; @@ -34,10 +33,10 @@ import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; import org.jivesoftware.smack.initializer.SmackInitializer; import org.jivesoftware.smack.util.DNSUtil; import org.jivesoftware.smack.util.dns.DNSResolver; -import org.jivesoftware.smack.util.dns.HostAddress; -import org.jivesoftware.smack.util.dns.SRVRecord; +import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure; import org.minidns.dnsname.DnsName; +import org.minidns.record.SRV; /** * A DNS resolver (mostly for SRV records), which makes use of the API provided in the javax.* namespace. @@ -84,9 +83,8 @@ public class JavaxResolver extends DNSResolver implements SmackInitializer { } @Override - protected List lookupSRVRecords0(DnsName name, List failedAddresses, DnssecMode dnssecMode) { - List res = null; - + protected List lookupSrvRecords0(DnsName name, List lookupFailures, + DnssecMode dnssecMode) { Attribute srvAttribute; try { Attributes dnsLookup = dirContext.getAttributes(name.ace, new String[] { "SRV" }); @@ -97,14 +95,16 @@ public class JavaxResolver extends DNSResolver implements SmackInitializer { LOGGER.log(Level.FINEST, "No DNS SRV RR found for " + name, e); return null; } catch (NamingException e) { - LOGGER.log(Level.WARNING, "Exception while resolving DNS SRV RR for " + name, e); + RemoteConnectionEndpointLookupFailure failure = new RemoteConnectionEndpointLookupFailure.DnsLookupFailure( + name, e); + lookupFailures.add(failure); return null; } + List res = new ArrayList<>(); try { @SuppressWarnings("unchecked") NamingEnumeration srvRecords = (NamingEnumeration) srvAttribute.getAll(); - res = new ArrayList<>(); while (srvRecords.hasMore()) { String srvRecordString = srvRecords.next(); String[] srvRecordEntries = srvRecordString.split(" "); @@ -118,19 +118,15 @@ public class JavaxResolver extends DNSResolver implements SmackInitializer { if (srvTarget.length() > 0 && srvTarget.charAt(srvTarget.length() - 1) == '.') { srvTarget = srvTarget.substring(0, srvTarget.length() - 1); } - DnsName host = DnsName.from(srvTarget); - List hostAddresses = lookupHostAddress0(host, failedAddresses, dnssecMode); - if (shouldContinue(name, host, hostAddresses)) { - continue; - } - - SRVRecord srvRecord = new SRVRecord(host, port, priority, weight, hostAddresses); + SRV srvRecord = new SRV(priority, weight, port, srvTarget); res.add(srvRecord); } } catch (NamingException e) { - LOGGER.log(Level.SEVERE, "Exception while resolving DNS SRV RR for" + name, e); + RemoteConnectionEndpointLookupFailure failure = new RemoteConnectionEndpointLookupFailure.DnsLookupFailure( + name, e); + lookupFailures.add(failure); } return res; diff --git a/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsResolver.java b/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsResolver.java index 615c6a3e1..e4cec5d44 100644 --- a/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsResolver.java +++ b/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsResolver.java @@ -1,6 +1,6 @@ /** * - * Copyright 2014-2017 Florian Schmaus + * Copyright 2014-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -29,12 +28,12 @@ import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; import org.jivesoftware.smack.initializer.SmackInitializer; import org.jivesoftware.smack.util.DNSUtil; import org.jivesoftware.smack.util.dns.DNSResolver; -import org.jivesoftware.smack.util.dns.HostAddress; -import org.jivesoftware.smack.util.dns.SRVRecord; +import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure; import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE; import org.minidns.dnsmessage.Question; import org.minidns.dnsname.DnsName; +import org.minidns.dnssec.DnssecResultNotAuthenticException; import org.minidns.hla.DnssecResolverApi; import org.minidns.hla.ResolutionUnsuccessfulException; import org.minidns.hla.ResolverApi; @@ -66,44 +65,38 @@ public class MiniDnsResolver extends DNSResolver implements SmackInitializer { } @Override - protected List lookupSRVRecords0(final DnsName name, List failedAddresses, DnssecMode dnssecMode) { + protected Set lookupSrvRecords0(final DnsName name, List lookupFailures, + DnssecMode dnssecMode) { final ResolverApi resolver = getResolver(dnssecMode); SrvResolverResult result; try { result = resolver.resolveSrv(name); } catch (IOException e) { - failedAddresses.add(new HostAddress(name, e)); + RemoteConnectionEndpointLookupFailure failure = new RemoteConnectionEndpointLookupFailure.DnsLookupFailure( + name, e); + lookupFailures.add(failure); return null; } ResolutionUnsuccessfulException resolutionUnsuccessfulException = result.getResolutionUnsuccessfulException(); if (resolutionUnsuccessfulException != null) { - failedAddresses.add(new HostAddress(name, resolutionUnsuccessfulException)); + RemoteConnectionEndpointLookupFailure failure = new RemoteConnectionEndpointLookupFailure.DnsLookupFailure( + name, resolutionUnsuccessfulException); + lookupFailures.add(failure); return null; } - if (shouldAbortIfNotAuthentic(name, dnssecMode, result, failedAddresses)) { + if (shouldAbortIfNotAuthentic(name, dnssecMode, result, lookupFailures)) { return null; } - List res = new LinkedList<>(); - for (SRV srv : result.getAnswers()) { - DnsName hostname = srv.target; - List hostAddresses = lookupHostAddress0(hostname, failedAddresses, dnssecMode); - if (shouldContinue(name, hostname, hostAddresses)) { - continue; - } - - SRVRecord srvRecord = new SRVRecord(hostname, srv.port, srv.priority, srv.weight, hostAddresses); - res.add(srvRecord); - } - - return res; + return result.getAnswers(); } @Override - protected List lookupHostAddress0(final DnsName name, List failedAddresses, DnssecMode dnssecMode) { + protected List lookupHostAddress0(final DnsName name, + List lookupFailures, DnssecMode dnssecMode) { final ResolverApi resolver = getResolver(dnssecMode); final ResolverResult aResult; @@ -113,19 +106,25 @@ public class MiniDnsResolver extends DNSResolver implements SmackInitializer { aResult = resolver.resolve(name, A.class); aaaaResult = resolver.resolve(name, AAAA.class); } catch (IOException e) { - failedAddresses.add(new HostAddress(name, e)); + RemoteConnectionEndpointLookupFailure failure = new RemoteConnectionEndpointLookupFailure.DnsLookupFailure( + name, e); + lookupFailures.add(failure); return null; } if (!aResult.wasSuccessful() && !aaaaResult.wasSuccessful()) { // Both results where not successful. - failedAddresses.add(new HostAddress(name, getExceptionFrom(aResult))); - failedAddresses.add(new HostAddress(name, getExceptionFrom(aaaaResult))); + RemoteConnectionEndpointLookupFailure failureA = new RemoteConnectionEndpointLookupFailure.DnsLookupFailure( + name, getExceptionFrom(aResult)); + lookupFailures.add(failureA); + RemoteConnectionEndpointLookupFailure failureAaaa = new RemoteConnectionEndpointLookupFailure.DnsLookupFailure( + name, getExceptionFrom(aaaaResult)); + lookupFailures.add(failureAaaa); return null; } - if (shouldAbortIfNotAuthentic(name, dnssecMode, aResult, failedAddresses) - || shouldAbortIfNotAuthentic(name, dnssecMode, aaaaResult, failedAddresses)) { + if (shouldAbortIfNotAuthentic(name, dnssecMode, aResult, lookupFailures) + || shouldAbortIfNotAuthentic(name, dnssecMode, aaaaResult, lookupFailures)) { return null; } @@ -194,15 +193,16 @@ public class MiniDnsResolver extends DNSResolver implements SmackInitializer { } private static boolean shouldAbortIfNotAuthentic(DnsName name, DnssecMode dnssecMode, - ResolverResult result, List failedAddresses) { + ResolverResult result, List lookupFailures) { switch (dnssecMode) { case needsDnssec: case needsDnssecAndDane: // Check if the result is authentic data, i.e. there a no reasons the result is unverified. - // TODO: Use ResolverResult.getDnssecResultNotAuthenticException() of newer MiniDNS versions. - if (!result.isAuthenticData()) { - Exception exception = new Exception("DNSSEC verification failed: " + result.getUnverifiedReasons().iterator().next().getReasonString()); - failedAddresses.add(new HostAddress(name, exception)); + DnssecResultNotAuthenticException exception = result.getDnssecResultNotAuthenticException(); + if (exception != null) { + RemoteConnectionEndpointLookupFailure failure = new RemoteConnectionEndpointLookupFailure.DnsLookupFailure( + name, exception); + lookupFailures.add(failure); return true; } break; diff --git a/smack-tcp/Makefile b/smack-tcp/Makefile deleted file mode 100644 index 8decb8444..000000000 --- a/smack-tcp/Makefile +++ /dev/null @@ -1,21 +0,0 @@ -.PHONY := all clean - -GRADLE_QUITE_ARGS := --quiet --console plain - -XMPP_NIO_TCP_CONNECTION_STATE_GRAPH_PNG := src/javadoc/org/jivesoftware/smack/tcp/doc-files/XmppNioTcpConnectionStateGraph.png -XMPP_NIO_TCP_CONNECTION_STATE_GRAPH_DOT := $(XMPP_NIO_TCP_CONNECTION_STATE_GRAPH_PNG:.png=.dot) - -GENERATED_FILES := $(XMPP_NIO_TCP_CONNECTION_STATE_GRAPH_PNG) $(XMPP_NIO_TCP_CONNECTION_STATE_GRAPH_DOT) - -all: $(XMPP_NIO_TCP_CONNECTION_STATE_GRAPH_PNG) - -clean: - rm -f $(GENERATED_FILES) - -%.png: %.dot - dot -Tpng -o $@ $^ - -$(XMPP_NIO_TCP_CONNECTION_STATE_GRAPH_DOT): src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnection.java ../smack-core/src/main/java/org/jivesoftware/smack/fsm/AbstractXmppStateMachineConnection.java - # TODO: This also creates the dot file even if the command - # fails. It would be better if this was not the case. - gradle $(GRADLE_QUITE_ARGS) :smack-repl:printXmppNioTcpConnectionStateGraph > $@ diff --git a/smack-tcp/src/javadoc/org/jivesoftware/smack/tcp/doc-files/.gitignore b/smack-tcp/src/javadoc/org/jivesoftware/smack/tcp/doc-files/.gitignore deleted file mode 100644 index 35616fd31..000000000 --- a/smack-tcp/src/javadoc/org/jivesoftware/smack/tcp/doc-files/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/XmppNioTcpConnectionStateGraph.png -/XmppNioTcpConnectionStateGraph.dot diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementModule.java b/smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementModule.java new file mode 100644 index 000000000..58be31a34 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementModule.java @@ -0,0 +1,141 @@ +/** + * + * Copyright 2019-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.sm; + +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.AuthenticatedAndResourceBoundStateDescriptor; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.AuthenticatedButUnboundStateDescriptor; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.ResourceBindingStateDescriptor; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModule; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.c2s.internal.WalkStateGraphContext; +import org.jivesoftware.smack.compression.CompressionModule.CompressionStateDescriptor; +import org.jivesoftware.smack.fsm.State; +import org.jivesoftware.smack.fsm.StateDescriptor; +import org.jivesoftware.smack.fsm.StateTransitionResult; + +public class StreamManagementModule extends ModularXmppClientToServerConnectionModule { + + protected StreamManagementModule(StreamManagementModuleDescriptor moduleDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + super(moduleDescriptor, connectionInternal); + } + + private boolean useSm = true; + + private boolean useSmResumption = true; + + public static final class EnableStreamManagementStateDescriptor extends StateDescriptor { + + private EnableStreamManagementStateDescriptor() { + super(StreamManagementModule.EnableStreamManagementState.class, 198, StateDescriptor.Property.notImplemented); + + addPredeccessor(ResourceBindingStateDescriptor.class); + addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); + declarePrecedenceOver(AuthenticatedAndResourceBoundStateDescriptor.class); + } + + @Override + protected StreamManagementModule.EnableStreamManagementState constructState(ModularXmppClientToServerConnectionInternal connectionInternal) { + // This is the trick: the module is constructed prior the states, so we get the actual state out of the module by fetching the module from the connection. + StreamManagementModule smModule = connectionInternal.connection.getConnectionModuleFor(StreamManagementModuleDescriptor.class); + return smModule.constructEnableStreamMangementState(this, connectionInternal); + } + + } + + private EnableStreamManagementState constructEnableStreamMangementState( + EnableStreamManagementStateDescriptor enableStreamManagementStateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + return new EnableStreamManagementState(enableStreamManagementStateDescriptor, connectionInternal); + } + + private final class EnableStreamManagementState extends State { + private EnableStreamManagementState(EnableStreamManagementStateDescriptor stateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + super(stateDescriptor, connectionInternal); + } + + @Override + public StateTransitionResult.TransitionImpossible isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { + if (!useSm) { + return new StateTransitionResult.TransitionImpossibleReason("Stream management not enabled"); + } + + return new StateTransitionResult.TransitionImpossibleBecauseNotImplemented(stateDescriptor); + } + + @Override + public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + throw new IllegalStateException("SM not implemented"); + } + } + + public static final class ResumeStreamStateDescriptor extends StateDescriptor { + private ResumeStreamStateDescriptor() { + super(StreamManagementModule.ResumeStreamState.class, 198, StateDescriptor.Property.notImplemented); + + addPredeccessor(AuthenticatedButUnboundStateDescriptor.class); + addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); + declarePrecedenceOver(ResourceBindingStateDescriptor.class); + declareInferiortyTo(CompressionStateDescriptor.class); + } + + @Override + protected StreamManagementModule.ResumeStreamState constructState(ModularXmppClientToServerConnectionInternal connectionInternal) { + StreamManagementModule smModule = connectionInternal.connection.getConnectionModuleFor(StreamManagementModuleDescriptor.class); + return smModule.constructResumeStreamState(this, connectionInternal); + } + + } + + private ResumeStreamState constructResumeStreamState( + ResumeStreamStateDescriptor resumeStreamStateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + return new ResumeStreamState(resumeStreamStateDescriptor, connectionInternal); + } + + private final class ResumeStreamState extends State { + private ResumeStreamState(ResumeStreamStateDescriptor stateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + super(stateDescriptor, connectionInternal); + } + + + @Override + public StateTransitionResult.TransitionImpossible isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { + if (!useSmResumption) { + return new StateTransitionResult.TransitionImpossibleReason("Stream resumption not enabled"); + } + + return new StateTransitionResult.TransitionImpossibleBecauseNotImplemented(stateDescriptor); + } + + @Override + public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) { + throw new IllegalStateException("Stream resumption not implemented"); + } + } + + public void setStreamManagementEnabled(boolean useSm) { + this.useSm = useSm; + } + + public void setStreamResumptionEnabled(boolean useSmResumption) { + this.useSmResumption = useSmResumption; + } + +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementModuleDescriptor.java b/smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementModuleDescriptor.java new file mode 100644 index 000000000..3620ca63a --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/sm/StreamManagementModuleDescriptor.java @@ -0,0 +1,58 @@ +/** + * + * Copyright 2019-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.sm; + +import java.util.HashSet; +import java.util.Set; + +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModuleDescriptor; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.fsm.StateDescriptor; +import org.jivesoftware.smack.sm.StreamManagementModule.EnableStreamManagementStateDescriptor; +import org.jivesoftware.smack.sm.StreamManagementModule.ResumeStreamStateDescriptor; + +public class StreamManagementModuleDescriptor extends ModularXmppClientToServerConnectionModuleDescriptor { + + private static final StreamManagementModuleDescriptor INSTANCE = new StreamManagementModuleDescriptor(); + + @Override + protected Set> getStateDescriptors() { + Set> res = new HashSet<>(); + res.add(EnableStreamManagementStateDescriptor.class); + res.add(ResumeStreamStateDescriptor.class); + return res; + } + + @Override + protected StreamManagementModule constructXmppConnectionModule( + ModularXmppClientToServerConnectionInternal connectionInternal) { + return new StreamManagementModule(this, connectionInternal); + } + + public static class Builder extends ModularXmppClientToServerConnectionModuleDescriptor.Builder { + + protected Builder(ModularXmppClientToServerConnectionConfiguration.Builder connectionConfigurationBuilder) { + super(connectionConfigurationBuilder); + } + + @Override + protected StreamManagementModuleDescriptor build() { + return INSTANCE; + } + } +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/ConnectionAttemptState.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/ConnectionAttemptState.java new file mode 100644 index 000000000..415af19f9 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/ConnectionAttemptState.java @@ -0,0 +1,175 @@ +/** + * + * Copyright 2019-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.tcp; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.jivesoftware.smack.SmackException.ConnectionException; +import org.jivesoftware.smack.SmackException.EndpointConnectionException; +import org.jivesoftware.smack.SynchronizationPoint; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.tcp.XmppTcpTransportModule.EstablishingTcpConnectionState; +import org.jivesoftware.smack.tcp.rce.Rfc6120TcpRemoteConnectionEndpoint; +import org.jivesoftware.smack.util.Async; +import org.jivesoftware.smack.util.rce.RemoteConnectionEndpoint; +import org.jivesoftware.smack.util.rce.RemoteConnectionException; + +public final class ConnectionAttemptState { + + private final ModularXmppClientToServerConnectionInternal connectionInternal; + + private final XmppTcpTransportModule.XmppTcpNioTransport.DiscoveredTcpEndpoints discoveredEndpoints; + + private final EstablishingTcpConnectionState establishingTcpConnectionState; + + // TODO: Check if we can re-use the socket channel in case some InetSocketAddress fail to connect to. + final SocketChannel socketChannel; + + final List> connectionExceptions; + final SynchronizationPoint tcpConnectionEstablishedSyncPoint; + + final Iterator connectionEndpointIterator; + /** The current connection endpoint we are trying */ + Rfc6120TcpRemoteConnectionEndpoint connectionEndpoint; + Iterator inetAddressIterator; + + ConnectionAttemptState(ModularXmppClientToServerConnectionInternal connectionInternal, + XmppTcpTransportModule.XmppTcpNioTransport.DiscoveredTcpEndpoints discoveredEndpoints, + EstablishingTcpConnectionState establishingTcpConnectionState) throws IOException { + this.connectionInternal = connectionInternal; + this.discoveredEndpoints = discoveredEndpoints; + this.establishingTcpConnectionState = establishingTcpConnectionState; + + socketChannel = SocketChannel.open(); + socketChannel.configureBlocking(false); + + connectionEndpointIterator = discoveredEndpoints.result.discoveredRemoteConnectionEndpoints.iterator(); + connectionEndpoint = connectionEndpointIterator.next(); + connectionExceptions = new ArrayList<>(discoveredEndpoints.result.discoveredRemoteConnectionEndpoints.size()); + + tcpConnectionEstablishedSyncPoint = new SynchronizationPoint<>(connectionInternal.connection, + "TCP connection establishment"); + } + + void establishTcpConnection() { + RemoteConnectionEndpoint.InetSocketAddressCoupling address = nextAddress(); + establishTcpConnection(address); + } + + private void establishTcpConnection( + RemoteConnectionEndpoint.InetSocketAddressCoupling address) { + TcpHostEvent.ConnectingToHostEvent connectingToHostEvent = new TcpHostEvent.ConnectingToHostEvent( + establishingTcpConnectionState, address); + connectionInternal.invokeConnectionStateMachineListener(connectingToHostEvent); + + final boolean connected; + final InetSocketAddress inetSocketAddress = address.getInetSocketAddress(); + try { + connected = socketChannel.connect(inetSocketAddress); + } catch (IOException e) { + onIOExceptionWhenEstablishingTcpConnection(e, address); + return; + } + + if (connected) { + TcpHostEvent.ConnectedToHostEvent connectedToHostEvent = new TcpHostEvent.ConnectedToHostEvent( + establishingTcpConnectionState, address, true); + connectionInternal.invokeConnectionStateMachineListener(connectedToHostEvent); + + tcpConnectionEstablishedSyncPoint.reportSuccess(); + return; + } + + try { + connectionInternal.registerWithSelector(socketChannel, SelectionKey.OP_CONNECT, + (selectedChannel, selectedSelectionKey) -> { + SocketChannel selectedSocketChannel = (SocketChannel) selectedChannel; + + boolean finishConnected; + try { + finishConnected = selectedSocketChannel.finishConnect(); + } catch (IOException e) { + Async.go(() -> onIOExceptionWhenEstablishingTcpConnection(e, address)); + return; + } + + if (!finishConnected) { + Async.go(() -> onIOExceptionWhenEstablishingTcpConnection(new IOException("finishConnect() failed"), address)); + return; + } + + TcpHostEvent.ConnectedToHostEvent connectedToHostEvent = new TcpHostEvent.ConnectedToHostEvent( + establishingTcpConnectionState, address, false); + connectionInternal.invokeConnectionStateMachineListener(connectedToHostEvent); + + // Do not set 'state' here, since this is processed by a reactor thread, which doesn't hold + // the objects lock. + tcpConnectionEstablishedSyncPoint.reportSuccess(); + }); + } catch (ClosedChannelException e) { + onIOExceptionWhenEstablishingTcpConnection(e, address); + } + } + + private void onIOExceptionWhenEstablishingTcpConnection(IOException exception, + RemoteConnectionEndpoint.InetSocketAddressCoupling failedAddress) { + RemoteConnectionEndpoint.InetSocketAddressCoupling nextInetSocketAddress = nextAddress(); + if (nextInetSocketAddress == null) { + EndpointConnectionException connectionException = EndpointConnectionException.from( + discoveredEndpoints.result.lookupFailures, connectionExceptions); + tcpConnectionEstablishedSyncPoint.reportFailure(connectionException); + return; + } + + tcpConnectionEstablishedSyncPoint.resetTimeout(); + + RemoteConnectionException rce = new RemoteConnectionException<>( + failedAddress, exception); + connectionExceptions.add(rce); + + TcpHostEvent.ConnectionToHostFailedEvent connectionToHostFailedEvent = new TcpHostEvent.ConnectionToHostFailedEvent( + establishingTcpConnectionState, nextInetSocketAddress, exception); + connectionInternal.invokeConnectionStateMachineListener(connectionToHostFailedEvent); + + establishTcpConnection(nextInetSocketAddress); + } + + private RemoteConnectionEndpoint.InetSocketAddressCoupling nextAddress() { + if (inetAddressIterator == null || !inetAddressIterator.hasNext()) { + if (!connectionEndpointIterator.hasNext()) { + return null; + } + + connectionEndpoint = connectionEndpointIterator.next(); + inetAddressIterator = connectionEndpoint.getInetAddresses().iterator(); + // Every valid connection addresspoint must have a non-empty collection of inet addresses. + assert inetAddressIterator.hasNext(); + } + + InetAddress inetAddress = inetAddressIterator.next(); + + return new RemoteConnectionEndpoint.InetSocketAddressCoupling<>(connectionEndpoint, inetAddress); + } +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/TCPInitializer.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/TCPInitializer.java index 7ecf5b4db..0630af22a 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/TCPInitializer.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/TCPInitializer.java @@ -1,6 +1,6 @@ /** * - * Copyright ยฉ 2014 Florian Schmaus + * Copyright ยฉ 2014-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,17 @@ */ package org.jivesoftware.smack.tcp; +import org.jivesoftware.smack.SmackConfiguration; import org.jivesoftware.smack.initializer.UrlInitializer; +import org.jivesoftware.smack.sm.StreamManagementModuleDescriptor; public class TCPInitializer extends UrlInitializer { + static { + SmackConfiguration.addModule(StreamManagementModuleDescriptor.class); + SmackConfiguration.addModule(XmppTcpTransportModuleDescriptor.class); + } + @Override protected String getProvidersUri() { return "classpath:org.jivesoftware.smack.tcp/smacktcp.providers"; diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/TcpHostEvent.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/TcpHostEvent.java new file mode 100644 index 000000000..8c25987d6 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/TcpHostEvent.java @@ -0,0 +1,79 @@ +/** + * + * Copyright 2019-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.tcp; + +import java.io.IOException; + +import org.jivesoftware.smack.fsm.ConnectionStateEvent.DetailedTransitionIntoInformation; +import org.jivesoftware.smack.fsm.State; +import org.jivesoftware.smack.tcp.rce.Rfc6120TcpRemoteConnectionEndpoint; +import org.jivesoftware.smack.util.rce.RemoteConnectionEndpoint; + +public abstract class TcpHostEvent extends DetailedTransitionIntoInformation { + protected final RemoteConnectionEndpoint.InetSocketAddressCoupling address; + + protected TcpHostEvent(State state, RemoteConnectionEndpoint.InetSocketAddressCoupling address) { + super(state); + this.address = address; + } + + public RemoteConnectionEndpoint.InetSocketAddressCoupling getAddress() { + return address; + } + + @Override + public String toString() { + return super.toString() + ": " + address; + } + + public static final class ConnectingToHostEvent extends TcpHostEvent { + ConnectingToHostEvent(State state, + RemoteConnectionEndpoint.InetSocketAddressCoupling address) { + super(state, address); + } + } + + public static final class ConnectedToHostEvent extends TcpHostEvent { + private final boolean connectionEstablishedImmediately; + + ConnectedToHostEvent(State state, RemoteConnectionEndpoint.InetSocketAddressCoupling address, boolean immediately) { + super(state, address); + this.connectionEstablishedImmediately = immediately; + } + + @Override + public String toString() { + return super.toString() + (connectionEstablishedImmediately ? "" : " not") + " connected immediately"; + } + } + + public static final class ConnectionToHostFailedEvent extends TcpHostEvent { + private final IOException ioException; + + ConnectionToHostFailedEvent(State state, + RemoteConnectionEndpoint.InetSocketAddressCoupling address, + IOException ioException) { + super(state, address); + this.ioException = ioException; + } + + @Override + public String toString() { + return super.toString() + ioException; + } + } +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java index c4ee49ca9..068dc867d 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java @@ -64,6 +64,7 @@ import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException.AlreadyConnectedException; import org.jivesoftware.smack.SmackException.AlreadyLoggedInException; import org.jivesoftware.smack.SmackException.ConnectionException; +import org.jivesoftware.smack.SmackException.EndpointConnectionException; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotLoggedInException; @@ -78,6 +79,7 @@ import org.jivesoftware.smack.XMPPException.StreamErrorException; import org.jivesoftware.smack.compress.packet.Compress; import org.jivesoftware.smack.compress.packet.Compressed; import org.jivesoftware.smack.compression.XMPPInputOutputStream; +import org.jivesoftware.smack.datatypes.UInt16; import org.jivesoftware.smack.filter.StanzaFilter; import org.jivesoftware.smack.packet.Element; import org.jivesoftware.smack.packet.IQ; @@ -105,6 +107,8 @@ import org.jivesoftware.smack.sm.packet.StreamManagement.Resumed; import org.jivesoftware.smack.sm.packet.StreamManagement.StreamManagementFeature; import org.jivesoftware.smack.sm.predicates.Predicate; import org.jivesoftware.smack.sm.provider.ParseStreamManagement; +import org.jivesoftware.smack.tcp.rce.RemoteXmppTcpConnectionEndpoints; +import org.jivesoftware.smack.tcp.rce.Rfc6120TcpRemoteConnectionEndpoint; import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown; import org.jivesoftware.smack.util.Async; import org.jivesoftware.smack.util.CloseableUtil; @@ -112,7 +116,7 @@ import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.TLSUtils; import org.jivesoftware.smack.util.XmlStringBuilder; -import org.jivesoftware.smack.util.dns.HostAddress; +import org.jivesoftware.smack.util.rce.RemoteConnectionException; import org.jivesoftware.smack.xml.SmackXmlParser; import org.jivesoftware.smack.xml.XmlPullParser; import org.jivesoftware.smack.xml.XmlPullParserException; @@ -556,19 +560,23 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } private void connectUsingConfiguration() throws ConnectionException, IOException, InterruptedException { - List failedAddresses = populateHostAddresses(); + RemoteXmppTcpConnectionEndpoints.Result result = RemoteXmppTcpConnectionEndpoints.lookup(config); + + List> connectionExceptions = new ArrayList<>(); + SocketFactory socketFactory = config.getSocketFactory(); ProxyInfo proxyInfo = config.getProxyInfo(); int timeout = config.getConnectTimeout(); if (socketFactory == null) { socketFactory = SocketFactory.getDefault(); } - for (HostAddress hostAddress : hostAddresses) { - Iterator inetAddresses; - String host = hostAddress.getHost(); - int port = hostAddress.getPort(); + for (Rfc6120TcpRemoteConnectionEndpoint endpoint : result.discoveredRemoteConnectionEndpoints) { + Iterator inetAddresses; + String host = endpoint.getHost().toString(); + UInt16 portUint16 = endpoint.getPort(); + int port = portUint16.intValue(); if (proxyInfo == null) { - inetAddresses = hostAddress.getInetAddresses().iterator(); + inetAddresses = endpoint.getInetAddresses().iterator(); assert inetAddresses.hasNext(); innerloop: while (inetAddresses.hasNext()) { @@ -584,7 +592,9 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { try { socket = socketFuture.getOrThrow(); } catch (IOException e) { - hostAddress.setException(inetAddress, e); + RemoteConnectionException rce = new RemoteConnectionException<>( + endpoint, inetAddress, e); + connectionExceptions.add(rce); if (inetAddresses.hasNext()) { continue innerloop; } else { @@ -594,34 +604,36 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { LOGGER.finer("Established TCP connection to " + inetSocketAddress); // We found a host to connect to, return here this.host = host; - this.port = port; + this.port = portUint16; return; } - failedAddresses.add(hostAddress); } else { + // TODO: Move this into the inner-loop above. There appears no reason why we should not try a proxy + // connection to every inet address of each connection endpoint. socket = socketFactory.createSocket(); - StringUtils.requireNotNullNorEmpty(host, "Host of HostAddress " + hostAddress + " must not be null when using a Proxy"); + StringUtils.requireNotNullNorEmpty(host, "Host of endpoint " + endpoint + " must not be null when using a Proxy"); final String hostAndPort = host + " at port " + port; LOGGER.finer("Trying to establish TCP connection via Proxy to " + hostAndPort); try { proxyInfo.getProxySocketConnection().connect(socket, host, port, timeout); } catch (IOException e) { CloseableUtil.maybeClose(socket, LOGGER); - hostAddress.setException(e); - failedAddresses.add(hostAddress); + RemoteConnectionException rce = new RemoteConnectionException<>(endpoint, null, e); + connectionExceptions.add(rce); continue; } LOGGER.finer("Established TCP connection to " + hostAndPort); // We found a host to connect to, return here this.host = host; - this.port = port; + this.port = portUint16; return; } } + // There are no more host addresses to try // throw an exception and report all tried // HostAddresses in the exception - throw ConnectionException.from(failedAddresses); + throw EndpointConnectionException.from(result.lookupFailures, connectionExceptions); } /** @@ -815,7 +827,6 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { */ @Override protected void connectInternal() throws SmackException, IOException, XMPPException, InterruptedException { - closingStreamReceived.init(); // Establishes the TCP connection to the server and does setup the reader and writer. Throws an exception if // there is an error establishing the connection connectUsingConfiguration(); @@ -1125,6 +1136,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { } } catch (Exception e) { + // TODO: Move the call closingStreamReceived.reportFailure(e) into notifyConnectionError? closingStreamReceived.reportFailure(e); // The exception can be ignored if the the connection is 'done' // or if the it was caused because the socket got closed. It can not be ignored if it diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnection.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnection.java deleted file mode 100644 index 94d4abf49..000000000 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppNioTcpConnection.java +++ /dev/null @@ -1,1883 +0,0 @@ -/** - * - * Copyright 2018-2019 Florian Schmaus - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jivesoftware.smack.tcp; - -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.nio.Buffer; -import java.nio.ByteBuffer; -import java.nio.channels.ClosedChannelException; -import java.nio.channels.SelectionKey; -import java.nio.channels.SocketChannel; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.locks.ReentrantLock; -import java.util.logging.Level; -import java.util.logging.Logger; - -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLEngineResult; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLSession; - -import org.jivesoftware.smack.AbstractXmppNioConnection; -import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; -import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.SmackException.ConnectionException; -import org.jivesoftware.smack.SmackException.ConnectionUnexpectedTerminatedException; -import org.jivesoftware.smack.SmackException.NoResponseException; -import org.jivesoftware.smack.SmackException.NotConnectedException; -import org.jivesoftware.smack.SmackException.SecurityRequiredByClientException; -import org.jivesoftware.smack.SmackException.SecurityRequiredByServerException; -import org.jivesoftware.smack.SmackException.SmackWrappedException; -import org.jivesoftware.smack.SmackReactor.ChannelSelectedCallback; -import org.jivesoftware.smack.SmackReactor.SelectionKeyAttachment; -import org.jivesoftware.smack.SynchronizationPoint; -import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.XMPPException.FailedNonzaException; -import org.jivesoftware.smack.XMPPException.XMPPErrorException; -import org.jivesoftware.smack.XmppInputOutputFilter; -import org.jivesoftware.smack.fsm.ConnectionStateEvent.DetailedTransitionIntoInformation; -import org.jivesoftware.smack.fsm.StateDescriptor; -import org.jivesoftware.smack.fsm.StateDescriptorGraph; -import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex; -import org.jivesoftware.smack.packet.Nonza; -import org.jivesoftware.smack.packet.Stanza; -import org.jivesoftware.smack.packet.StartTls; -import org.jivesoftware.smack.packet.StreamClose; -import org.jivesoftware.smack.packet.StreamOpen; -import org.jivesoftware.smack.packet.TlsFailure; -import org.jivesoftware.smack.packet.TlsProceed; -import org.jivesoftware.smack.packet.TopLevelStreamElement; -import org.jivesoftware.smack.sasl.SASLErrorException; -import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown; -import org.jivesoftware.smack.util.Async; -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.dns.HostAddress; -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.impl.JidCreate; -import org.jxmpp.jid.util.JidUtil; -import org.jxmpp.stringprep.XmppStringprepException; -import org.jxmpp.xml.splitter.Utf8ByteXmppXmlSplitter; -import org.jxmpp.xml.splitter.XmlPrettyPrinter; -import org.jxmpp.xml.splitter.XmlPrinter; -import org.jxmpp.xml.splitter.XmppElementCallback; -import org.jxmpp.xml.splitter.XmppXmlSplitter; - -/** - * Represents and manages a client connection to an XMPP server via TCP. - * - *

Smack XMPP TCP NIO connection states

- *

- * The graph below shows the current graph of states of this XMPP connection. Only some states are final states, most - * states are intermediate states in order to reach a final state. - *

- * The state graph of XmppNioTcpConnection - * - */ -public class XmppNioTcpConnection extends AbstractXmppNioConnection { - - private static final Logger LOGGER = Logger.getLogger(XmppNioTcpConnection.class.getName()); - - private static final Set> BACKWARD_EDGES_STATE_DESCRIPTORS = new HashSet<>(); - - static final GraphVertex INITIAL_STATE_DESCRIPTOR_VERTEX; - - static { - BACKWARD_EDGES_STATE_DESCRIPTORS.add(LookupHostAddressesStateDescriptor.class); - BACKWARD_EDGES_STATE_DESCRIPTORS.add(EnableStreamManagementStateDescriptor.class); - BACKWARD_EDGES_STATE_DESCRIPTORS.add(ResumeStreamStateDescriptor.class); - BACKWARD_EDGES_STATE_DESCRIPTORS.add(InstantStreamResumptionStateDescriptor.class); - BACKWARD_EDGES_STATE_DESCRIPTORS.add(Bind2StateDescriptor.class); - BACKWARD_EDGES_STATE_DESCRIPTORS.add(InstantShutdownStateDescriptor.class); - BACKWARD_EDGES_STATE_DESCRIPTORS.add(ShutdownStateDescriptor.class); - - try { - INITIAL_STATE_DESCRIPTOR_VERTEX = StateDescriptorGraph.constructStateDescriptorGraph(BACKWARD_EDGES_STATE_DESCRIPTORS); - } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException - | NoSuchMethodException | SecurityException e) { - throw new IllegalStateException(e); - } - } - - private static final int CALLBACK_MAX_BYTES_READ = 10 * 1024 * 1024; - private static final int CALLBACK_MAX_BYTES_WRITEN = CALLBACK_MAX_BYTES_READ; - - private static final int MAX_ELEMENT_SIZE = 64 * 1024; - - private SelectionKey selectionKey; - private SelectionKeyAttachment selectionKeyAttachment; - private SocketChannel socketChannel; - private InetSocketAddress remoteAddress; - private TlsState tlsState; - - /** - * Note that this field is effective final, but due to https://stackoverflow.com/q/30360824/194894 we have to declare it non-final. - */ - private Utf8ByteXmppXmlSplitter splitter; - - /** - * Note that this field is effective final, but due to https://stackoverflow.com/q/30360824/194894 we have to declare it non-final. - */ - private XmppXmlSplitter outputDebugSplitter; - - private static final Level STREAM_OPEN_CLOSE_DEBUG_LOG_LEVEL = Level.FINER; - - private final XmppElementCallback xmppElementCallback = new XmppElementCallback() { - private String streamOpen; - private String streamClose; - - @Override - public void onCompleteElement(String completeElement) { - assert streamOpen != null; - assert streamClose != null; - - if (debugger != null) { - debugger.onIncomingElementCompleted(); - } - - String wrappedCompleteElement = streamOpen + completeElement + streamClose; - try { - parseAndProcessElement(wrappedCompleteElement); - } catch (Exception e) { - notifyConnectionError(e); - } - } - - - @Override - public void streamOpened(String prefix, Map attributes) { - if (LOGGER.isLoggable(STREAM_OPEN_CLOSE_DEBUG_LOG_LEVEL)) { - LOGGER.log(STREAM_OPEN_CLOSE_DEBUG_LOG_LEVEL, - "Stream of " + this + " opened. prefix=" + prefix + " attributes=" + attributes); - } - - final String prefixXmlns = "xmlns:" + prefix; - final StringBuilder streamClose = new StringBuilder(32); - final StringBuilder streamOpen = new StringBuilder(256); - - streamOpen.append('<'); - streamClose.append(""); - for (Entry entry : attributes.entrySet()) { - String attributeName = entry.getKey(); - String attributeValue = entry.getValue(); - switch (attributeName) { - case "id": - streamId = attributeValue; - break; - case "version": - break; - case "xml:lang": - streamOpen.append(" xml:lang='").append(attributeValue).append('\''); - break; - case "to": - break; - case "from": - DomainBareJid reportedServerDomain; - try { - reportedServerDomain = JidCreate.domainBareFrom(attributeValue); - } catch (XmppStringprepException e) { - IllegalStateException ise = new IllegalStateException( - "Reporting server domain '" + attributeValue + "' is not a valid JID", e); - notifyConnectionError(ise); - return; - } - assert config.getXMPPServiceDomain().equals(reportedServerDomain); - break; - case "xmlns": - streamOpen.append(" xmlns='").append(attributeValue).append('\''); - break; - default: - if (attributeName.equals(prefixXmlns)) { - streamOpen.append(' ').append(prefixXmlns).append("='").append(attributeValue).append('\''); - break; - } - LOGGER.info("Unknown attribute: " + attributeName); - break; - } - } - streamOpen.append('>'); - - 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); - } - onStreamOpen(streamOpenParser); - } - - @Override - public void streamClosed() { - if (LOGGER.isLoggable(STREAM_OPEN_CLOSE_DEBUG_LOG_LEVEL)) { - LOGGER.log(STREAM_OPEN_CLOSE_DEBUG_LOG_LEVEL, "Stream of " + this + " closed"); - } - - closingStreamReceived.reportSuccess(); - } - }; - - private final ArrayBlockingQueueWithShutdown outgoingElementsQueue = new ArrayBlockingQueueWithShutdown<>( - 100, true); - - private Iterator outgoingCharSequenceIterator; - - private final List currentlyOutgoingElements = new ArrayList<>(); - private final Map> bufferToElementMap = new IdentityHashMap<>(); - - private ByteBuffer outgoingBuffer; - private ByteBuffer filteredOutgoingBuffer; - private final List networkOutgoingBuffers = new ArrayList<>(); - private long networkOutgoingBuffersBytes; - - // TODO: Make the size of the incomingBuffer configurable. - private final ByteBuffer incomingBuffer = ByteBuffer.allocateDirect(2 * 4096); - - private final ReentrantLock channelSelectedCallbackLock = new ReentrantLock(); - - private long totalBytesRead; - private long totalBytesWritten; - private long totalBytesReadAfterFilter; - private long totalBytesWrittenBeforeFilter; - private long handledChannelSelectedCallbacks; - private long callbackPreemtBecauseBytesWritten; - private long callbackPreemtBecauseBytesRead; - private int sslEngineDelegatedTasks; - private int maxPendingSslEngineDelegatedTasks; - - // TODO: Use LongAdder once Smack's minimum Android API level is 24 or higher. - private final AtomicLong setWriteInterestAfterChannelSelectedCallback = new AtomicLong(); - private final AtomicLong reactorThreadAlreadyRacing = new AtomicLong(); - private final AtomicLong afterOutgoingElementsQueueModifiedSetInterestOps = new AtomicLong(); - private final AtomicLong rejectedChannelSelectedCallbacks = new AtomicLong(); - - private Jid lastDestinationAddress; - - private boolean pendingInputFilterData; - private boolean pendingOutputFilterData; - - private boolean pendingWriteInterestAfterRead; - - private boolean useDirectTls = false; - - private boolean useSm = false; - private boolean useSmResumption = false; - private boolean useIsr = false; - - private boolean useBind2 = false; - - public XmppNioTcpConnection(XMPPTCPConnectionConfiguration configuration) { - super(configuration, INITIAL_STATE_DESCRIPTOR_VERTEX); - - XmlPrinter incomingDebugPrettyPrinter = null; - if (debugger != null) { - // Incoming stream debugging. - incomingDebugPrettyPrinter = XmlPrettyPrinter.builder() - .setPrettyWriter(sb -> debugger.incomingStreamSink(sb)) - .build(); - - // Outgoing stream debugging. - XmlPrinter outgoingDebugPrettyPrinter = XmlPrettyPrinter.builder() - .setPrettyWriter(sb -> debugger.outgoingStreamSink(sb)) - .build(); - outputDebugSplitter = new XmppXmlSplitter(outgoingDebugPrettyPrinter); - } - - XmppXmlSplitter xmppXmlSplitter = new XmppXmlSplitter(MAX_ELEMENT_SIZE, xmppElementCallback, - incomingDebugPrettyPrinter); - splitter = new Utf8ByteXmppXmlSplitter(xmppXmlSplitter); - } - - private final ChannelSelectedCallback channelSelectedCallback = - (selectedChannel, selectedSelectionKey) -> { - assert selectionKey == null || selectionKey == selectedSelectionKey; - SocketChannel selectedSocketChannel = (SocketChannel) selectedChannel; - // We are *always* interested in OP_READ. - int newInterestedOps = SelectionKey.OP_READ; - boolean newPendingOutputFilterData = false; - - if (!channelSelectedCallbackLock.tryLock()) { - rejectedChannelSelectedCallbacks.incrementAndGet(); - return; - } - - // LOGGER.info("Accepted channel selected callback"); - - handledChannelSelectedCallbacks++; - - long callbackBytesRead = 0; - long callbackBytesWritten = 0; - - try { - boolean destinationAddressChanged = false; - boolean isLastPartOfElement = false; - TopLevelStreamElement currentlyOutgonigTopLevelStreamElement = null; - StringBuilder outgoingStreamForDebugger = null; - - writeLoop: while (true) { - final boolean moreDataAvailable = !isLastPartOfElement || !outgoingElementsQueue.isEmpty(); - - if (filteredOutgoingBuffer != null || !networkOutgoingBuffers.isEmpty()) { - if (filteredOutgoingBuffer != null) { - networkOutgoingBuffers.add(filteredOutgoingBuffer); - networkOutgoingBuffersBytes += filteredOutgoingBuffer.remaining(); - - filteredOutgoingBuffer = null; - if (moreDataAvailable && networkOutgoingBuffersBytes < 8096) { - continue; - } - } - - ByteBuffer[] output = networkOutgoingBuffers.toArray(new ByteBuffer[networkOutgoingBuffers.size()]); - long bytesWritten; - try { - bytesWritten = selectedSocketChannel.write(output); - } catch (IOException e) { - // We have seen here so far - // - IOException "Broken pipe" - handleReadWriteIoException(e); - break; - } - - if (bytesWritten == 0) { - newInterestedOps |= SelectionKey.OP_WRITE; - break; - } - - callbackBytesWritten += bytesWritten; - - networkOutgoingBuffersBytes -= bytesWritten; - - List prunedBuffers = pruneBufferList(networkOutgoingBuffers); - - for (Buffer prunedBuffer : prunedBuffers) { - List sendElements = bufferToElementMap.remove(prunedBuffer); - if (sendElements == null) { - continue; - } - for (TopLevelStreamElement elementJustSend : sendElements) { - firePacketSendingListeners(elementJustSend); - } - } - - // Prevent one callback from dominating the reactor thread. Break out of the write-loop if we have - // written a certain amount. - if (callbackBytesWritten > CALLBACK_MAX_BYTES_WRITEN) { - newInterestedOps |= SelectionKey.OP_WRITE; - callbackPreemtBecauseBytesWritten++; - break; - } - } else if (outgoingBuffer != null || pendingOutputFilterData) { - pendingOutputFilterData = false; - - if (outgoingBuffer != null) { - totalBytesWrittenBeforeFilter += outgoingBuffer.remaining(); - if (isLastPartOfElement) { - assert currentlyOutgonigTopLevelStreamElement != null; - currentlyOutgoingElements.add(currentlyOutgonigTopLevelStreamElement); - } - } - - ByteBuffer outputFilterInputData = outgoingBuffer; - // We can now null the outgoingBuffer since the filter step will take care of it from now on. - outgoingBuffer = null; - - for (ListIterator it = getXmppInputOutputFilterBeginIterator(); it.hasNext();) { - XmppInputOutputFilter inputOutputFilter = it.next(); - XmppInputOutputFilter.OutputResult outputResult; - try { - outputResult = inputOutputFilter.output(outputFilterInputData, isLastPartOfElement, - destinationAddressChanged, moreDataAvailable); - } catch (IOException e) { - notifyConnectionError(e); - break writeLoop; - } - newPendingOutputFilterData |= outputResult.pendingFilterData; - outputFilterInputData = outputResult.filteredOutputData; - if (outputFilterInputData != null) { - outputFilterInputData.flip(); - } - } - - // It is ok if outpuFilterInputData is 'null' here, this is expected behavior. - if (outputFilterInputData != null && outputFilterInputData.hasRemaining()) { - filteredOutgoingBuffer = outputFilterInputData; - } else { - filteredOutgoingBuffer = null; - } - - // If the filters did eventually not produce any output data but if there is - // pending output data then we have a pending write request after read. - if (filteredOutgoingBuffer == null && newPendingOutputFilterData) { - pendingWriteInterestAfterRead = true; - } - - if (filteredOutgoingBuffer != null && isLastPartOfElement) { - bufferToElementMap.put(filteredOutgoingBuffer, new ArrayList<>(currentlyOutgoingElements)); - currentlyOutgoingElements.clear(); - } - - // Reset that the destination address has changed. - if (destinationAddressChanged) { - destinationAddressChanged = false; - } - } else if (outgoingCharSequenceIterator != null) { - CharSequence nextCharSequence = outgoingCharSequenceIterator.next(); - outgoingBuffer = UTF8.encode(nextCharSequence); - if (!outgoingCharSequenceIterator.hasNext()) { - outgoingCharSequenceIterator = null; - isLastPartOfElement = true; - } else { - isLastPartOfElement = false; - } - - if (debugger != null) { - if (outgoingStreamForDebugger == null) { - outgoingStreamForDebugger = new StringBuilder(); - } - outgoingStreamForDebugger.append(nextCharSequence); - - if (isLastPartOfElement) { - try { - outputDebugSplitter.append(outgoingStreamForDebugger); - } catch (IOException e) { - throw new AssertionError(e); - } - debugger.onOutgoingElementCompleted(); - outgoingStreamForDebugger = null; - } - } - } else if (!outgoingElementsQueue.isEmpty()) { - currentlyOutgonigTopLevelStreamElement = outgoingElementsQueue.poll(); - if (currentlyOutgonigTopLevelStreamElement instanceof Stanza) { - Stanza currentlyOutgoingStanza = (Stanza) currentlyOutgonigTopLevelStreamElement; - Jid currentDestinationAddress = currentlyOutgoingStanza.getTo(); - destinationAddressChanged = !JidUtil.equals(lastDestinationAddress, currentDestinationAddress); - lastDestinationAddress = currentDestinationAddress; - } - CharSequence nextCharSequence = currentlyOutgonigTopLevelStreamElement.toXML(StreamOpen.CLIENT_NAMESPACE); - if (nextCharSequence instanceof XmlStringBuilder) { - XmlStringBuilder xmlStringBuilder = (XmlStringBuilder) nextCharSequence; - outgoingCharSequenceIterator = xmlStringBuilder.toList(outgoingStreamXmlEnvironment).iterator(); - } else { - outgoingCharSequenceIterator = Collections.singletonList(nextCharSequence).iterator(); - } - assert outgoingCharSequenceIterator != null; - } else { - // There is nothing more to write. - break; - } - } - - pendingOutputFilterData = newPendingOutputFilterData; - if (!pendingWriteInterestAfterRead && pendingOutputFilterData) { - newInterestedOps |= SelectionKey.OP_WRITE; - } - - readLoop: while (true) { - // Prevent one callback from dominating the reactor thread. Break out of the read-loop if we have - // read a certain amount. - if (callbackBytesRead > CALLBACK_MAX_BYTES_READ) { - callbackPreemtBecauseBytesRead++; - break; - } - - int bytesRead; - incomingBuffer.clear(); - try { - bytesRead = selectedSocketChannel.read(incomingBuffer); - } catch (IOException e) { - handleReadWriteIoException(e); - return; - } - - if (bytesRead < 0) { - LOGGER.finer("NIO read() returned " + bytesRead - + " for " + this + ". This probably means that the TCP connection was terminated."); - // According to the socket channel javadoc section about "asynchronous reads" a socket channel's - // read() may return -1 if the input side of a socket is shut down. - - // Note that we do not call notifyConnectionError() here because the connection may be - // cleanly shutdown which would also cause read() to return '-1. I assume that this socket - // will be selected again, on which read() would throw an IOException, which will be catched - // and invoke notifyConnectionError() (see a few lines above). - /* - IOException exception = new IOException("NIO read() returned " + bytesRead); - notifyConnectionError(exception); - */ - return; - } - - if (!pendingInputFilterData) { - if (bytesRead == 0) { - // Nothing more to read. - break; - } - } else { - pendingInputFilterData = false; - } - - // We have successfully read something. It is now possible that a filter is now also able to write - // additional data (for example SSLEngine). - if (pendingWriteInterestAfterRead) { - pendingWriteInterestAfterRead = false; - newInterestedOps |= SelectionKey.OP_WRITE; - } - - callbackBytesRead += bytesRead; - - ByteBuffer filteredIncomingBuffer = incomingBuffer; - for (ListIterator it = getXmppInputOutputFilterEndIterator(); it.hasPrevious();) { - filteredIncomingBuffer.flip(); - - ByteBuffer newFilteredIncomingBuffer; - try { - newFilteredIncomingBuffer = it.previous().input(filteredIncomingBuffer); - } catch (IOException e) { - notifyConnectionError(e); - return; - } - if (newFilteredIncomingBuffer == null) { - break readLoop; - } - filteredIncomingBuffer = newFilteredIncomingBuffer; - } - - final int bytesReadAfterFilter = filteredIncomingBuffer.flip().remaining(); - - totalBytesReadAfterFilter += bytesReadAfterFilter; - - try { - splitter.write(filteredIncomingBuffer); - } catch (IOException e) { - notifyConnectionError(e); - return; - } - } - } finally { - totalBytesWritten += callbackBytesWritten; - totalBytesRead += callbackBytesRead; - - channelSelectedCallbackLock.unlock(); - } - - // Indicate that there is no reactor thread racing towards handling this selection key. - final SelectionKeyAttachment selectionKeyAttachment = this.selectionKeyAttachment; - if (selectionKeyAttachment != null) { - selectionKeyAttachment.resetReactorThreadRacing(); - } - - // Check the queue again to prevent lost wakeups caused by elements inserted before we - // called resetReactorThreadRacing() a few lines above. - if (!outgoingElementsQueue.isEmpty()) { - setWriteInterestAfterChannelSelectedCallback.incrementAndGet(); - newInterestedOps |= SelectionKey.OP_WRITE; - } - - setInterestOps(selectionKey, newInterestedOps); - }; - - private void handleReadWriteIoException(IOException e) { - if (e instanceof ClosedChannelException && !isConnected()) { - // The connection is already closed. - return; - } - - notifyConnectionError(e); - } - - private void callChannelSelectedCallback(boolean setPendingInputFilterData, boolean setPendingOutputFilterData) { - final SocketChannel channel = socketChannel; - final SelectionKey key = selectionKey; - if (channel == null || key == null) { - LOGGER.info("Not calling channel selected callback because the connection was eventually disconnected"); - return; - } - - channelSelectedCallbackLock.lock(); - try { - // Note that it is important that we send the pending(Input|Output)FilterData flags while holding the lock. - if (setPendingInputFilterData) { - pendingInputFilterData = true; - } - if (setPendingOutputFilterData) { - pendingOutputFilterData = true; - } - - channelSelectedCallback.onChannelSelected(channel, key); - } finally { - channelSelectedCallbackLock.unlock(); - } - } - - private abstract static class TcpHostEvent extends DetailedTransitionIntoInformation { - protected final InetSocketAddress inetSocketAddress; - - protected TcpHostEvent(State state, InetSocketAddress inetSocketAddress) { - super(state); - this.inetSocketAddress = inetSocketAddress; - } - - public InetSocketAddress getInetSocketAddress() { - return inetSocketAddress; - } - - @Override - public String toString() { - return super.toString() + ": " + inetSocketAddress; - } - } - - public static final class ConnectingToHostEvent extends TcpHostEvent { - private ConnectingToHostEvent(State state, InetSocketAddress inetSocketAddress) { - super(state, inetSocketAddress); - } - } - - public static final class ConnectedToHostEvent extends TcpHostEvent { - private final boolean connectionEstablishedImmediately; - - private ConnectedToHostEvent(State state, InetSocketAddress inetSocketAddress, boolean immediately) { - super(state, inetSocketAddress); - this.connectionEstablishedImmediately = immediately; - } - - @Override - public String toString() { - return super.toString() + (connectionEstablishedImmediately ? "" : " not") + " connected immediately"; - } - } - - public static final class ConnectionToHostFailedEvent extends TcpHostEvent { - private final IOException ioException; - - private ConnectionToHostFailedEvent(State state, InetSocketAddress inetSocketAddress, IOException ioException) { - super(state, inetSocketAddress); - this.ioException = ioException; - } - - @Override - public String toString() { - return super.toString() + ioException; - } - } - - private final class ConnectionAttemptState { - private final ConnectingToHostState connectingToHostState; - InetSocketAddress inetSocketAddress; - // TODO: Check if we can re-use the socket channel in case some InetSocketAddress fail to connect to. - final SocketChannel socketChannel; - final Iterator remainingAddresses; - final List failedAddresses; - final SynchronizationPoint tcpConnectionEstablishedSyncPoint; - - private ConnectionAttemptState(List inetSocketAddresses, List failedAddresses, - ConnectingToHostState connectingToHostState) throws IOException { - socketChannel = SocketChannel.open(); - socketChannel.configureBlocking(false); - remainingAddresses = inetSocketAddresses.iterator(); - inetSocketAddress = remainingAddresses.next(); - this.failedAddresses = failedAddresses; - this.connectingToHostState = connectingToHostState; - - tcpConnectionEstablishedSyncPoint = new SynchronizationPoint<>(XmppNioTcpConnection.this, - "TCP connection establishment"); - } - - private void establishTcpConnection() { - ConnectingToHostEvent connectingToHostEvent = new ConnectingToHostEvent(connectingToHostState, inetSocketAddress); - invokeConnectionStateMachineListener(connectingToHostEvent); - - final boolean connected; - try { - connected = socketChannel.connect(inetSocketAddress); - } catch (IOException e) { - onIOExceptionWhenEstablishingTcpConnection(e); - return; - } - - if (connected) { - ConnectedToHostEvent connectedToHostEvent = new ConnectedToHostEvent(connectingToHostState, - inetSocketAddress, true); - invokeConnectionStateMachineListener(connectedToHostEvent); - - tcpConnectionEstablishedSyncPoint.reportSuccess(); - return; - } - - try { - registerWithSelector(socketChannel, SelectionKey.OP_CONNECT, - (selectedChannel, selectedSelectionKey) -> { - SocketChannel selectedSocketChannel = (SocketChannel) selectedChannel; - - boolean finishConnected; - try { - finishConnected = selectedSocketChannel.finishConnect(); - } catch (IOException e) { - Async.go(() -> onIOExceptionWhenEstablishingTcpConnection(e)); - return; - } - - if (!finishConnected) { - Async.go(() -> onIOExceptionWhenEstablishingTcpConnection(new IOException("finishConnect() failed"))); - return; - } - - ConnectedToHostEvent connectedToHostEvent = new ConnectedToHostEvent(connectingToHostState, inetSocketAddress, false); - invokeConnectionStateMachineListener(connectedToHostEvent); - - // Do not set 'state' here, since this is processed by a reactor thread, which doesn't hold - // the objects lock. - tcpConnectionEstablishedSyncPoint.reportSuccess(); - }); - } catch (ClosedChannelException e) { - onIOExceptionWhenEstablishingTcpConnection(e); - } - } - - private void onIOExceptionWhenEstablishingTcpConnection(IOException exception) { - if (!remainingAddresses.hasNext()) { - ConnectionException connectionException = ConnectionException.from(failedAddresses); - tcpConnectionEstablishedSyncPoint.reportFailure(connectionException); - return; - } - - tcpConnectionEstablishedSyncPoint.resetTimeout(); - - HostAddress failedHostAddress = new HostAddress(inetSocketAddress, exception); - failedAddresses.add(failedHostAddress); - - ConnectionToHostFailedEvent connectionToHostFailedEvent = new ConnectionToHostFailedEvent( - connectingToHostState, inetSocketAddress, exception); - invokeConnectionStateMachineListener(connectionToHostFailedEvent); - - inetSocketAddress = remainingAddresses.next(); - - establishTcpConnection(); - } - } - - @Override - protected void connectInternal() throws SmackException, IOException, XMPPException, InterruptedException { - // TODO: Check if those initialization methods can be invoked later. - outgoingElementsQueue.start(); - closingStreamReceived.init(); - - WalkStateGraphContext walkStateGraphContext = buildNewWalkTo(ConnectedButUnauthenticatedStateDescriptor.class) - .build(); - - walkStateGraph(walkStateGraphContext); - } - - private List failedAddresses; - private List inetSocketAddresses; - - private static final class LookupHostAddressesStateDescriptor extends StateDescriptor { - private LookupHostAddressesStateDescriptor() { - super(LookupHostAddressesState.class); - addPredeccessor(DisconnectedStateDescriptor.class); - addSuccessor(ConnectingToHostStateDescriptor.class); - addSuccessor(DirectTlsConnectionToHostStateDescriptor.class); - } - } - - private final class LookupHostAddressesState extends State { - private LookupHostAddressesState(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws ConnectionException { - failedAddresses = populateHostAddresses(); - if (hostAddresses.isEmpty()) { - throw ConnectionException.from(failedAddresses); - } - - inetSocketAddresses = new ArrayList<>(2 * hostAddresses.size()); - for (HostAddress hostAddress : XmppNioTcpConnection.this.hostAddresses) { - List inetAddresses = hostAddress.getInetAddresses(); - for (InetAddress inetAddress : inetAddresses) { - InetSocketAddress inetSocketAddress = new InetSocketAddress(inetAddress, hostAddress.getPort()); - inetSocketAddresses.add(inetSocketAddress); - } - } - - return new HostLookupResult(inetSocketAddresses); - } - - @Override - protected void resetState() { - failedAddresses = null; - inetSocketAddresses = null; - } - } - - public static final class HostLookupResult extends TransitionSuccessResult { - private final List remoteAddresses; - - private HostLookupResult(List remoteAddresses) { - super("Host lookup yielded the following addressess: " + remoteAddresses); - - List remoteAddressesLocal = new ArrayList<>(remoteAddresses.size()); - remoteAddressesLocal.addAll(remoteAddresses); - this.remoteAddresses = Collections.unmodifiableList(remoteAddressesLocal); - } - - public List getRemoteAddresses() { - return remoteAddresses; - } - } - - private static final class DirectTlsConnectionToHostStateDescriptor extends StateDescriptor { - private DirectTlsConnectionToHostStateDescriptor() { - super(DirectTlsConnectionToHostState.class, 368, StateDescriptor.Property.notImplemented); - addPredeccessor(LookupHostAddressesStateDescriptor.class); - addSuccessor(ConnectedButUnauthenticatedStateDescriptor.class); - declarePrecedenceOver(ConnectingToHostStateDescriptor.class); - } - } - - private final class DirectTlsConnectionToHostState extends State { - private DirectTlsConnectionToHostState(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { - if (!useDirectTls) { - return new TransitionImpossibleReason("Direct TLS not enabled"); - } - - // TODO: Check if lookup yielded any xmpps SRV RRs. - - throw new IllegalStateException("Direct TLS not implemented"); - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { - throw new IllegalStateException("Direct TLS not implemented"); - } - } - - private static final class ConnectingToHostStateDescriptor extends StateDescriptor { - private ConnectingToHostStateDescriptor() { - super(ConnectingToHostState.class); - addSuccessor(EstablishTlsStateDescriptor.class); - addSuccessor(ConnectedButUnauthenticatedStateDescriptor.class); - } - } - - private final class ConnectingToHostState extends State { - private ConnectingToHostState(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) - throws IOException, InterruptedException, NoResponseException, ConnectionException, - ConnectionUnexpectedTerminatedException, NotConnectedException { - // The fields inetSocketAddress and failedAddresses are handed over from LookupHostAddresses to - // ConnectingToHost. - ConnectionAttemptState connectionAttemptState = new ConnectionAttemptState(inetSocketAddresses, - failedAddresses, this); - connectionAttemptState.establishTcpConnection(); - - try { - connectionAttemptState.tcpConnectionEstablishedSyncPoint.checkIfSuccessOrWaitOrThrow(); - } catch (SmackWrappedException e) { - // Should never throw SmackWrappedException. - throw new AssertionError(e); - } - - socketChannel = connectionAttemptState.socketChannel; - remoteAddress = (InetSocketAddress) socketChannel.socket().getRemoteSocketAddress(); - - selectionKey = registerWithSelector(socketChannel, SelectionKey.OP_READ, channelSelectedCallback); - selectionKeyAttachment = (SelectionKeyAttachment) selectionKey.attachment(); - - newStreamOpenWaitForFeaturesSequence("stream features after initial connection"); - - return new TcpSocketConnectedResult(remoteAddress); - } - - @Override - protected void resetState() { - cleanUpSelectionKeyAndSocketChannel(); - } - } - - public static final class TcpSocketConnectedResult extends TransitionSuccessResult { - private final InetSocketAddress remoteAddress; - - private TcpSocketConnectedResult(InetSocketAddress remoteAddress) { - super("TCP connection established to " + remoteAddress); - this.remoteAddress = remoteAddress; - } - - public InetSocketAddress getRemoteAddress() { - return remoteAddress; - } - } - - private static final class EstablishTlsStateDescriptor extends StateDescriptor { - private EstablishTlsStateDescriptor() { - super(EstablishTlsState.class, "RFC 6120 ยง 5"); - addSuccessor(ConnectedButUnauthenticatedStateDescriptor.class); - declarePrecedenceOver(ConnectedButUnauthenticatedStateDescriptor.class); - } - } - - private final class EstablishTlsState extends State { - private EstablishTlsState(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) - throws SecurityRequiredByClientException, SecurityRequiredByServerException { - StartTls startTlsFeature = getFeature(StartTls.ELEMENT, StartTls.NAMESPACE); - SecurityMode securityMode = config.getSecurityMode(); - - switch (securityMode) { - case required: - case ifpossible: - if (startTlsFeature == null) { - if (securityMode == SecurityMode.ifpossible) { - return new TransitionImpossibleReason("Server does not announce support for TLS and we do not required it"); - } - throw new SecurityRequiredByClientException(); - } - // Allows transition by returning null. - return null; - case disabled: - if (startTlsFeature != null && startTlsFeature.required()) { - throw new SecurityRequiredByServerException(); - } - return new TransitionImpossibleReason("TLS disabled in client settings and server does not require it"); - default: - throw new AssertionError("Unknown security mode: " + securityMode); - } - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) - throws SmackWrappedException, FailedNonzaException, IOException, InterruptedException, - ConnectionUnexpectedTerminatedException, NoResponseException, NotConnectedException { - sendAndWaitForResponse(StartTls.INSTANCE, TlsProceed.class, TlsFailure.class); - - SmackTlsContext smackTlsContext; - try { - smackTlsContext = getSmackTlsContext(); - } catch (KeyManagementException | UnrecoverableKeyException | NoSuchAlgorithmException - | CertificateException | KeyStoreException | NoSuchProviderException e) { - throw new SmackWrappedException(e); - } - - tlsState = new TlsState(smackTlsContext); - addXmppInputOutputFilter(tlsState); - - channelSelectedCallbackLock.lock(); - try { - pendingOutputFilterData = true; - // The beginHandshake() is possibly not really required here, but it does not hurt either. - tlsState.engine.beginHandshake(); - tlsState.handshakeStatus = TlsHandshakeStatus.initiated; - } finally { - channelSelectedCallbackLock.unlock(); - } - setInterestOps(selectionKey, SelectionKey.OP_WRITE | SelectionKey.OP_READ); - - try { - tlsState.waitForHandshakeFinished(); - } catch (CertificateException e) { - throw new SmackWrappedException(e); - } - - newStreamOpenWaitForFeaturesSequence("stream features after TLS established"); - - return new TlsEstablishedResult(tlsState.engine); - } - - @Override - protected void resetState() { - tlsState = null; - } - } - - public static final class TlsEstablishedResult extends TransitionSuccessResult { - - private TlsEstablishedResult(SSLEngine sslEngine) { - super("TLS established: " + sslEngine.getSession()); - } - } - - private enum TlsHandshakeStatus { - initial, - initiated, - successful, - failed, - } - - private static final Level SSL_ENGINE_DEBUG_LOG_LEVEL = Level.FINEST; - - private static void debugLogSslEngineResult(String operation, SSLEngineResult result) { - if (!LOGGER.isLoggable(SSL_ENGINE_DEBUG_LOG_LEVEL)) { - return; - } - - LOGGER.log(SSL_ENGINE_DEBUG_LOG_LEVEL, "SSLEngineResult of " + operation + "(): " + result); - } - - private final class TlsState implements XmppInputOutputFilter { - - private static final int MAX_PENDING_OUTPUT_BYTES = 8096; - - private final SmackTlsContext smackTlsContext; - private final SSLEngine engine; - - private TlsHandshakeStatus handshakeStatus = TlsHandshakeStatus.initial; - private SSLException handshakeException; - - private ByteBuffer myNetData; - private ByteBuffer peerAppData; - - private final List pendingOutputData = new ArrayList<>(); - private int pendingOutputBytes; - private ByteBuffer pendingInputData; - - private final AtomicInteger pendingDelegatedTasks = new AtomicInteger(); - - private long wrapInBytes; - private long wrapOutBytes; - - private long unwrapInBytes; - private long unwrapOutBytes; - - private TlsState(SmackTlsContext smackTlsContext) throws IOException { - this.smackTlsContext = smackTlsContext; - - // Call createSSLEngine()'s variant with two parameters as this allows for TLS session resumption. - - // Note that it is not really clear what the value of peer host should be. It could be A) the XMPP service's - // domainpart or B) the DNS name of the host we are connecting to (usually the DNS SRV RR target name). While - // the javadoc of createSSLEngine(String, int) indicates with "Some cipher suites (such as Kerberos) require - // remote hostname information, in which case peerHost needs to be specified." that A should be used. TLS - // session resumption may would need or at least benefit from B. Variant A would also be required if the - // String is used for certificate verification. And it appears at least likely that TLS session resumption - // would not be hurt by using variant A. Therefore we currently use variant A. - // TODO: Should we use the ACE representation of the XMPP service domain? Compare with f60e4055ec529f0b8160acedf13275592ab10a4b - // If yes, then we should probably introduce getXmppServiceDomainAceEncodedIfPossible(). - engine = smackTlsContext.sslContext.createSSLEngine(config.getXMPPServiceDomain().toString(), remoteAddress.getPort()); - engine.setUseClientMode(true); - - SSLSession session = engine.getSession(); - int applicationBufferSize = session.getApplicationBufferSize(); - int packetBufferSize = session.getPacketBufferSize(); - - myNetData = ByteBuffer.allocateDirect(packetBufferSize); - peerAppData = ByteBuffer.allocate(applicationBufferSize); - } - - @Override - public OutputResult output(ByteBuffer outputData, boolean isFinalDataOfElement, boolean destinationAddressChanged, - boolean moreDataAvailable) throws SSLException { - if (outputData != null) { - pendingOutputData.add(outputData); - pendingOutputBytes += outputData.remaining(); - if (moreDataAvailable && pendingOutputBytes < MAX_PENDING_OUTPUT_BYTES) { - return OutputResult.NO_OUTPUT; - } - } - - ByteBuffer[] outputDataArray = pendingOutputData.toArray(new ByteBuffer[pendingOutputData.size()]); - - myNetData.clear(); - - while (true) { - SSLEngineResult result; - try { - result = engine.wrap(outputDataArray, myNetData); - } catch (SSLException e) { - handleSslException(e); - throw e; - } - - debugLogSslEngineResult("wrap", result); - - SSLEngineResult.Status engineResultStatus = result.getStatus(); - - pendingOutputBytes -= result.bytesConsumed(); - - if (engineResultStatus == SSLEngineResult.Status.OK) { - wrapInBytes += result.bytesConsumed(); - wrapOutBytes += result.bytesProduced(); - - SSLEngineResult.HandshakeStatus handshakeStatus = handleHandshakeStatus(result); - switch (handshakeStatus) { - case NEED_UNWRAP: - // NEED_UNWRAP means that we need to receive something in order to continue the handshake. The - // standard channelSelectedCallback logic will take care of this, as there is eventually always - // a interest to read from the socket. - break; - case NEED_WRAP: - // Same as need task: Cycle the reactor. - case NEED_TASK: - // Note that we also set pendingOutputFilterData in the OutputResult in the NEED_TASK case, as - // we also want to retry the wrap() operation above in this case. - return new OutputResult(true, myNetData); - default: - break; - } - } - - switch (engineResultStatus) { - case OK: - // No need to outputData.compact() here, since we do not reuse the buffer. - // Clean up the pending output data. - pruneBufferList(pendingOutputData); - return new OutputResult(!pendingOutputData.isEmpty(), myNetData); - case CLOSED: - pendingOutputData.clear(); - return OutputResult.NO_OUTPUT; - case BUFFER_OVERFLOW: - LOGGER.warning("SSLEngine status BUFFER_OVERFLOW, this is hopefully uncommon"); - int outputDataRemaining = outputData != null ? outputData.remaining() : 0; - int newCapacity = (int) (1.3 * outputDataRemaining); - // If newCapacity would not increase myNetData, then double it. - if (newCapacity <= myNetData.capacity()) { - newCapacity = 2 * myNetData.capacity(); - } - ByteBuffer newMyNetData = ByteBuffer.allocateDirect(newCapacity); - myNetData.flip(); - newMyNetData.put(myNetData); - myNetData = newMyNetData; - continue; - case BUFFER_UNDERFLOW: - throw new IllegalStateException( - "Buffer underflow as result of SSLEngine.wrap() should never happen"); - } - } - } - - @Override - public ByteBuffer input(ByteBuffer inputData) throws SSLException { - ByteBuffer accumulatedData; - if (pendingInputData == null) { - accumulatedData = inputData; - } else { - int accumulatedDataBytes = pendingInputData.remaining() + inputData.remaining(); - accumulatedData = ByteBuffer.allocate(accumulatedDataBytes); - accumulatedData.put(pendingInputData) - .put(inputData) - .flip(); - pendingInputData = null; - } - - peerAppData.clear(); - - while (true) { - SSLEngineResult result; - try { - result = engine.unwrap(accumulatedData, peerAppData); - } catch (SSLException e) { - handleSslException(e); - throw e; - } - - debugLogSslEngineResult("unwrap", result); - - SSLEngineResult.Status engineResultStatus = result.getStatus(); - - if (engineResultStatus == SSLEngineResult.Status.OK) { - unwrapInBytes += result.bytesConsumed(); - unwrapOutBytes += result.bytesProduced(); - - SSLEngineResult.HandshakeStatus handshakeStatus = handleHandshakeStatus(result); - switch (handshakeStatus) { - case NEED_TASK: - // A delegated task is asynchronously running. Signal that there is pending input data and - // cycle again through the smack reactor. - addAsPendingInputData(accumulatedData); - break; - case NEED_UNWRAP: - continue; - case NEED_WRAP: - // NEED_WRAP means that the SSLEngine needs to send data, probably without consuming data. - // We exploit here the fact that the channelSelectedCallback is single threaded and that the - // input processing is after the output processing. - asyncGo(() -> callChannelSelectedCallback(false, true)); - break; - default: - break; - } - } - - switch (engineResultStatus) { - case OK: - // SSLEngine's unwrap() may not consume all bytes from the source buffer. If this is the case, then - // simply perform another unwrap until accumlatedData has no remaining bytes. - if (accumulatedData.hasRemaining()) { - continue; - } - return peerAppData; - case CLOSED: - return null; - case BUFFER_UNDERFLOW: - // There were not enough source bytes available to make a complete packet. Let it in - // pendingInputData. Note that we do not resize SSLEngine's source buffer - inputData in our case - - // as it is not possible. - addAsPendingInputData(accumulatedData); - return null; - case BUFFER_OVERFLOW: - int applicationBufferSize = engine.getSession().getApplicationBufferSize(); - assert peerAppData.remaining() < applicationBufferSize; - peerAppData = ByteBuffer.allocate(applicationBufferSize); - continue; - } - } - } - - private void addAsPendingInputData(ByteBuffer byteBuffer) { - pendingInputData = ByteBuffer.allocate(byteBuffer.remaining()); - pendingInputData.put(byteBuffer).flip(); - } - - private SSLEngineResult.HandshakeStatus handleHandshakeStatus(SSLEngineResult sslEngineResult) { - SSLEngineResult.HandshakeStatus handshakeStatus = sslEngineResult.getHandshakeStatus(); - switch (handshakeStatus) { - case NEED_TASK: - while (true) { - final Runnable delegatedTask = engine.getDelegatedTask(); - if (delegatedTask == null) { - break; - } - sslEngineDelegatedTasks++; - int currentPendingDelegatedTasks = pendingDelegatedTasks.incrementAndGet(); - if (currentPendingDelegatedTasks > maxPendingSslEngineDelegatedTasks) { - maxPendingSslEngineDelegatedTasks = currentPendingDelegatedTasks; - } - - Runnable wrappedDelegatedTask = () -> { - delegatedTask.run(); - int wrappedCurrentPendingDelegatedTasks = pendingDelegatedTasks.decrementAndGet(); - if (wrappedCurrentPendingDelegatedTasks == 0) { - callChannelSelectedCallback(true, true); - } - }; - asyncGo(wrappedDelegatedTask); - } - break; - case FINISHED: - onHandshakeFinished(); - break; - default: - break; - } - - SSLEngineResult.HandshakeStatus afterHandshakeStatus = engine.getHandshakeStatus(); - return afterHandshakeStatus; - } - - private void handleSslException(SSLException e) { - handshakeException = e; - handshakeStatus = TlsHandshakeStatus.failed; - synchronized (this) { - notifyAll(); - } - } - - private void onHandshakeFinished() { - handshakeStatus = TlsHandshakeStatus.successful; - synchronized (this) { - notifyAll(); - } - } - - private boolean isHandshakeFinished() { - return handshakeStatus == TlsHandshakeStatus.successful || handshakeStatus == TlsHandshakeStatus.failed; - } - - private void waitForHandshakeFinished() throws InterruptedException, CertificateException, SSLException, ConnectionUnexpectedTerminatedException, NoResponseException { - final long deadline = System.currentTimeMillis() + getReplyTimeout(); - - synchronized (this) { - while (!isHandshakeFinished() && currentConnectionException == null) { - final long now = System.currentTimeMillis(); - if (now >= deadline) break; - wait(deadline - now); - } - } - - if (currentConnectionException != null) { - throw new SmackException.ConnectionUnexpectedTerminatedException(currentConnectionException); - } - - if (!isHandshakeFinished()) { - throw NoResponseException.newWith(XmppNioTcpConnection.this, "TLS Handshake finsih"); - } - - if (handshakeStatus == TlsHandshakeStatus.failed) { - throw handshakeException; - } - - assert handshakeStatus == TlsHandshakeStatus.successful; - - if (smackTlsContext.daneVerifier != null) { - smackTlsContext.daneVerifier.finish(engine.getSession()); - } - } - - @Override - public Object getStats() { - return new TlsStateStats(this); - } - - @Override - public void closeInputOutput() { - engine.closeOutbound(); - try { - engine.closeInbound(); - } catch (SSLException e) { - LOGGER.log(Level.FINEST, - "SSLException when closing inbound TLS session. This can likely be ignored if a possible truncation attack is suggested." - + " You may want to ask your XMPP server vendor to implement a clean TLS session shutdown sending close_notify after ", - e); - } - } - - @Override - public void waitUntilInputOutputClosed() throws IOException, CertificateException, InterruptedException, - ConnectionUnexpectedTerminatedException, NoResponseException { - waitForHandshakeFinished(); - } - } - - public static final class TlsStateStats { - public final long wrapInBytes; - public final long wrapOutBytes; - public final double wrapRatio; - - public final long unwrapInBytes; - public final long unwrapOutBytes; - public final double unwrapRatio; - - private TlsStateStats(TlsState tlsState) { - wrapOutBytes = tlsState.wrapOutBytes; - wrapInBytes = tlsState.wrapInBytes; - wrapRatio = (double) wrapOutBytes / wrapInBytes; - - unwrapOutBytes = tlsState.unwrapOutBytes; - unwrapInBytes = tlsState.unwrapInBytes; - unwrapRatio = (double) unwrapInBytes / unwrapOutBytes; - } - - private transient String toStringCache; - - @Override - public String toString() { - if (toStringCache != null) { - return toStringCache; - } - - toStringCache = - "wrap-in-bytes: " + wrapInBytes + '\n' - + "wrap-out-bytes: " + wrapOutBytes + '\n' - + "wrap-ratio: " + wrapRatio + '\n' - + "unwrap-in-bytes: " + unwrapInBytes + '\n' - + "unwrap-out-bytes: " + unwrapOutBytes + '\n' - + "unwrap-ratio: " + unwrapRatio - ; - - return toStringCache; - } - } - - protected static final class EnableStreamManagementStateDescriptor extends StateDescriptor { - private EnableStreamManagementStateDescriptor() { - super(EnableStreamManagementState.class, 198, StateDescriptor.Property.notImplemented); - addPredeccessor(ResourceBindingStateDescriptor.class); - addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); - declarePrecedenceOver(AuthenticatedAndResourceBoundStateDescriptor.class); - } - } - - private final class EnableStreamManagementState extends State { - private EnableStreamManagementState(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { - if (!useSm) { - return new TransitionImpossibleReason("Stream management not enabled"); - } - - throw new IllegalStateException("SM not implemented"); - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { - throw new IllegalStateException("SM not implemented"); - } - } - - private static final class ResumeStreamStateDescriptor extends StateDescriptor { - private ResumeStreamStateDescriptor() { - super(ResumeStreamState.class, 198, StateDescriptor.Property.notImplemented); - addPredeccessor(AuthenticatedButUnboundStateDescriptor.class); - addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); - declarePrecedenceOver(ResourceBindingStateDescriptor.class); - declareInferiortyTo(CompressionStateDescriptor.class); - } - } - - private final class ResumeStreamState extends State { - private ResumeStreamState(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { - if (!useSmResumption) { - return new TransitionImpossibleReason("Stream resumption not enabled"); - } - - throw new IllegalStateException("Stream resumptionimplemented"); - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, - SASLErrorException, IOException, SmackException, InterruptedException, FailedNonzaException { - throw new IllegalStateException("Stream resumptionimplemented"); - } - } - - private static final class InstantStreamResumptionStateDescriptor extends StateDescriptor { - private InstantStreamResumptionStateDescriptor() { - super(InstantStreamResumptionState.class, 397, StateDescriptor.Property.notImplemented); - addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); - addPredeccessor(ConnectedButUnauthenticatedStateDescriptor.class); - declarePrecedenceOver(SaslAuthenticationStateDescriptor.class); - } - } - - private final class InstantStreamResumptionState extends State { - private InstantStreamResumptionState(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { - if (!useIsr) { - return new TransitionImpossibleReason("Instant stream resumption not enabled nor implemented"); - } - - throw new IllegalStateException("Instant stream resumption not implemented"); - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { - throw new IllegalStateException("Instant stream resumption not implemented"); - } - } - - private static final class Bind2StateDescriptor extends StateDescriptor { - private Bind2StateDescriptor() { - super(Bind2State.class, 386, StateDescriptor.Property.notImplemented); - addPredeccessor(ConnectedButUnauthenticatedStateDescriptor.class); - addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); - declarePrecedenceOver(SaslAuthenticationStateDescriptor.class); - } - } - - private final class Bind2State extends State { - private Bind2State(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { - if (!useBind2) { - return new TransitionImpossibleReason("Bind2 not enabled nor implemented"); - } - - throw new IllegalStateException("Bind2 not implemented"); - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { - throw new IllegalStateException("Bind2 not implemented"); - } - } - - static final class InstantShutdownStateDescriptor extends StateDescriptor { - private InstantShutdownStateDescriptor() { - super(InstantShutdownState.class); - addSuccessor(CloseConnectionStateDescriptor.class); - addPredeccessor(AuthenticatedAndResourceBoundStateDescriptor.class); - addPredeccessor(ConnectedButUnauthenticatedStateDescriptor.class); - } - } - - private final class InstantShutdownState extends State { - private InstantShutdownState(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { - ensureNotOnOurWayToAuthenticatedAndResourceBound(walkStateGraphContext); - return null; - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { - outgoingElementsQueue.shutdown(); - afterOutgoingElementsQueueModified(); - - return TransitionSuccessResult.EMPTY_INSTANCE; - } - } - - private static final class ShutdownStateDescriptor extends StateDescriptor { - private ShutdownStateDescriptor() { - super(ShutdownState.class); - addSuccessor(CloseConnectionStateDescriptor.class); - addPredeccessor(AuthenticatedAndResourceBoundStateDescriptor.class); - addPredeccessor(ConnectedButUnauthenticatedStateDescriptor.class); - } - } - - private final class ShutdownState extends State { - private ShutdownState(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionImpossibleReason isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { - ensureNotOnOurWayToAuthenticatedAndResourceBound(walkStateGraphContext); - return null; - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { - closingStreamReceived.init(); - - boolean streamCloseIssued = outgoingElementsQueue.offerAndShutdown(StreamClose.INSTANCE); - - afterOutgoingElementsQueueModified(); - - if (streamCloseIssued) { - boolean successfullyReceivedStreamClose = waitForClosingStreamTagFromServer(); - if (successfullyReceivedStreamClose) { - for (Iterator it = getXmppInputOutputFilterBeginIterator(); it.hasNext();) { - XmppInputOutputFilter filter = it.next(); - filter.closeInputOutput(); - } - - // Flush the new state. - pendingInputFilterData = pendingOutputFilterData = true; - afterOutgoingElementsQueueModified(); - - for (Iterator it = getXmppInputOutputFilterBeginIterator(); it.hasNext();) { - XmppInputOutputFilter filter = it.next(); - try { - filter.waitUntilInputOutputClosed(); - } catch (IOException | CertificateException | InterruptedException | SmackException e) { - LOGGER.log(Level.WARNING, "waitUntilInputOutputClosed() threw", e); - } - } - } - } - - return TransitionSuccessResult.EMPTY_INSTANCE; - } - } - - private static final class CloseConnectionStateDescriptor extends StateDescriptor { - private CloseConnectionStateDescriptor() { - super(CloseConnectionState.class); - addSuccessor(DisconnectedStateDescriptor.class); - } - } - - private final class CloseConnectionState extends State { - private CloseConnectionState(StateDescriptor stateDescriptor) { - super(stateDescriptor); - } - - @Override - protected TransitionIntoResult transitionInto(WalkStateGraphContext walkStateGraphContext) { - cleanUpSelectionKeyAndSocketChannel(); - - return TransitionSuccessResult.EMPTY_INSTANCE; - } - } - - @Override - public boolean isSecureConnection() { - final TlsState tlsState = this.tlsState; - return tlsState != null && tlsState.handshakeStatus == TlsHandshakeStatus.successful; - } - - private void sendTopLevelStreamElement(TopLevelStreamElement topLevelStreamElement) - throws InterruptedException { - outgoingElementsQueue.put(topLevelStreamElement); - afterOutgoingElementsQueueModified(); - } - - private void afterOutgoingElementsQueueModified() { - final SelectionKeyAttachment selectionKeyAttachment = this.selectionKeyAttachment; - if (selectionKeyAttachment != null && selectionKeyAttachment.isReactorThreadRacing()) { - // A reactor thread is already racing to the channel selected callback and will take care of this. - reactorThreadAlreadyRacing.incrementAndGet(); - return; - } - - afterOutgoingElementsQueueModifiedSetInterestOps.incrementAndGet(); - - // Add OP_WRITE to the interested Ops, since we have now new things to write. Note that this may cause - // multiple reactor threads to race to the channel selected callback in case we perform this right after - // a select() returned with this selection key in the selected-key set. Hence we use tryLock() in the - // channel selected callback to keep the invariant that only exactly one thread is performing the - // callback. - // Note that we need to perform setInterestedOps() *without* holding the channelSelectedCallbackLock, as - // otherwise the reactor thread racing to the channel selected callback may found the lock still locked, which - // would result in the outgoingElementsQueue not being handled. - setInterestOps(selectionKey, SelectionKey.OP_WRITE | SelectionKey.OP_READ); - } - - @Override - protected void throwNotConnectedExceptionIfAppropriate() throws NotConnectedException { - if (!connected && !isSmResumptionPossible()) { - throw new NotConnectedException(this, "XMPP connection not connected"); - } - } - - @Override - protected void sendStanzaInternal(Stanza stanza) throws NotConnectedException, InterruptedException { - sendTopLevelStreamElement(stanza); - // TODO: Here would be stream management code once this connection type supports it. - } - - @Override - public void sendNonza(Nonza nonza) throws NotConnectedException, InterruptedException { - sendTopLevelStreamElement(nonza); - } - - @Override - protected void shutdown() { - shutdown(false); - } - - @Override - public synchronized void instantShutdown() { - shutdown(true); - } - - private void shutdown(boolean instant) { - Class mandatoryIntermediateState; - if (instant) { - mandatoryIntermediateState = InstantShutdownStateDescriptor.class; - } else { - mandatoryIntermediateState = ShutdownStateDescriptor.class; - } - - WalkStateGraphContext context = buildNewWalkTo(DisconnectedStateDescriptor.class) - .withMandatoryIntermediateState(mandatoryIntermediateState) - .build(); - - try { - walkStateGraph(context); - } catch (XMPPErrorException | SASLErrorException | IOException | SmackException | InterruptedException | FailedNonzaException e) { - throw new IllegalStateException("A walk to disconnected state should never throw", e); - } - } - - private void cleanUpSelectionKeyAndSocketChannel() { - final SelectionKey selectionKey = this.selectionKey; - if (selectionKey != null) { - selectionKey.cancel(); - } - final SocketChannel socketChannel = this.socketChannel; - if (socketChannel != null) { - try { - socketChannel.close(); - } catch (IOException e) { - - } - } - - this.selectionKey = null; - this.socketChannel = null; - - selectionKeyAttachment = null; - remoteAddress = null; - } - - public boolean isSmResumptionPossible() { - return false; - } - - public Stats getStats() { - return new Stats(this); - } - - public static final class Stats { - public final long totalBytesWritten; - public final long totalBytesWrittenBeforeFilter; - public final double writeRatio; - - public final long totalBytesRead; - public final long totalBytesReadAfterFilter; - public final double readRatio; - - public final long handledChannelSelectedCallbacks; - public final long setWriteInterestAfterChannelSelectedCallback; - public final long reactorThreadAlreadyRacing; - public final long afterOutgoingElementsQueueModifiedSetInterestOps; - public final long rejectedChannelSelectedCallbacks; - public final long totalCallbackRequests; - public final long callbackPreemtBecauseBytesWritten; - public final long callbackPreemtBecauseBytesRead; - public final int sslEngineDelegatedTasks; - public final int maxPendingSslEngineDelegatedTasks; - public final List filterStats; - - private Stats(XmppNioTcpConnection connection) { - totalBytesWritten = connection.totalBytesWritten; - totalBytesWrittenBeforeFilter = connection.totalBytesWrittenBeforeFilter; - writeRatio = (double) totalBytesWritten / totalBytesWrittenBeforeFilter; - - totalBytesReadAfterFilter = connection.totalBytesReadAfterFilter; - totalBytesRead = connection.totalBytesRead; - readRatio = (double) totalBytesRead / totalBytesReadAfterFilter; - - handledChannelSelectedCallbacks = connection.handledChannelSelectedCallbacks; - setWriteInterestAfterChannelSelectedCallback = connection.setWriteInterestAfterChannelSelectedCallback.get(); - reactorThreadAlreadyRacing = connection.reactorThreadAlreadyRacing.get(); - afterOutgoingElementsQueueModifiedSetInterestOps = connection.afterOutgoingElementsQueueModifiedSetInterestOps - .get(); - rejectedChannelSelectedCallbacks = connection.rejectedChannelSelectedCallbacks.get(); - - totalCallbackRequests = handledChannelSelectedCallbacks + rejectedChannelSelectedCallbacks; - - callbackPreemtBecauseBytesRead = connection.callbackPreemtBecauseBytesRead; - callbackPreemtBecauseBytesWritten = connection.callbackPreemtBecauseBytesWritten; - - sslEngineDelegatedTasks = connection.sslEngineDelegatedTasks; - maxPendingSslEngineDelegatedTasks = connection.maxPendingSslEngineDelegatedTasks; - - filterStats = connection.getFilterStats(); - } - - private transient String toStringCache; - - @Override - public String toString() { - if (toStringCache != null) { - return toStringCache; - } - - StringBuilder sb = new StringBuilder( - "Total bytes\n" - + "recv: " + totalBytesRead + '\n' - + "send: " + totalBytesWritten + '\n' - + "recv-aft-filter: " + totalBytesReadAfterFilter + '\n' - + "send-bef-filter: " + totalBytesWrittenBeforeFilter + '\n' - + "read-ratio: " + readRatio + '\n' - + "write-ratio: " + writeRatio + '\n' - + "Events\n" - + "total-callback-requests: " + totalCallbackRequests + '\n' - + "handled-channel-selected-callbacks: " + handledChannelSelectedCallbacks + '\n' - + "rejected-channel-selected-callbacks: " + rejectedChannelSelectedCallbacks + '\n' - + "set-write-interest-after-callback: " + setWriteInterestAfterChannelSelectedCallback + '\n' - + "reactor-thread-already-racing: " + reactorThreadAlreadyRacing + '\n' - + "after-queue-modified-set-interest-ops: " + afterOutgoingElementsQueueModifiedSetInterestOps + '\n' - + "callback-preemt-because-bytes-read: " + callbackPreemtBecauseBytesRead + '\n' - + "callback-preemt-because-bytes-written: " + callbackPreemtBecauseBytesWritten + '\n' - + "ssl-engine-delegated-tasks: " + sslEngineDelegatedTasks + '\n' - + "max-pending-ssl-engine-delegated-tasks: " + maxPendingSslEngineDelegatedTasks + '\n' - ); - - if (!filterStats.isEmpty()) { - sb.append("Filter Stats\n"); - for (Object filterStat : filterStats) { - sb.append(filterStat); - } - } - - toStringCache = sb.toString(); - - return toStringCache; - } - } - - private static List pruneBufferList(Collection buffers) { - return CollectionUtil.removeUntil(buffers, b -> b.hasRemaining()); - } - - @Override - protected SSLSession getSSLSession() { - if (tlsState == null) { - return null; - } - return tlsState.engine.getSession(); - } - - public static Set> getBackwardEdgesStateDescriptors() { - return Collections.unmodifiableSet(BACKWARD_EDGES_STATE_DESCRIPTORS); - } - -} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java new file mode 100644 index 000000000..ead3a6438 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModule.java @@ -0,0 +1,1431 @@ +/** + * + * Copyright 2019-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.tcp; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; + +import org.jivesoftware.smack.AbstractXMPPConnection.SmackTlsContext; +import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.SmackException.ConnectionException; +import org.jivesoftware.smack.SmackException.ConnectionUnexpectedTerminatedException; +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.SmackException.SecurityRequiredByClientException; +import org.jivesoftware.smack.SmackException.SecurityRequiredByServerException; +import org.jivesoftware.smack.SmackException.SmackWrappedException; +import org.jivesoftware.smack.SmackFuture; +import org.jivesoftware.smack.SmackFuture.InternalSmackFuture; +import org.jivesoftware.smack.SmackReactor.ChannelSelectedCallback; +import org.jivesoftware.smack.SmackReactor.SelectionKeyAttachment; +import org.jivesoftware.smack.XMPPException.FailedNonzaException; +import org.jivesoftware.smack.XmppInputOutputFilter; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.ConnectedButUnauthenticatedStateDescriptor; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.LookupRemoteConnectionEndpointsStateDescriptor; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModule; +import org.jivesoftware.smack.c2s.XmppClientToServerTransport; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.c2s.internal.WalkStateGraphContext; +import org.jivesoftware.smack.debugger.SmackDebugger; +import org.jivesoftware.smack.fsm.State; +import org.jivesoftware.smack.fsm.StateDescriptor; +import org.jivesoftware.smack.fsm.StateTransitionResult; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.packet.StartTls; +import org.jivesoftware.smack.packet.StreamOpen; +import org.jivesoftware.smack.packet.TlsFailure; +import org.jivesoftware.smack.packet.TlsProceed; +import org.jivesoftware.smack.packet.TopLevelStreamElement; +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.tcp.XmppTcpTransportModule.XmppTcpNioTransport.DiscoveredTcpEndpoints; +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.Jid; +import org.jxmpp.jid.util.JidUtil; +import org.jxmpp.xml.splitter.Utf8ByteXmppXmlSplitter; +import org.jxmpp.xml.splitter.XmlPrettyPrinter; +import org.jxmpp.xml.splitter.XmlPrinter; +import org.jxmpp.xml.splitter.XmppElementCallback; +import org.jxmpp.xml.splitter.XmppXmlSplitter; + +public class XmppTcpTransportModule extends ModularXmppClientToServerConnectionModule { + + private static final Logger LOGGER = Logger.getLogger(XmppTcpTransportModule.class.getName()); + + private static final int CALLBACK_MAX_BYTES_READ = 10 * 1024 * 1024; + private static final int CALLBACK_MAX_BYTES_WRITEN = CALLBACK_MAX_BYTES_READ; + + private static final int MAX_ELEMENT_SIZE = 64 * 1024; + + private final XmppTcpNioTransport tcpNioTransport; + + private SelectionKey selectionKey; + private SelectionKeyAttachment selectionKeyAttachment; + private SocketChannel socketChannel; + private InetSocketAddress remoteAddress; + + private TlsState tlsState; + + private Iterator outgoingCharSequenceIterator; + + private final List currentlyOutgoingElements = new ArrayList<>(); + private final Map> bufferToElementMap = new IdentityHashMap<>(); + + private ByteBuffer outgoingBuffer; + private ByteBuffer filteredOutgoingBuffer; + private final List networkOutgoingBuffers = new ArrayList<>(); + private long networkOutgoingBuffersBytes; + + // TODO: Make the size of the incomingBuffer configurable. + private final ByteBuffer incomingBuffer = ByteBuffer.allocateDirect(2 * 4096); + + private final ReentrantLock channelSelectedCallbackLock = new ReentrantLock(); + + private long totalBytesRead; + private long totalBytesWritten; + private long totalBytesReadAfterFilter; + private long totalBytesWrittenBeforeFilter; + private long handledChannelSelectedCallbacks; + private long callbackPreemtBecauseBytesWritten; + private long callbackPreemtBecauseBytesRead; + private int sslEngineDelegatedTasks; + private int maxPendingSslEngineDelegatedTasks; + + // TODO: Use LongAdder once Smack's minimum Android API level is 24 or higher. + private final AtomicLong setWriteInterestAfterChannelSelectedCallback = new AtomicLong(); + private final AtomicLong reactorThreadAlreadyRacing = new AtomicLong(); + private final AtomicLong afterOutgoingElementsQueueModifiedSetInterestOps = new AtomicLong(); + private final AtomicLong rejectedChannelSelectedCallbacks = new AtomicLong(); + + private Jid lastDestinationAddress; + + private boolean pendingInputFilterData; + private boolean pendingOutputFilterData; + + private boolean pendingWriteInterestAfterRead; + + /** + * Note that this field is effective final, but due to https://stackoverflow.com/q/30360824/194894 we have to declare it non-final. + */ + private Utf8ByteXmppXmlSplitter splitter; + + /** + * Note that this field is effective final, but due to https://stackoverflow.com/q/30360824/194894 we have to declare it non-final. + */ + private XmppXmlSplitter outputDebugSplitter; + + private static final Level STREAM_OPEN_CLOSE_DEBUG_LOG_LEVEL = Level.FINER; + + XmppTcpTransportModule(XmppTcpTransportModuleDescriptor moduleDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { + super(moduleDescriptor, connectionInternal); + + tcpNioTransport = new XmppTcpNioTransport(connectionInternal); + + XmlPrinter incomingDebugPrettyPrinter = null; + final SmackDebugger debugger = connectionInternal.smackDebugger; + if (debugger != null) { + // Incoming stream debugging. + incomingDebugPrettyPrinter = XmlPrettyPrinter.builder() + .setPrettyWriter(sb -> debugger.incomingStreamSink(sb)) + .build(); + + // Outgoing stream debugging. + XmlPrinter outgoingDebugPrettyPrinter = XmlPrettyPrinter.builder() + .setPrettyWriter(sb -> debugger.outgoingStreamSink(sb)) + .build(); + outputDebugSplitter = new XmppXmlSplitter(outgoingDebugPrettyPrinter); + } + + XmppXmlSplitter xmppXmlSplitter = new XmppXmlSplitter(MAX_ELEMENT_SIZE, xmppElementCallback, + incomingDebugPrettyPrinter); + splitter = new Utf8ByteXmppXmlSplitter(xmppXmlSplitter); + } + + private final XmppElementCallback xmppElementCallback = new XmppElementCallback() { + private String streamOpen; + private String streamClose; + + @Override + public void onCompleteElement(String completeElement) { + assert streamOpen != null; + assert streamClose != null; + + connectionInternal.withSmackDebugger(debugger -> debugger.onIncomingElementCompleted()); + + String wrappedCompleteElement = streamOpen + completeElement + streamClose; + connectionInternal.parseAndProcessElement(wrappedCompleteElement); + } + + + @Override + public void streamOpened(String prefix, Map attributes) { + if (LOGGER.isLoggable(STREAM_OPEN_CLOSE_DEBUG_LOG_LEVEL)) { + LOGGER.log(STREAM_OPEN_CLOSE_DEBUG_LOG_LEVEL, + "Stream of " + this + " opened. prefix=" + prefix + " attributes=" + attributes); + } + + final String prefixXmlns = "xmlns:" + prefix; + final StringBuilder streamClose = new StringBuilder(32); + final StringBuilder streamOpen = new StringBuilder(256); + + streamOpen.append('<'); + streamClose.append(""); + for (Entry entry : attributes.entrySet()) { + String attributeName = entry.getKey(); + String attributeValue = entry.getValue(); + switch (attributeName) { + case "to": + case "from": + case "id": + case "version": + break; + case "xml:lang": + streamOpen.append(" xml:lang='").append(attributeValue).append('\''); + break; + case "xmlns": + streamOpen.append(" xmlns='").append(attributeValue).append('\''); + break; + default: + if (attributeName.equals(prefixXmlns)) { + streamOpen.append(' ').append(prefixXmlns).append("='").append(attributeValue).append('\''); + break; + } + LOGGER.info("Unknown attribute: " + attributeName); + break; + } + } + streamOpen.append('>'); + + 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); + } + + @Override + public void streamClosed() { + if (LOGGER.isLoggable(STREAM_OPEN_CLOSE_DEBUG_LOG_LEVEL)) { + LOGGER.log(STREAM_OPEN_CLOSE_DEBUG_LOG_LEVEL, "Stream of " + this + " closed"); + } + + connectionInternal.onStreamClosed(); + } + }; + + private final ChannelSelectedCallback channelSelectedCallback = + (selectedChannel, selectedSelectionKey) -> { + assert selectionKey == null || selectionKey == selectedSelectionKey; + SocketChannel selectedSocketChannel = (SocketChannel) selectedChannel; + // We are *always* interested in OP_READ. + int newInterestedOps = SelectionKey.OP_READ; + boolean newPendingOutputFilterData = false; + + if (!channelSelectedCallbackLock.tryLock()) { + rejectedChannelSelectedCallbacks.incrementAndGet(); + return; + } + + handledChannelSelectedCallbacks++; + + long callbackBytesRead = 0; + long callbackBytesWritten = 0; + + try { + boolean destinationAddressChanged = false; + boolean isLastPartOfElement = false; + TopLevelStreamElement currentlyOutgonigTopLevelStreamElement = null; + StringBuilder outgoingStreamForDebugger = null; + + writeLoop: while (true) { + final boolean moreDataAvailable = !isLastPartOfElement || !connectionInternal.outgoingElementsQueue.isEmpty(); + + if (filteredOutgoingBuffer != null || !networkOutgoingBuffers.isEmpty()) { + if (filteredOutgoingBuffer != null) { + networkOutgoingBuffers.add(filteredOutgoingBuffer); + networkOutgoingBuffersBytes += filteredOutgoingBuffer.remaining(); + + filteredOutgoingBuffer = null; + if (moreDataAvailable && networkOutgoingBuffersBytes < 8096) { + continue; + } + } + + ByteBuffer[] output = networkOutgoingBuffers.toArray(new ByteBuffer[networkOutgoingBuffers.size()]); + long bytesWritten; + try { + bytesWritten = selectedSocketChannel.write(output); + } catch (IOException e) { + // We have seen here so far + // - IOException "Broken pipe" + handleReadWriteIoException(e); + break; + } + + if (bytesWritten == 0) { + newInterestedOps |= SelectionKey.OP_WRITE; + break; + } + + callbackBytesWritten += bytesWritten; + + networkOutgoingBuffersBytes -= bytesWritten; + + List prunedBuffers = pruneBufferList(networkOutgoingBuffers); + + for (Buffer prunedBuffer : prunedBuffers) { + List sendElements = bufferToElementMap.remove(prunedBuffer); + if (sendElements == null) { + continue; + } + for (TopLevelStreamElement elementJustSend : sendElements) { + connectionInternal.fireFirstLevelElementSendListeners(elementJustSend); + } + } + + // Prevent one callback from dominating the reactor thread. Break out of the write-loop if we have + // written a certain amount. + if (callbackBytesWritten > CALLBACK_MAX_BYTES_WRITEN) { + newInterestedOps |= SelectionKey.OP_WRITE; + callbackPreemtBecauseBytesWritten++; + break; + } + } else if (outgoingBuffer != null || pendingOutputFilterData) { + pendingOutputFilterData = false; + + if (outgoingBuffer != null) { + totalBytesWrittenBeforeFilter += outgoingBuffer.remaining(); + if (isLastPartOfElement) { + assert currentlyOutgonigTopLevelStreamElement != null; + currentlyOutgoingElements.add(currentlyOutgonigTopLevelStreamElement); + } + } + + ByteBuffer outputFilterInputData = outgoingBuffer; + // We can now null the outgoingBuffer since the filter step will take care of it from now on. + outgoingBuffer = null; + + for (ListIterator it = connectionInternal.getXmppInputOutputFilterBeginIterator(); it.hasNext();) { + XmppInputOutputFilter inputOutputFilter = it.next(); + XmppInputOutputFilter.OutputResult outputResult; + try { + outputResult = inputOutputFilter.output(outputFilterInputData, isLastPartOfElement, + destinationAddressChanged, moreDataAvailable); + } catch (IOException e) { + connectionInternal.notifyConnectionError(e); + break writeLoop; + } + newPendingOutputFilterData |= outputResult.pendingFilterData; + outputFilterInputData = outputResult.filteredOutputData; + if (outputFilterInputData != null) { + outputFilterInputData.flip(); + } + } + + // It is ok if outpuFilterInputData is 'null' here, this is expected behavior. + if (outputFilterInputData != null && outputFilterInputData.hasRemaining()) { + filteredOutgoingBuffer = outputFilterInputData; + } else { + filteredOutgoingBuffer = null; + } + + // If the filters did eventually not produce any output data but if there is + // pending output data then we have a pending write request after read. + if (filteredOutgoingBuffer == null && newPendingOutputFilterData) { + pendingWriteInterestAfterRead = true; + } + + if (filteredOutgoingBuffer != null && isLastPartOfElement) { + bufferToElementMap.put(filteredOutgoingBuffer, new ArrayList<>(currentlyOutgoingElements)); + currentlyOutgoingElements.clear(); + } + + // Reset that the destination address has changed. + if (destinationAddressChanged) { + destinationAddressChanged = false; + } + } else if (outgoingCharSequenceIterator != null) { + CharSequence nextCharSequence = outgoingCharSequenceIterator.next(); + outgoingBuffer = UTF8.encode(nextCharSequence); + if (!outgoingCharSequenceIterator.hasNext()) { + outgoingCharSequenceIterator = null; + isLastPartOfElement = true; + } else { + isLastPartOfElement = false; + } + + final SmackDebugger debugger = connectionInternal.smackDebugger; + if (debugger != null) { + if (outgoingStreamForDebugger == null) { + outgoingStreamForDebugger = new StringBuilder(); + } + outgoingStreamForDebugger.append(nextCharSequence); + + if (isLastPartOfElement) { + try { + outputDebugSplitter.append(outgoingStreamForDebugger); + } catch (IOException e) { + throw new AssertionError(e); + } + debugger.onOutgoingElementCompleted(); + outgoingStreamForDebugger = null; + } + } + } else if (!connectionInternal.outgoingElementsQueue.isEmpty()) { + currentlyOutgonigTopLevelStreamElement = connectionInternal.outgoingElementsQueue.poll(); + if (currentlyOutgonigTopLevelStreamElement instanceof Stanza) { + Stanza currentlyOutgoingStanza = (Stanza) currentlyOutgonigTopLevelStreamElement; + Jid currentDestinationAddress = currentlyOutgoingStanza.getTo(); + destinationAddressChanged = !JidUtil.equals(lastDestinationAddress, currentDestinationAddress); + lastDestinationAddress = currentDestinationAddress; + } + CharSequence nextCharSequence = currentlyOutgonigTopLevelStreamElement.toXML(StreamOpen.CLIENT_NAMESPACE); + if (nextCharSequence instanceof XmlStringBuilder) { + XmlStringBuilder xmlStringBuilder = (XmlStringBuilder) nextCharSequence; + XmlEnvironment outgoingStreamXmlEnvironment = connectionInternal.getOutgoingStreamXmlEnvironment(); + outgoingCharSequenceIterator = xmlStringBuilder.toList(outgoingStreamXmlEnvironment).iterator(); + } else { + outgoingCharSequenceIterator = Collections.singletonList(nextCharSequence).iterator(); + } + assert outgoingCharSequenceIterator != null; + } else { + // There is nothing more to write. + break; + } + } + + pendingOutputFilterData = newPendingOutputFilterData; + if (!pendingWriteInterestAfterRead && pendingOutputFilterData) { + newInterestedOps |= SelectionKey.OP_WRITE; + } + + readLoop: while (true) { + // Prevent one callback from dominating the reactor thread. Break out of the read-loop if we have + // read a certain amount. + if (callbackBytesRead > CALLBACK_MAX_BYTES_READ) { + callbackPreemtBecauseBytesRead++; + break; + } + + int bytesRead; + incomingBuffer.clear(); + try { + bytesRead = selectedSocketChannel.read(incomingBuffer); + } catch (IOException e) { + handleReadWriteIoException(e); + return; + } + + if (bytesRead < 0) { + LOGGER.finer("NIO read() returned " + bytesRead + + " for " + this + ". This probably means that the TCP connection was terminated."); + // According to the socket channel javadoc section about "asynchronous reads" a socket channel's + // read() may return -1 if the input side of a socket is shut down. + + // Note that we do not call notifyConnectionError() here because the connection may be + // cleanly shutdown which would also cause read() to return '-1. I assume that this socket + // will be selected again, on which read() would throw an IOException, which will be catched + // and invoke notifyConnectionError() (see a few lines above). + /* + IOException exception = new IOException("NIO read() returned " + bytesRead); + notifyConnectionError(exception); + */ + return; + } + + if (!pendingInputFilterData) { + if (bytesRead == 0) { + // Nothing more to read. + break; + } + } else { + pendingInputFilterData = false; + } + + // We have successfully read something. It is now possible that a filter is now also able to write + // additional data (for example SSLEngine). + if (pendingWriteInterestAfterRead) { + pendingWriteInterestAfterRead = false; + newInterestedOps |= SelectionKey.OP_WRITE; + } + + callbackBytesRead += bytesRead; + + ByteBuffer filteredIncomingBuffer = incomingBuffer; + for (ListIterator it = connectionInternal.getXmppInputOutputFilterEndIterator(); it.hasPrevious();) { + filteredIncomingBuffer.flip(); + + ByteBuffer newFilteredIncomingBuffer; + try { + newFilteredIncomingBuffer = it.previous().input(filteredIncomingBuffer); + } catch (IOException e) { + connectionInternal.notifyConnectionError(e); + return; + } + if (newFilteredIncomingBuffer == null) { + break readLoop; + } + filteredIncomingBuffer = newFilteredIncomingBuffer; + } + + final int bytesReadAfterFilter = filteredIncomingBuffer.flip().remaining(); + + totalBytesReadAfterFilter += bytesReadAfterFilter; + + try { + splitter.write(filteredIncomingBuffer); + } catch (IOException e) { + connectionInternal.notifyConnectionError(e); + return; + } + } + } finally { + totalBytesWritten += callbackBytesWritten; + totalBytesRead += callbackBytesRead; + + channelSelectedCallbackLock.unlock(); + } + + // Indicate that there is no reactor thread racing towards handling this selection key. + final SelectionKeyAttachment selectionKeyAttachment = this.selectionKeyAttachment; + if (selectionKeyAttachment != null) { + selectionKeyAttachment.resetReactorThreadRacing(); + } + + // Check the queue again to prevent lost wakeups caused by elements inserted before we + // called resetReactorThreadRacing() a few lines above. + if (!connectionInternal.outgoingElementsQueue.isEmpty()) { + setWriteInterestAfterChannelSelectedCallback.incrementAndGet(); + newInterestedOps |= SelectionKey.OP_WRITE; + } + + connectionInternal.setInterestOps(selectionKey, newInterestedOps); + }; + + private void handleReadWriteIoException(IOException e) { + if (e instanceof ClosedChannelException && !tcpNioTransport.isConnected()) { + // The connection is already closed. + return; + } + + connectionInternal.notifyConnectionError(e); + } + + /** + * This is the interface between the "lookup remote connection endpoints" state and the "establish TCP connection" + * state. The field is indirectly populated by {@link XmppTcpNioTransport#lookupConnectionEndpoints()} and consumed + * by {@link ConnectionAttemptState}. + */ + DiscoveredTcpEndpoints discoveredTcpEndpoints; + + final class XmppTcpNioTransport extends XmppClientToServerTransport { + + protected XmppTcpNioTransport(ModularXmppClientToServerConnectionInternal connectionInternal) { + super(connectionInternal); + } + + @Override + protected void resetDiscoveredConnectionEndpoints() { + discoveredTcpEndpoints = null; + } + + @Override + protected List> lookupConnectionEndpoints() { + // Assert that there are no stale discovred endpoints prior performing the lookup. + assert discoveredTcpEndpoints == null; + + List> futures = new ArrayList<>(2); + + InternalSmackFuture tcpEndpointsLookupFuture = new InternalSmackFuture<>(); + connectionInternal.asyncGo(() -> { + Result result = RemoteXmppTcpConnectionEndpoints.lookup( + connectionInternal.connection.getConfiguration()); + + LookupConnectionEndpointsResult endpointsResult; + if (result.discoveredRemoteConnectionEndpoints.isEmpty()) { + endpointsResult = new TcpEndpointDiscoveryFailed(result); + } else { + endpointsResult = new DiscoveredTcpEndpoints(result); + } + tcpEndpointsLookupFuture.setResult(endpointsResult); + }); + futures.add(tcpEndpointsLookupFuture); + + if (moduleDescriptor.isDirectTlsEnabled()) { + // TODO: Implement this. + throw new IllegalArgumentException("DirectTLS is not implemented yet"); + } + + return futures; + } + + @Override + protected void loadConnectionEndpoints(LookupConnectionEndpointsSuccess lookupConnectionEndpointsSuccess) { + // The API contract stats that we will be given the instance we handed out with lookupConnectionEndpoints, + // which must be of type DiscoveredTcpEndpoints here. Hence if we can not cast it, then there is an internal + // Smack error. + discoveredTcpEndpoints = (DiscoveredTcpEndpoints) lookupConnectionEndpointsSuccess; + } + + @Override + protected void afterFiltersClosed() { + pendingInputFilterData = pendingOutputFilterData = true; + afterOutgoingElementsQueueModified(); + } + + @Override + protected void disconnect() { + XmppTcpTransportModule.this.closeSocketAndCleanup(); + } + + @Override + protected void notifyAboutNewOutgoingElements() { + afterOutgoingElementsQueueModified(); + } + + @Override + public SSLSession getSslSession() { + TlsState tlsState = XmppTcpTransportModule.this.tlsState; + if (tlsState == null) { + return null; + } + + return tlsState.engine.getSession(); + } + + @Override + public boolean isConnected() { + SocketChannel socketChannel = XmppTcpTransportModule.this.socketChannel; + if (socketChannel == null) { + return false; + } + + return socketChannel.isConnected(); + } + + @Override + public boolean isTransportSecured() { + final TlsState tlsState = XmppTcpTransportModule.this.tlsState; + return tlsState != null && tlsState.handshakeStatus == TlsHandshakeStatus.successful; + } + + @Override + public Stats getStats() { + return XmppTcpTransportModule.this.getStats(); + } + + final class DiscoveredTcpEndpoints implements LookupConnectionEndpointsSuccess { + final RemoteXmppTcpConnectionEndpoints.Result result; + DiscoveredTcpEndpoints(RemoteXmppTcpConnectionEndpoints.Result result) { + this.result = result; + } + } + + final class TcpEndpointDiscoveryFailed implements LookupConnectionEndpointsFailed { + final List lookupFailures; + TcpEndpointDiscoveryFailed(RemoteXmppTcpConnectionEndpoints.Result result) { + lookupFailures = result.lookupFailures; + } + } + } + + private void afterOutgoingElementsQueueModified() { + final SelectionKeyAttachment selectionKeyAttachment = this.selectionKeyAttachment; + if (selectionKeyAttachment != null && selectionKeyAttachment.isReactorThreadRacing()) { + // A reactor thread is already racing to the channel selected callback and will take care of this. + reactorThreadAlreadyRacing.incrementAndGet(); + return; + } + + afterOutgoingElementsQueueModifiedSetInterestOps.incrementAndGet(); + + // Add OP_WRITE to the interested Ops, since we have now new things to write. Note that this may cause + // multiple reactor threads to race to the channel selected callback in case we perform this right after + // a select() returned with this selection key in the selected-key set. Hence we use tryLock() in the + // channel selected callback to keep the invariant that only exactly one thread is performing the + // callback. + // Note that we need to perform setInterestedOps() *without* holding the channelSelectedCallbackLock, as + // otherwise the reactor thread racing to the channel selected callback may found the lock still locked, which + // would result in the outgoingElementsQueue not being handled. + connectionInternal.setInterestOps(selectionKey, SelectionKey.OP_WRITE | SelectionKey.OP_READ); + } + + @Override + protected XmppTcpNioTransport getTransport() { + return tcpNioTransport; + } + + static final class EstablishingTcpConnectionStateDescriptor extends StateDescriptor { + private EstablishingTcpConnectionStateDescriptor() { + super(XmppTcpTransportModule.EstablishingTcpConnectionState.class); + addPredeccessor(LookupRemoteConnectionEndpointsStateDescriptor.class); + addSuccessor(EstablishTlsStateDescriptor.class); + addSuccessor(ConnectedButUnauthenticatedStateDescriptor.class); + } + + @Override + protected XmppTcpTransportModule.EstablishingTcpConnectionState constructState(ModularXmppClientToServerConnectionInternal connectionInternal) { + XmppTcpTransportModule tcpTransportModule = connectionInternal.connection.getConnectionModuleFor(XmppTcpTransportModuleDescriptor.class); + return tcpTransportModule.constructEstablishingTcpConnectionState(this, connectionInternal); + } + } + + private EstablishingTcpConnectionState constructEstablishingTcpConnectionState( + EstablishingTcpConnectionStateDescriptor stateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + return new EstablishingTcpConnectionState(stateDescriptor, connectionInternal); + } + + final class EstablishingTcpConnectionState extends State { + private EstablishingTcpConnectionState(EstablishingTcpConnectionStateDescriptor stateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + super(stateDescriptor, connectionInternal); + } + + @Override + public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) + throws InterruptedException, ConnectionUnexpectedTerminatedException, NotConnectedException, + NoResponseException, IOException { + // The fields inetSocketAddress and failedAddresses are handed over from LookupHostAddresses to + // ConnectingToHost. + ConnectionAttemptState connectionAttemptState = new ConnectionAttemptState(connectionInternal, discoveredTcpEndpoints, + this); + connectionAttemptState.establishTcpConnection(); + + try { + connectionAttemptState.tcpConnectionEstablishedSyncPoint.checkIfSuccessOrWaitOrThrow(); + } catch (ConnectionException | NoResponseException e) { + // TODO: It is not really elegant that we catch the exception here. Ideally ConnectionAttemptState would + // simply return a StateTranstionResult.FailureCausedByException. + return new StateTransitionResult.FailureCausedByException<>(e); + } catch (SmackWrappedException e) { + // Should never throw SmackWrappedException. + throw new AssertionError(e); + } + + socketChannel = connectionAttemptState.socketChannel; + remoteAddress = (InetSocketAddress) socketChannel.socket().getRemoteSocketAddress(); + + selectionKey = connectionInternal.registerWithSelector(socketChannel, SelectionKey.OP_READ, + channelSelectedCallback); + selectionKeyAttachment = (SelectionKeyAttachment) selectionKey.attachment(); + + connectionInternal.setTransport(tcpNioTransport); + + connectionInternal.newStreamOpenWaitForFeaturesSequence("stream features after initial connection"); + + return new TcpSocketConnectedResult(remoteAddress); + } + + @Override + public void resetState() { + closeSocketAndCleanup(); + } + } + + public static final class TcpSocketConnectedResult extends StateTransitionResult.Success { + private final InetSocketAddress remoteAddress; + + private TcpSocketConnectedResult(InetSocketAddress remoteAddress) { + super("TCP connection established to " + remoteAddress); + this.remoteAddress = remoteAddress; + } + + public InetSocketAddress getRemoteAddress() { + return remoteAddress; + } + } + + public static final class TlsEstablishedResult extends StateTransitionResult.Success { + + private TlsEstablishedResult(SSLEngine sslEngine) { + super("TLS established: " + sslEngine.getSession()); + } + } + + static final class EstablishTlsStateDescriptor extends StateDescriptor { + private EstablishTlsStateDescriptor() { + super(XmppTcpTransportModule.EstablishTlsState.class, "RFC 6120 ยง 5"); + addSuccessor(ConnectedButUnauthenticatedStateDescriptor.class); + declarePrecedenceOver(ConnectedButUnauthenticatedStateDescriptor.class); + } + + @Override + protected EstablishTlsState constructState(ModularXmppClientToServerConnectionInternal connectionInternal) { + XmppTcpTransportModule tcpTransportModule = connectionInternal.connection.getConnectionModuleFor(XmppTcpTransportModuleDescriptor.class); + return tcpTransportModule.constructEstablishingTlsState(this, connectionInternal); + } + } + + private EstablishTlsState constructEstablishingTlsState( + EstablishTlsStateDescriptor stateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + return new EstablishTlsState(stateDescriptor, connectionInternal); + } + + private final class EstablishTlsState extends State { + private EstablishTlsState(EstablishTlsStateDescriptor stateDescriptor, + ModularXmppClientToServerConnectionInternal connectionInternal) { + super(stateDescriptor, connectionInternal); + } + + @Override + public StateTransitionResult.TransitionImpossible isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) + throws SecurityRequiredByClientException, SecurityRequiredByServerException { + StartTls startTlsFeature = connectionInternal.connection.getFeature(StartTls.ELEMENT, StartTls.NAMESPACE); + SecurityMode securityMode = connectionInternal.connection.getConfiguration().getSecurityMode(); + + switch (securityMode) { + case required: + case ifpossible: + if (startTlsFeature == null) { + if (securityMode == SecurityMode.ifpossible) { + return new StateTransitionResult.TransitionImpossibleReason("Server does not announce support for TLS and we do not required it"); + } + throw new SecurityRequiredByClientException(); + } + // Allows transition by returning null. + return null; + case disabled: + if (startTlsFeature != null && startTlsFeature.required()) { + throw new SecurityRequiredByServerException(); + } + return new StateTransitionResult.TransitionImpossibleReason("TLS disabled in client settings and server does not require it"); + default: + throw new AssertionError("Unknown security mode: " + securityMode); + } + } + + @Override + public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) + throws SmackWrappedException, FailedNonzaException, IOException, InterruptedException, + ConnectionUnexpectedTerminatedException, NoResponseException, NotConnectedException { + connectionInternal.sendAndWaitForResponse(StartTls.INSTANCE, TlsProceed.class, TlsFailure.class); + + SmackTlsContext smackTlsContext; + try { + smackTlsContext = connectionInternal.getSmackTlsContext(); + } catch (KeyManagementException | UnrecoverableKeyException | NoSuchAlgorithmException + | CertificateException | KeyStoreException | NoSuchProviderException e) { + throw new SmackWrappedException(e); + } + + tlsState = new TlsState(smackTlsContext); + connectionInternal.addXmppInputOutputFilter(tlsState); + + channelSelectedCallbackLock.lock(); + try { + pendingOutputFilterData = true; + // The beginHandshake() is possibly not really required here, but it does not hurt either. + tlsState.engine.beginHandshake(); + tlsState.handshakeStatus = TlsHandshakeStatus.initiated; + } finally { + channelSelectedCallbackLock.unlock(); + } + connectionInternal.setInterestOps(selectionKey, SelectionKey.OP_WRITE | SelectionKey.OP_READ); + + try { + tlsState.waitForHandshakeFinished(); + } catch (CertificateException e) { + throw new SmackWrappedException(e); + } + + connectionInternal.newStreamOpenWaitForFeaturesSequence("stream features after TLS established"); + + return new TlsEstablishedResult(tlsState.engine); + } + + @Override + public void resetState() { + tlsState = null; + } + } + + private enum TlsHandshakeStatus { + initial, + initiated, + successful, + failed, + } + + private static final Level SSL_ENGINE_DEBUG_LOG_LEVEL = Level.FINEST; + + private static void debugLogSslEngineResult(String operation, SSLEngineResult result) { + if (!LOGGER.isLoggable(SSL_ENGINE_DEBUG_LOG_LEVEL)) { + return; + } + + LOGGER.log(SSL_ENGINE_DEBUG_LOG_LEVEL, "SSLEngineResult of " + operation + "(): " + result); + } + + private final class TlsState implements XmppInputOutputFilter { + + private static final int MAX_PENDING_OUTPUT_BYTES = 8096; + + private final SmackTlsContext smackTlsContext; + private final SSLEngine engine; + + private TlsHandshakeStatus handshakeStatus = TlsHandshakeStatus.initial; + private SSLException handshakeException; + + private ByteBuffer myNetData; + private ByteBuffer peerAppData; + + private final List pendingOutputData = new ArrayList<>(); + private int pendingOutputBytes; + private ByteBuffer pendingInputData; + + private final AtomicInteger pendingDelegatedTasks = new AtomicInteger(); + + private long wrapInBytes; + private long wrapOutBytes; + + private long unwrapInBytes; + private long unwrapOutBytes; + + private TlsState(SmackTlsContext smackTlsContext) throws IOException { + this.smackTlsContext = smackTlsContext; + + // Call createSSLEngine()'s variant with two parameters as this allows for TLS session resumption. + + // Note that it is not really clear what the value of peer host should be. It could be A) the XMPP service's + // domainpart or B) the DNS name of the host we are connecting to (usually the DNS SRV RR target name). While + // the javadoc of createSSLEngine(String, int) indicates with "Some cipher suites (such as Kerberos) require + // remote hostname information, in which case peerHost needs to be specified." that A should be used. TLS + // session resumption may would need or at least benefit from B. Variant A would also be required if the + // String is used for certificate verification. And it appears at least likely that TLS session resumption + // would not be hurt by using variant A. Therefore we currently use variant A. + // TODO: Should we use the ACE representation of the XMPP service domain? Compare with f60e4055ec529f0b8160acedf13275592ab10a4b + // If yes, then we should probably introduce getXmppServiceDomainAceEncodedIfPossible(). + String peerHost = connectionInternal.connection.getConfiguration().getXMPPServiceDomain().toString(); + engine = smackTlsContext.sslContext.createSSLEngine(peerHost, remoteAddress.getPort()); + engine.setUseClientMode(true); + + SSLSession session = engine.getSession(); + int applicationBufferSize = session.getApplicationBufferSize(); + int packetBufferSize = session.getPacketBufferSize(); + + myNetData = ByteBuffer.allocateDirect(packetBufferSize); + peerAppData = ByteBuffer.allocate(applicationBufferSize); + } + + @Override + public OutputResult output(ByteBuffer outputData, boolean isFinalDataOfElement, boolean destinationAddressChanged, + boolean moreDataAvailable) throws SSLException { + if (outputData != null) { + pendingOutputData.add(outputData); + pendingOutputBytes += outputData.remaining(); + if (moreDataAvailable && pendingOutputBytes < MAX_PENDING_OUTPUT_BYTES) { + return OutputResult.NO_OUTPUT; + } + } + + ByteBuffer[] outputDataArray = pendingOutputData.toArray(new ByteBuffer[pendingOutputData.size()]); + + myNetData.clear(); + + while (true) { + SSLEngineResult result; + try { + result = engine.wrap(outputDataArray, myNetData); + } catch (SSLException e) { + handleSslException(e); + throw e; + } + + debugLogSslEngineResult("wrap", result); + + SSLEngineResult.Status engineResultStatus = result.getStatus(); + + pendingOutputBytes -= result.bytesConsumed(); + + if (engineResultStatus == SSLEngineResult.Status.OK) { + wrapInBytes += result.bytesConsumed(); + wrapOutBytes += result.bytesProduced(); + + SSLEngineResult.HandshakeStatus handshakeStatus = handleHandshakeStatus(result); + switch (handshakeStatus) { + case NEED_UNWRAP: + // NEED_UNWRAP means that we need to receive something in order to continue the handshake. The + // standard channelSelectedCallback logic will take care of this, as there is eventually always + // a interest to read from the socket. + break; + case NEED_WRAP: + // Same as need task: Cycle the reactor. + case NEED_TASK: + // Note that we also set pendingOutputFilterData in the OutputResult in the NEED_TASK case, as + // we also want to retry the wrap() operation above in this case. + return new OutputResult(true, myNetData); + default: + break; + } + } + + switch (engineResultStatus) { + case OK: + // No need to outputData.compact() here, since we do not reuse the buffer. + // Clean up the pending output data. + pruneBufferList(pendingOutputData); + return new OutputResult(!pendingOutputData.isEmpty(), myNetData); + case CLOSED: + pendingOutputData.clear(); + return OutputResult.NO_OUTPUT; + case BUFFER_OVERFLOW: + LOGGER.warning("SSLEngine status BUFFER_OVERFLOW, this is hopefully uncommon"); + int outputDataRemaining = outputData != null ? outputData.remaining() : 0; + int newCapacity = (int) (1.3 * outputDataRemaining); + // If newCapacity would not increase myNetData, then double it. + if (newCapacity <= myNetData.capacity()) { + newCapacity = 2 * myNetData.capacity(); + } + ByteBuffer newMyNetData = ByteBuffer.allocateDirect(newCapacity); + myNetData.flip(); + newMyNetData.put(myNetData); + myNetData = newMyNetData; + continue; + case BUFFER_UNDERFLOW: + throw new IllegalStateException( + "Buffer underflow as result of SSLEngine.wrap() should never happen"); + } + } + } + + @Override + public ByteBuffer input(ByteBuffer inputData) throws SSLException { + ByteBuffer accumulatedData; + if (pendingInputData == null) { + accumulatedData = inputData; + } else { + int accumulatedDataBytes = pendingInputData.remaining() + inputData.remaining(); + accumulatedData = ByteBuffer.allocate(accumulatedDataBytes); + accumulatedData.put(pendingInputData) + .put(inputData) + .flip(); + pendingInputData = null; + } + + peerAppData.clear(); + + while (true) { + SSLEngineResult result; + try { + result = engine.unwrap(accumulatedData, peerAppData); + } catch (SSLException e) { + handleSslException(e); + throw e; + } + + debugLogSslEngineResult("unwrap", result); + + SSLEngineResult.Status engineResultStatus = result.getStatus(); + + if (engineResultStatus == SSLEngineResult.Status.OK) { + unwrapInBytes += result.bytesConsumed(); + unwrapOutBytes += result.bytesProduced(); + + SSLEngineResult.HandshakeStatus handshakeStatus = handleHandshakeStatus(result); + switch (handshakeStatus) { + case NEED_TASK: + // A delegated task is asynchronously running. Signal that there is pending input data and + // cycle again through the smack reactor. + addAsPendingInputData(accumulatedData); + break; + case NEED_UNWRAP: + continue; + case NEED_WRAP: + // NEED_WRAP means that the SSLEngine needs to send data, probably without consuming data. + // We exploit here the fact that the channelSelectedCallback is single threaded and that the + // input processing is after the output processing. + connectionInternal.asyncGo(() -> callChannelSelectedCallback(false, true)); + break; + default: + break; + } + } + + switch (engineResultStatus) { + case OK: + // SSLEngine's unwrap() may not consume all bytes from the source buffer. If this is the case, then + // simply perform another unwrap until accumlatedData has no remaining bytes. + if (accumulatedData.hasRemaining()) { + continue; + } + return peerAppData; + case CLOSED: + return null; + case BUFFER_UNDERFLOW: + // There were not enough source bytes available to make a complete packet. Let it in + // pendingInputData. Note that we do not resize SSLEngine's source buffer - inputData in our case - + // as it is not possible. + addAsPendingInputData(accumulatedData); + return null; + case BUFFER_OVERFLOW: + int applicationBufferSize = engine.getSession().getApplicationBufferSize(); + assert peerAppData.remaining() < applicationBufferSize; + peerAppData = ByteBuffer.allocate(applicationBufferSize); + continue; + } + } + } + + private void addAsPendingInputData(ByteBuffer byteBuffer) { + pendingInputData = ByteBuffer.allocate(byteBuffer.remaining()); + pendingInputData.put(byteBuffer).flip(); + } + + private SSLEngineResult.HandshakeStatus handleHandshakeStatus(SSLEngineResult sslEngineResult) { + SSLEngineResult.HandshakeStatus handshakeStatus = sslEngineResult.getHandshakeStatus(); + switch (handshakeStatus) { + case NEED_TASK: + while (true) { + final Runnable delegatedTask = engine.getDelegatedTask(); + if (delegatedTask == null) { + break; + } + sslEngineDelegatedTasks++; + int currentPendingDelegatedTasks = pendingDelegatedTasks.incrementAndGet(); + if (currentPendingDelegatedTasks > maxPendingSslEngineDelegatedTasks) { + maxPendingSslEngineDelegatedTasks = currentPendingDelegatedTasks; + } + + Runnable wrappedDelegatedTask = () -> { + delegatedTask.run(); + int wrappedCurrentPendingDelegatedTasks = pendingDelegatedTasks.decrementAndGet(); + if (wrappedCurrentPendingDelegatedTasks == 0) { + callChannelSelectedCallback(true, true); + } + }; + connectionInternal.asyncGo(wrappedDelegatedTask); + } + break; + case FINISHED: + onHandshakeFinished(); + break; + default: + break; + } + + SSLEngineResult.HandshakeStatus afterHandshakeStatus = engine.getHandshakeStatus(); + return afterHandshakeStatus; + } + + private void handleSslException(SSLException e) { + handshakeException = e; + handshakeStatus = TlsHandshakeStatus.failed; + synchronized (this) { + notifyAll(); + } + } + + private void onHandshakeFinished() { + handshakeStatus = TlsHandshakeStatus.successful; + synchronized (this) { + notifyAll(); + } + } + + private boolean isHandshakeFinished() { + return handshakeStatus == TlsHandshakeStatus.successful || handshakeStatus == TlsHandshakeStatus.failed; + } + + private void waitForHandshakeFinished() throws InterruptedException, CertificateException, SSLException, ConnectionUnexpectedTerminatedException, NoResponseException { + final long deadline = System.currentTimeMillis() + connectionInternal.connection.getReplyTimeout(); + + Exception currentConnectionException = null; + synchronized (this) { + while (!isHandshakeFinished() + && (currentConnectionException = connectionInternal.getCurrentConnectionException()) == null) { + final long now = System.currentTimeMillis(); + if (now >= deadline) + break; + wait(deadline - now); + } + } + + if (currentConnectionException != null) { + throw new SmackException.ConnectionUnexpectedTerminatedException(currentConnectionException); + } + + if (!isHandshakeFinished()) { + throw NoResponseException.newWith(connectionInternal.connection, "TLS handshake to finish"); + } + + if (handshakeStatus == TlsHandshakeStatus.failed) { + throw handshakeException; + } + + assert handshakeStatus == TlsHandshakeStatus.successful; + + if (smackTlsContext.daneVerifier != null) { + smackTlsContext.daneVerifier.finish(engine.getSession()); + } + } + + @Override + public Object getStats() { + return new TlsStateStats(this); + } + + @Override + public void closeInputOutput() { + engine.closeOutbound(); + try { + engine.closeInbound(); + } catch (SSLException e) { + LOGGER.log(Level.FINEST, + "SSLException when closing inbound TLS session. This can likely be ignored if a possible truncation attack is suggested." + + " You may want to ask your XMPP server vendor to implement a clean TLS session shutdown sending close_notify after ", + e); + } + } + + @Override + public void waitUntilInputOutputClosed() throws IOException, CertificateException, InterruptedException, + ConnectionUnexpectedTerminatedException, NoResponseException { + waitForHandshakeFinished(); + } + + @Override + public String getFilterName() { + return "TLS (" + engine + ')'; + } + } + + public static final class TlsStateStats { + public final long wrapInBytes; + public final long wrapOutBytes; + public final double wrapRatio; + + public final long unwrapInBytes; + public final long unwrapOutBytes; + public final double unwrapRatio; + + private TlsStateStats(TlsState tlsState) { + wrapOutBytes = tlsState.wrapOutBytes; + wrapInBytes = tlsState.wrapInBytes; + wrapRatio = (double) wrapOutBytes / wrapInBytes; + + unwrapOutBytes = tlsState.unwrapOutBytes; + unwrapInBytes = tlsState.unwrapInBytes; + unwrapRatio = (double) unwrapInBytes / unwrapOutBytes; + } + + private transient String toStringCache; + + @Override + public String toString() { + if (toStringCache != null) { + return toStringCache; + } + + toStringCache = + "wrap-in-bytes: " + wrapInBytes + '\n' + + "wrap-out-bytes: " + wrapOutBytes + '\n' + + "wrap-ratio: " + wrapRatio + '\n' + + "unwrap-in-bytes: " + unwrapInBytes + '\n' + + "unwrap-out-bytes: " + unwrapOutBytes + '\n' + + "unwrap-ratio: " + unwrapRatio + '\n' + ; + + return toStringCache; + } + } + + private void callChannelSelectedCallback(boolean setPendingInputFilterData, boolean setPendingOutputFilterData) { + final SocketChannel channel = socketChannel; + final SelectionKey key = selectionKey; + if (channel == null || key == null) { + LOGGER.info("Not calling channel selected callback because the connection was eventually disconnected"); + return; + } + + channelSelectedCallbackLock.lock(); + try { + // Note that it is important that we send the pending(Input|Output)FilterData flags while holding the lock. + if (setPendingInputFilterData) { + pendingInputFilterData = true; + } + if (setPendingOutputFilterData) { + pendingOutputFilterData = true; + } + + channelSelectedCallback.onChannelSelected(channel, key); + } finally { + channelSelectedCallbackLock.unlock(); + } + } + + private void closeSocketAndCleanup() { + final SelectionKey selectionKey = this.selectionKey; + if (selectionKey != null) { + selectionKey.cancel(); + } + final SocketChannel socketChannel = this.socketChannel; + if (socketChannel != null) { + try { + socketChannel.close(); + } catch (IOException e) { + + } + } + + this.selectionKey = null; + this.socketChannel = null; + + selectionKeyAttachment = null; + remoteAddress = null; + } + + private static List pruneBufferList(Collection buffers) { + return CollectionUtil.removeUntil(buffers, b -> b.hasRemaining()); + } + + public Stats getStats() { + return new Stats(this); + } + + public static final class Stats extends XmppClientToServerTransport.Stats { + public final long totalBytesWritten; + public final long totalBytesWrittenBeforeFilter; + public final double writeRatio; + + public final long totalBytesRead; + public final long totalBytesReadAfterFilter; + public final double readRatio; + + public final long handledChannelSelectedCallbacks; + public final long setWriteInterestAfterChannelSelectedCallback; + public final long reactorThreadAlreadyRacing; + public final long afterOutgoingElementsQueueModifiedSetInterestOps; + public final long rejectedChannelSelectedCallbacks; + public final long totalCallbackRequests; + public final long callbackPreemtBecauseBytesWritten; + public final long callbackPreemtBecauseBytesRead; + public final int sslEngineDelegatedTasks; + public final int maxPendingSslEngineDelegatedTasks; + + private Stats(XmppTcpTransportModule connection) { + totalBytesWritten = connection.totalBytesWritten; + totalBytesWrittenBeforeFilter = connection.totalBytesWrittenBeforeFilter; + writeRatio = (double) totalBytesWritten / totalBytesWrittenBeforeFilter; + + totalBytesReadAfterFilter = connection.totalBytesReadAfterFilter; + totalBytesRead = connection.totalBytesRead; + readRatio = (double) totalBytesRead / totalBytesReadAfterFilter; + + handledChannelSelectedCallbacks = connection.handledChannelSelectedCallbacks; + setWriteInterestAfterChannelSelectedCallback = connection.setWriteInterestAfterChannelSelectedCallback.get(); + reactorThreadAlreadyRacing = connection.reactorThreadAlreadyRacing.get(); + afterOutgoingElementsQueueModifiedSetInterestOps = connection.afterOutgoingElementsQueueModifiedSetInterestOps + .get(); + rejectedChannelSelectedCallbacks = connection.rejectedChannelSelectedCallbacks.get(); + + totalCallbackRequests = handledChannelSelectedCallbacks + rejectedChannelSelectedCallbacks; + + callbackPreemtBecauseBytesRead = connection.callbackPreemtBecauseBytesRead; + callbackPreemtBecauseBytesWritten = connection.callbackPreemtBecauseBytesWritten; + + sslEngineDelegatedTasks = connection.sslEngineDelegatedTasks; + maxPendingSslEngineDelegatedTasks = connection.maxPendingSslEngineDelegatedTasks; + } + + private transient String toStringCache; + + @Override + public String toString() { + if (toStringCache != null) { + return toStringCache; + } + + toStringCache = + "Total bytes\n" + + "recv: " + totalBytesRead + '\n' + + "send: " + totalBytesWritten + '\n' + + "recv-aft-filter: " + totalBytesReadAfterFilter + '\n' + + "send-bef-filter: " + totalBytesWrittenBeforeFilter + '\n' + + "read-ratio: " + readRatio + '\n' + + "write-ratio: " + writeRatio + '\n' + + "Events\n" + + "total-callback-requests: " + totalCallbackRequests + '\n' + + "handled-channel-selected-callbacks: " + handledChannelSelectedCallbacks + '\n' + + "rejected-channel-selected-callbacks: " + rejectedChannelSelectedCallbacks + '\n' + + "set-write-interest-after-callback: " + setWriteInterestAfterChannelSelectedCallback + '\n' + + "reactor-thread-already-racing: " + reactorThreadAlreadyRacing + '\n' + + "after-queue-modified-set-interest-ops: " + afterOutgoingElementsQueueModifiedSetInterestOps + '\n' + + "callback-preemt-because-bytes-read: " + callbackPreemtBecauseBytesRead + '\n' + + "callback-preemt-because-bytes-written: " + callbackPreemtBecauseBytesWritten + '\n' + + "ssl-engine-delegated-tasks: " + sslEngineDelegatedTasks + '\n' + + "max-pending-ssl-engine-delegated-tasks: " + maxPendingSslEngineDelegatedTasks + '\n' + ; + + return toStringCache; + } + } +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModuleDescriptor.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModuleDescriptor.java new file mode 100644 index 000000000..c48d8c546 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XmppTcpTransportModuleDescriptor.java @@ -0,0 +1,91 @@ +/** + * + * Copyright 2019-2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.tcp; + +import java.util.HashSet; +import java.util.Set; + +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration; +import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModuleDescriptor; +import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; +import org.jivesoftware.smack.fsm.StateDescriptor; +import org.jivesoftware.smack.tcp.XmppTcpTransportModule.EstablishTlsStateDescriptor; +import org.jivesoftware.smack.tcp.XmppTcpTransportModule.EstablishingTcpConnectionStateDescriptor; + +public class XmppTcpTransportModuleDescriptor extends ModularXmppClientToServerConnectionModuleDescriptor { + + private final boolean startTls; + private final boolean directTls; + + public XmppTcpTransportModuleDescriptor(Builder builder) { + startTls = builder.startTls; + directTls = builder.directTls; + } + + @Override + protected Set> getStateDescriptors() { + Set> res = new HashSet<>(); + res.add(EstablishingTcpConnectionStateDescriptor.class); + if (startTls) { + res.add(EstablishTlsStateDescriptor.class); + } + if (directTls) { + // TODO: Add direct TLS. + throw new IllegalArgumentException("DirectTLS is not implemented yet"); + } + return res; + } + + @Override + protected XmppTcpTransportModule constructXmppConnectionModule(ModularXmppClientToServerConnectionInternal connectionInternal) { + return new XmppTcpTransportModule(this, connectionInternal); + } + + public boolean isStartTlsEnabled() { + return startTls; + } + + public boolean isDirectTlsEnabled() { + return directTls; + } + + public static final class Builder extends ModularXmppClientToServerConnectionModuleDescriptor.Builder { + + private Builder(ModularXmppClientToServerConnectionConfiguration.Builder connectionConfigurationBuilder) { + super(connectionConfigurationBuilder); + } + + private boolean startTls = true; + + private boolean directTls = false; + + public Builder disableDirectTls() { + directTls = false; + return this; + } + + public Builder disableStartTls() { + startTls = false; + return this; + } + + @Override + protected XmppTcpTransportModuleDescriptor build() { + return new XmppTcpTransportModuleDescriptor(this); + } + } +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/IpTcpRemoteConnectionEndpoint.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/IpTcpRemoteConnectionEndpoint.java new file mode 100644 index 000000000..2dabe1950 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/IpTcpRemoteConnectionEndpoint.java @@ -0,0 +1,78 @@ +/** + * + * Copyright 2020 Florian Schmaus. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.tcp.rce; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; + +import org.jivesoftware.smack.datatypes.UInt16; +import org.jivesoftware.smack.util.rce.SingleAddressRemoteConnectionEndpoint; + +import org.minidns.record.A; +import org.minidns.record.AAAA; +import org.minidns.record.InternetAddressRR; + +public final class IpTcpRemoteConnectionEndpoint + implements Rfc6120TcpRemoteConnectionEndpoint, SingleAddressRemoteConnectionEndpoint { + + private final CharSequence host; + + private final UInt16 port; + + private final IARR internetAddressResourceRecord; + + public IpTcpRemoteConnectionEndpoint(CharSequence host, UInt16 port, IARR internetAddressResourceRecord) { + this.host = host; + this.port = port; + this.internetAddressResourceRecord = internetAddressResourceRecord; + } + + public static IpTcpRemoteConnectionEndpoint from(CharSequence host, int port, + InetAddress inetAddress) { + InternetAddressRR internetAddressResourceRecord; + // TODO: Use InternetAddressRR.from(InetAddress) once MiniDNS is updated. + if (inetAddress instanceof Inet4Address) { + internetAddressResourceRecord = new A((Inet4Address) inetAddress); + } else { + internetAddressResourceRecord = new AAAA((Inet6Address) inetAddress); + } + + return new IpTcpRemoteConnectionEndpoint(host, UInt16.from(port), + internetAddressResourceRecord); + } + + @Override + public CharSequence getHost() { + return host; + } + + @Override + public UInt16 getPort() { + return port; + } + + @Override + public InetAddress getInetAddress() { + return internetAddressResourceRecord.getInetAddress(); + } + + @Override + public String getDescription() { + return "RFC 6120 A/AAAA Endpoint + [" + host + ":" + port + "]"; + } +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/RemoteXmppTcpConnectionEndpoints.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/RemoteXmppTcpConnectionEndpoints.java new file mode 100644 index 000000000..9328d0fe7 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/RemoteXmppTcpConnectionEndpoints.java @@ -0,0 +1,232 @@ +/** + * + * Copyright 2015-2020 Florian Schmaus. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.tcp.rce; + +import java.net.InetAddress; +import java.util.ArrayList; +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.ConnectionConfiguration; +import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; +import org.jivesoftware.smack.util.DNSUtil; +import org.jivesoftware.smack.util.dns.DNSResolver; +import org.jivesoftware.smack.util.rce.RemoteConnectionEndpoint; +import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure; + +import org.minidns.dnsname.DnsName; +import org.minidns.record.InternetAddressRR; +import org.minidns.record.SRV; +import org.minidns.util.SrvUtil; + +public class RemoteXmppTcpConnectionEndpoints { + + private static final Logger LOGGER = Logger.getLogger(RemoteXmppTcpConnectionEndpoints.class.getName()); + + public static final String XMPP_CLIENT_DNS_SRV_PREFIX = "_xmpp-client._tcp"; + public static final String XMPP_SERVER_DNS_SRV_PREFIX = "_xmpp-server._tcp"; + + /** + * Lookups remote connection endpoints on the server for XMPP connections over TCP taking A, AAAA and SRV resource + * records into account. If no host address was configured and all lookups failed, for example with NX_DOMAIN, then + * result will be populated with the empty list. + * + * @param config the connection configuration to lookup the endpoints for. + * @return a lookup result. + */ + public static Result lookup(ConnectionConfiguration config) { + List discoveredRemoteConnectionEndpoints; + List lookupFailures; + + final InetAddress hostAddress = config.getHostAddress(); + final DnsName host = config.getHost(); + + if (hostAddress != null) { + lookupFailures = Collections.emptyList(); + + IpTcpRemoteConnectionEndpoint connectionEndpoint = IpTcpRemoteConnectionEndpoint.from( + hostAddress.toString(), config.getPort(), hostAddress); + discoveredRemoteConnectionEndpoints = Collections.singletonList(connectionEndpoint); + } else if (host != null) { + lookupFailures = new ArrayList<>(1); + + List hostAddresses = DNSUtil.getDNSResolver().lookupHostAddress(host, + lookupFailures, config.getDnssecMode()); + + if (hostAddresses != null) { + discoveredRemoteConnectionEndpoints = new ArrayList<>(hostAddresses.size()); + int port = config.getPort(); + for (InetAddress inetAddress : hostAddresses) { + IpTcpRemoteConnectionEndpoint connectionEndpoint = IpTcpRemoteConnectionEndpoint.from( + host, port, inetAddress); + discoveredRemoteConnectionEndpoints.add(connectionEndpoint); + } + } else { + discoveredRemoteConnectionEndpoints = Collections.emptyList(); + } + } else { + lookupFailures = new ArrayList<>(); + + // N.B.: Important to use config.serviceName and not AbstractXMPPConnection.serviceName + DnsName dnsName = config.getXmppServiceDomainAsDnsNameIfPossible(); + if (dnsName == null) { + // TODO: ConnectionConfiguration should check on construction time that either the given XMPP service + // name is also a valid DNS name, or that a host is explicitly configured. + throw new IllegalStateException(); + } + discoveredRemoteConnectionEndpoints = resolveXmppServiceDomain(dnsName, lookupFailures, config.getDnssecMode()); + } + + // Either the populated host addresses are not empty *or* there must be at least one failed address. + assert !discoveredRemoteConnectionEndpoints.isEmpty() || !lookupFailures.isEmpty(); + + return new Result<>(discoveredRemoteConnectionEndpoints, lookupFailures); + } + + public static final class Result { + public final List discoveredRemoteConnectionEndpoints; + public final List lookupFailures; + + private Result(List discoveredRemoteConnectionEndpoints, List lookupFailures) { + this.discoveredRemoteConnectionEndpoints = discoveredRemoteConnectionEndpoints; + this.lookupFailures = lookupFailures; + } + } + + @SuppressWarnings("ImmutableEnumChecker") + enum DomainType { + server(XMPP_SERVER_DNS_SRV_PREFIX), + client(XMPP_CLIENT_DNS_SRV_PREFIX), + ; + public final DnsName srvPrefix; + + DomainType(String srvPrefixString) { + srvPrefix = DnsName.from(srvPrefixString); + } + } + + /** + * Returns a list of HostAddresses under which the specified XMPP server can be reached at for client-to-server + * communication. A DNS lookup for a SRV record in the form "_xmpp-client._tcp.example.com" is attempted, according + * to section 3.2.1 of RFC 6120. If that lookup fails, it's assumed that the XMPP server lives at the host resolved + * by a DNS lookup at the specified domain on the default port of 5222. + *

+ * As an example, a lookup for "example.com" may return "im.example.com:5269". + *

+ * + * @param domain the domain. + * @param lookupFailures on optional list that will be populated with host addresses that failed to resolve. + * @param dnssecMode DNSSec mode. + * @return List of HostAddress, which encompasses the hostname and port that the + * XMPP server can be reached at for the specified domain. + */ + public static List resolveXmppServiceDomain(DnsName domain, + List lookupFailures, DnssecMode dnssecMode) { + DNSResolver dnsResolver = getDnsResolverOrThrow(); + return resolveDomain(domain, DomainType.client, lookupFailures, dnssecMode, dnsResolver); + } + + /** + * Returns a list of HostAddresses under which the specified XMPP server can be reached at for server-to-server + * communication. A DNS lookup for a SRV record in the form "_xmpp-server._tcp.example.com" is attempted, according + * to section 3.2.1 of RFC 6120. If that lookup fails , it's assumed that the XMPP server lives at the host resolved + * by a DNS lookup at the specified domain on the default port of 5269. + *

+ * As an example, a lookup for "example.com" may return "im.example.com:5269". + *

+ * + * @param domain the domain. + * @param lookupFailures a list that will be populated with host addresses that failed to resolve. + * @param dnssecMode DNSSec mode. + * @return List of HostAddress, which encompasses the hostname and port that the + * XMPP server can be reached at for the specified domain. + */ + public static List resolveXmppServerDomain(DnsName domain, + List lookupFailures, DnssecMode dnssecMode) { + DNSResolver dnsResolver = getDnsResolverOrThrow(); + return resolveDomain(domain, DomainType.server, lookupFailures, dnssecMode, dnsResolver); + } + + /** + * + * @param domain the domain. + * @param domainType the XMPP domain type, server or client. + * @param failedAddresses a list that will be populated with host addresses that failed to resolve. + * @return a list of resolver host addresses for this domain. + */ + private static List resolveDomain(DnsName domain, DomainType domainType, + List lookupFailures, DnssecMode dnssecMode, DNSResolver dnsResolver) { + List endpoints = new ArrayList<>(); + + // Step one: Do SRV lookups + DnsName srvDomain = DnsName.from(domainType.srvPrefix, domain); + + Collection srvRecords = dnsResolver.lookupSrvRecords(srvDomain, lookupFailures, dnssecMode); + if (srvRecords != null && !srvRecords.isEmpty()) { + if (LOGGER.isLoggable(Level.FINE)) { + String logMessage = "Resolved SRV RR for " + srvDomain + ":"; + for (SRV r : srvRecords) + logMessage += " " + r; + LOGGER.fine(logMessage); + } + + List sortedSrvRecords = SrvUtil.sortSrvRecords(srvRecords); + + for (SRV srv : sortedSrvRecords) { + List targetInetAddresses = dnsResolver.lookupHostAddress(srv.target, lookupFailures, dnssecMode); + SrvXmppRemoteConnectionEndpoint endpoint = new SrvXmppRemoteConnectionEndpoint(srv, targetInetAddresses); + endpoints.add(endpoint); + } + } else { + LOGGER.info("Could not resolve DNS SRV resource records for " + srvDomain + ". Consider adding those."); + } + + int defaultPort; + switch (domainType) { + case client: + defaultPort = 5222; + break; + case server: + defaultPort = 5269; + break; + default: + throw new AssertionError(); + } + + // Step two: Add the hostname to the end of the list + List hostAddresses = dnsResolver.lookupHostAddress(domain, lookupFailures, dnssecMode); + if (hostAddresses != null) { + for (InetAddress inetAddress : hostAddresses) { + IpTcpRemoteConnectionEndpoint endpoint = IpTcpRemoteConnectionEndpoint.from(domain, defaultPort, inetAddress); + endpoints.add(endpoint); + } + } + + return endpoints; + } + + private static DNSResolver getDnsResolverOrThrow() { + final DNSResolver dnsResolver = DNSUtil.getDNSResolver(); + if (dnsResolver == null) { + throw new IllegalStateException("No DNS resolver configured in Smack"); + } + return dnsResolver; + } +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/Rfc6120TcpRemoteConnectionEndpoint.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/Rfc6120TcpRemoteConnectionEndpoint.java new file mode 100644 index 000000000..0ec434f8f --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/Rfc6120TcpRemoteConnectionEndpoint.java @@ -0,0 +1,23 @@ +/** + * + * Copyright 2020 Florian Schmaus. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.tcp.rce; + +import org.jivesoftware.smack.util.rce.RemoteConnectionEndpoint; + +public interface Rfc6120TcpRemoteConnectionEndpoint extends RemoteConnectionEndpoint { + +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/SrvRemoteConnectionEndpoint.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/SrvRemoteConnectionEndpoint.java new file mode 100644 index 000000000..980a2396e --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/SrvRemoteConnectionEndpoint.java @@ -0,0 +1,57 @@ +/** + * + * Copyright 2020 Florian Schmaus. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.tcp.rce; + +import java.net.InetAddress; +import java.util.Collection; +import java.util.List; + +import org.jivesoftware.smack.datatypes.UInt16; +import org.jivesoftware.smack.util.rce.RemoteConnectionEndpoint; + +import org.minidns.record.SRV; + +public abstract class SrvRemoteConnectionEndpoint implements RemoteConnectionEndpoint { + + protected final SRV srv; + + protected final UInt16 port; + + private final List inetAddresses; + + protected SrvRemoteConnectionEndpoint(SRV srv, List inetAddresses) { + this.srv = srv; + this.port = UInt16.from(srv.port); + this.inetAddresses = inetAddresses; + } + + @Override + public final CharSequence getHost() { + return srv.target; + } + + @Override + public final UInt16 getPort() { + return port; + } + + @Override + public final Collection getInetAddresses() { + return inetAddresses; + } + +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/SrvXmppRemoteConnectionEndpoint.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/SrvXmppRemoteConnectionEndpoint.java new file mode 100644 index 000000000..23d4e94a5 --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/SrvXmppRemoteConnectionEndpoint.java @@ -0,0 +1,35 @@ +/** + * + * Copyright 2020 Florian Schmaus. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.tcp.rce; + +import java.net.InetAddress; +import java.util.List; + +import org.minidns.record.SRV; + +public final class SrvXmppRemoteConnectionEndpoint extends SrvRemoteConnectionEndpoint + implements Rfc6120TcpRemoteConnectionEndpoint { + + protected SrvXmppRemoteConnectionEndpoint(SRV srv, List inetAddresses) { + super(srv, inetAddresses); + } + + @Override + public String getDescription() { + return "RFC 6120 SRV Endpoint + ['xmpp', " + srv + "]"; + } +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/SrvXmppsRemoteConnectionEndpoint.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/SrvXmppsRemoteConnectionEndpoint.java new file mode 100644 index 000000000..b14becc8a --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/SrvXmppsRemoteConnectionEndpoint.java @@ -0,0 +1,34 @@ +/** + * + * Copyright 2020 Florian Schmaus. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.tcp.rce; + +import java.net.InetAddress; +import java.util.List; + +import org.minidns.record.SRV; + +public class SrvXmppsRemoteConnectionEndpoint extends SrvRemoteConnectionEndpoint { + + protected SrvXmppsRemoteConnectionEndpoint(SRV srv, List inetAddresses) { + super(srv, inetAddresses); + } + + @Override + public String getDescription() { + return "XEP-0368 SRV Endpoint + ['xmpps', " + srv + "]"; + } +} diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/package-info.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/package-info.java new file mode 100644 index 000000000..06c0e12ca --- /dev/null +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/rce/package-info.java @@ -0,0 +1,21 @@ +/** + * + * Copyright 2020 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Smack's internal API for XMPP connections over TCP. + */ +package org.jivesoftware.smack.tcp.rce; diff --git a/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/XmppNioTcpConnectionTest.java b/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/XmppNioTcpConnectionTest.java deleted file mode 100644 index 16c9989c5..000000000 --- a/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/XmppNioTcpConnectionTest.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * - * Copyright 2018 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.tcp; - -import org.jivesoftware.smack.fsm.StateDescriptor; -import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex; -import org.jivesoftware.smack.tcp.XmppNioTcpConnection.InstantShutdownStateDescriptor; - -public class XmppNioTcpConnectionTest { - - public void graphComplete() { - assertContains(XmppNioTcpConnection.INITIAL_STATE_DESCRIPTOR_VERTEX, InstantShutdownStateDescriptor.class); - } - - private static void assertContains(GraphVertex graph, Class state) { - // TODO: Implement this. - throw new Error("Implement me: " + graph + " " + state); - } -} diff --git a/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/rce/RemoteXmppTcpConnectionEndpointsTest.java b/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/rce/RemoteXmppTcpConnectionEndpointsTest.java new file mode 100644 index 000000000..6aa7cc38d --- /dev/null +++ b/smack-tcp/src/test/java/org/jivesoftware/smack/tcp/rce/RemoteXmppTcpConnectionEndpointsTest.java @@ -0,0 +1,87 @@ +/** + * + * Copyright 2018-2020 Florian Schmaus. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.tcp.rce; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.datatypes.UInt16; +import org.jivesoftware.smack.tcp.rce.RemoteXmppTcpConnectionEndpoints.DomainType; +import org.jivesoftware.smack.util.rce.RemoteConnectionEndpoint; +import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure; +import org.jivesoftware.smack.util.rce.RemoteConnectionException; + +import org.junit.jupiter.api.Test; +import org.minidns.record.A; + +public class RemoteXmppTcpConnectionEndpointsTest { + + @Test + public void simpleDomainTypeTest() { + DomainType client = DomainType.client; + assertEquals(RemoteXmppTcpConnectionEndpoints.XMPP_CLIENT_DNS_SRV_PREFIX, client.srvPrefix.ace); + + DomainType server = DomainType.server; + assertEquals(RemoteXmppTcpConnectionEndpoints.XMPP_SERVER_DNS_SRV_PREFIX, server.srvPrefix.ace); + } + + @Test + public void testConnectionException() { + List> connectionExceptions = new ArrayList<>(); + + { + A aRr = new A("1.2.3.4"); + UInt16 port = UInt16.from(1234); + String host = "example.org"; + IpTcpRemoteConnectionEndpoint remoteConnectionEndpoint = new IpTcpRemoteConnectionEndpoint<>(host, port, + aRr); + Exception exception = new Exception("Failed for some reason"); + + RemoteConnectionException> remoteConnectionException = RemoteConnectionException.from( + remoteConnectionEndpoint, exception); + connectionExceptions.add(remoteConnectionException); + } + + { + A aRr = new A("1.3.3.7"); + UInt16 port = UInt16.from(5678); + String host = "other.example.org"; + IpTcpRemoteConnectionEndpoint remoteConnectionEndpoint = new IpTcpRemoteConnectionEndpoint<>(host, port, + aRr); + Exception exception = new Exception("Failed for some other reason"); + + RemoteConnectionException> remoteConnectionException = RemoteConnectionException.from( + remoteConnectionEndpoint, exception); + connectionExceptions.add(remoteConnectionException); + } + + List lookupFailures = Collections.emptyList(); + SmackException.EndpointConnectionException endpointConnectionException = SmackException.EndpointConnectionException.from( + lookupFailures, connectionExceptions); + + String message = endpointConnectionException.getMessage(); + assertEquals("The following addresses failed: " + + "'RFC 6120 A/AAAA Endpoint + [example.org:1234] (/1.2.3.4:1234)' failed because: java.lang.Exception: Failed for some reason, " + + "'RFC 6120 A/AAAA Endpoint + [other.example.org:5678] (/1.3.3.7:5678)' failed because: java.lang.Exception: Failed for some other reason", + message); + } + +}