getRationales()
+ {
+ return rationales;
+ }
+
+ public boolean isInconclusive()
+ {
+ return connectionState == null;
+ }
+
+ /**
+ * Given the configuration of the initiating and receiving server, returns the expected outcome of an outbound
+ * server-to-server connection attempt.
+ *
+ * @param initiatingServer Configuration of the local server
+ * @param receivingServer Configuration of the remote server
+ * @return the expected outcome
+ */
+ public static ExpectedOutcome generateExpectedOutcome(final ServerSettings initiatingServer, final ServerSettings receivingServer) {
+ final ExpectedOutcome expectedOutcome = new ExpectedOutcome();
+
+ switch (initiatingServer.encryptionPolicy) {
+ case disabled: // <-- Initiating server's encryption policy.
+ switch (receivingServer.encryptionPolicy) {
+ case disabled: // Intended fall-through: if one peer disables TLS, it won't be used in any circumstances.
+ case optional:
+ // The certificate status of both peers is irrelevant, as TLS won't happen.
+ if (initiatingServer.dialbackSupported && receivingServer.dialbackSupported) {
+ expectedOutcome.set(NON_ENCRYPTED_WITH_DIALBACK_AUTH, "although TLS is not available (so it cannot be used for encryption or authentication), Dialback is available, which allows the Initiating Entity to be authenticated by the Receiving Entity.");
+ } else {
+ expectedOutcome.set(NO_CONNECTION, "TLS and Dialback aren't available, making it impossible for the Initiating Entity to be authenticated by the Receiving Entity.");
+ }
+ break;
+ case required:
+ expectedOutcome.set(NO_CONNECTION, "one peer requires encryption while the other disables encryption. This cannot work.");
+ break;
+ }
+ break;
+
+ case optional: // <-- Initiating server's encryption policy.
+ switch (receivingServer.encryptionPolicy) {
+ case disabled:
+ // The certificate status of both peers is irrelevant, as TLS won't happen.
+ if (initiatingServer.dialbackSupported && receivingServer.dialbackSupported) {
+ expectedOutcome.set(NON_ENCRYPTED_WITH_DIALBACK_AUTH, "TLS is not available, so it cannot be used for encryption or authentication. Dialback is available, which allows the Initiating Entity to be authenticated by the Receiving Entity.");
+ } else {
+ expectedOutcome.set(NO_CONNECTION, "TLS and Dialback aren't available, making it impossible for the Initiating Entity to be authenticated by the Receiving Entity.");
+ }
+ break;
+ case optional:
+ switch (receivingServer.certificateState) {
+ case MISSING:
+ if (initiatingServer.dialbackSupported && receivingServer.dialbackSupported) {
+ // TODO: should we take into account a manual configuration of an ANON cypher suite, so that encryption-without-authentication can occur via TLS, followed by a Dialback-based authentication?
+ expectedOutcome.set(NON_ENCRYPTED_WITH_DIALBACK_AUTH, "Receiving Entity does not provides a TLS certificate. As ANON cypher suites are expected to be unavailable, Initiating Entity cannot negotiate TLS, so that cannot be used for encryption or authentication. Dialback is available, so authentication can occur.");
+ } else {
+ expectedOutcome.set(NO_CONNECTION, "Receiving Entity does not provides a TLS certificate, Initiating Entity cannot negotiate TLS. With TLS and Dialback unavailable, authentication cannot occur (even if usage of an ANON cypher suite would make TLS-for-encryption possible)");
+ }
+ break;
+ case INVALID:
+ if (initiatingServer.strictCertificateValidation) {
+ expectedOutcome.set(NO_CONNECTION, "Receiving Entity provides an invalid TLS certificate, which should cause Initiating Entity to abort TLS (as per RFC 6120 section 13.7.2).");
+ } else if (initiatingServer.dialbackSupported && receivingServer.dialbackSupported) {
+ expectedOutcome.set(ENCRYPTED_WITH_DIALBACK_AUTH, "Initiating Entity may choose to ignore Receiving Entities invalid certificate (for encryption purposes only) and choose to authenticate with Server Dialback (per RFC 7590 Section 3.4)");
+ } else {
+ expectedOutcome.set(NO_CONNECTION, "Receiving Entity provides an invalid TLS certificate. As Server Dialback is not available, authentication cannot occur.");
+ }
+ break;
+ case VALID:
+ switch (initiatingServer.certificateState) {
+ case MISSING:
+ if (initiatingServer.dialbackSupported && receivingServer.dialbackSupported) {
+ expectedOutcome.set(ENCRYPTED_WITH_DIALBACK_AUTH, "Receiving Entity provides a valid TLS certificate allowing Initiating Entity to be able to establish TLS for encryption. Initiating Entity but does not provide a certificate itself, so SASL EXTERNAL is not available for Receiving Entity to authenticate Initiating Entity, but Dialback is available for that purpose.");
+ } else {
+ expectedOutcome.set(NO_CONNECTION, "Receiving Entity provides a valid TLS certificate allowing Initiating Entity to be able to establish TLS for encryption. Initiating Entity but does not provide a certificate itself, so SASL EXTERNAL is not available for Receiving Entity to authenticate Initiating Entity. As Dialback is also not available, authentication cannot occur.");
+ }
+ break;
+ case INVALID:
+ if (receivingServer.strictCertificateValidation) {
+ expectedOutcome.set(NO_CONNECTION, "Initiating Entity provides an invalid TLS certificate, which should cause Receiving Entity to abort TLS (as per RFC 6120 section 13.7.2).");
+ } else if (initiatingServer.dialbackSupported && receivingServer.dialbackSupported) {
+ expectedOutcome.set(ENCRYPTED_WITH_DIALBACK_AUTH, "Receiving Entity may choose to ignore Initiating Entities invalid certificate (for encryption purposes only) and choose to authenticate with Server Dialback (per RFC 7590 Section 3.4)");
+ } else {
+ expectedOutcome.set(NO_CONNECTION, "Initiating Entity provides an invalid TLS certificate. As Server Dialback is not available, authentication cannot occur.");
+ }
+ break;
+ case VALID:
+ // valid certs all around. Dialback is not needed.
+ expectedOutcome.set(ENCRYPTED_WITH_SASLEXTERNAL_AUTH, "TLS is usable for both encryption and authentication.");
+ break;
+ }
+ break;
+ }
+ break;
+ case required:
+ switch (receivingServer.certificateState) {
+ case MISSING:
+ expectedOutcome.set(NO_CONNECTION, "Receiving Entity requires encryption, but it does not provide a TLS certificate. As ANON cypher suites are expected to be unavailable, the Initiating Entity cannot negotiate TLS and therefor the required encrypted connection cannot be established.");
+ // TODO: should we take into account a manual configuration of an ANON cypher suite, so that encryption-without-authentication can occur via TLS, followed by a Dialback-based authentication?
+ break;
+ case INVALID:
+ if (initiatingServer.strictCertificateValidation) {
+ expectedOutcome.set(NO_CONNECTION, "Receiving Entity provides an invalid TLS certificate, which should cause Initiating Entity to abort TLS (as per RFC 6120 section 13.7.2).");
+ } else if (initiatingServer.dialbackSupported && receivingServer.dialbackSupported) {
+ expectedOutcome.set(ENCRYPTED_WITH_DIALBACK_AUTH, "Initiating Entity may choose to ignore Receiving Entities invalid certificate (for encryption purposes only) and choose to authenticate with Server Dialback (per RFC 7590 Section 3.4)");
+ } else {
+ expectedOutcome.set(NO_CONNECTION, "Receiving Entity provides an invalid TLS certificate. As Server Dialback is not available, authentication cannot occur.");
+ }
+ break;
+ case VALID:
+ switch (initiatingServer.certificateState) {
+ case MISSING:
+ if (initiatingServer.dialbackSupported && receivingServer.dialbackSupported) {
+ expectedOutcome.set(ENCRYPTED_WITH_DIALBACK_AUTH, "Receiving Entity provides a valid TLS certificate allowing Initiating Entity to be able to establish TLS for encryption. Initiating Entity but does not provide a certificate itself, so SASL EXTERNAL is not available for Receiving Entity to authenticate Initiating Entity, but Dialback is available for that purpose.");
+ } else {
+ expectedOutcome.set(NO_CONNECTION, "Receiving Entity provides a valid TLS certificate allowing Initiating Entity to be able to establish TLS for encryption. Initiating Entity but does not provide a certificate itself, so SASL EXTERNAL is not available for Receiving Entity to authenticate Initiating Entity. As Dialback is also not available, authentication cannot occur.");
+ }
+ break;
+ case INVALID:
+ if (receivingServer.strictCertificateValidation) {
+ expectedOutcome.set(NO_CONNECTION, "Initiating Entity provides an invalid TLS certificate, which should cause Receiving Entity to abort TLS (as per RFC 6120 section 13.7.2).");
+ } else if (initiatingServer.dialbackSupported && receivingServer.dialbackSupported) {
+ expectedOutcome.set(ENCRYPTED_WITH_DIALBACK_AUTH, "Receiving Entity may choose to ignore Initiating Entities invalid certificate (for encryption purposes only) and choose to authenticate with Server Dialback (per RFC 7590 Section 3.4)");
+ } else {
+ expectedOutcome.set(NO_CONNECTION, "Initiating Entity provides an invalid TLS certificate. As Server Dialback is not available, authentication cannot occur.");
+ }
+ break;
+ case VALID:
+ // valid certs all around. Dialback is not needed.
+ expectedOutcome.set(ENCRYPTED_WITH_SASLEXTERNAL_AUTH, "TLS is usable for both encryption and authentication.");
+ break;
+ }
+ break;
+ }
+ break;
+ }
+ break;
+
+ case required: // <-- Initiating server's encryption policy.
+ switch (receivingServer.encryptionPolicy) {
+ case disabled:
+ expectedOutcome.set(NO_CONNECTION, "one peer requires encryption, the other disables encryption. This cannot work.");
+ break;
+ case optional:
+ switch (receivingServer.certificateState) {
+ case MISSING:
+ expectedOutcome.set(NO_CONNECTION, "Receiving Entity does not provide a TLS certificate. As ANON cypher suites are expected to be unavailable, the Initiating Entity cannot negotiate TLS and therefor the required encrypted connection cannot be established.");
+ // TODO: should we take into account a manual configuration of an ANON cypher suite, so that encryption-without-authentication can occur via TLS, followed by a Dialback-based authentication?
+ break;
+ case INVALID:
+ if (initiatingServer.strictCertificateValidation) {
+ expectedOutcome.set(NO_CONNECTION, "Receiving Entity provides an invalid TLS certificate, which should cause Initiating Entity to abort TLS (as per RFC 6120 section 13.7.2).");
+ } else if (initiatingServer.dialbackSupported && receivingServer.dialbackSupported) {
+ expectedOutcome.set(ENCRYPTED_WITH_DIALBACK_AUTH, "Initiating Entity may choose to ignore Receiving Entities invalid certificate (for encryption purposes only) and choose to authenticate with Server Dialback (per RFC 7590 Section 3.4)");
+ } else {
+ expectedOutcome.set(NO_CONNECTION, "Receiving Entity provides an invalid TLS certificate. As Server Dialback is not available, authentication cannot occur.");
+ }
+ break;
+ case VALID:
+ switch (initiatingServer.certificateState) {
+ case MISSING:
+ if (initiatingServer.dialbackSupported && receivingServer.dialbackSupported) {
+ expectedOutcome.set(ENCRYPTED_WITH_DIALBACK_AUTH, "Receiving Entity provides a valid TLS certificate allowing Initiating Entity to be able to establish TLS for encryption. Initiating Entity but does not provide a certificate itself, so SASL EXTERNAL is not available for Receiving Entity to authenticate Initiating Entity, but Dialback is available for that purpose.");
+ } else {
+ expectedOutcome.set(NO_CONNECTION, "Receiving Entity provides a valid TLS certificate allowing Initiating Entity to be able to establish TLS for encryption. Initiating Entity but does not provide a certificate itself, so SASL EXTERNAL is not available for Receiving Entity to authenticate Initiating Entity. As Dialback is also not available, authentication cannot occur.");
+ }
+ break;
+ case INVALID:
+ if (receivingServer.strictCertificateValidation) {
+ expectedOutcome.set(NO_CONNECTION, "Initiating Entity provides an invalid TLS certificate, which should cause Receiving Entity to abort TLS (as per RFC 6120 section 13.7.2).");
+ } else if (initiatingServer.dialbackSupported && receivingServer.dialbackSupported) {
+ expectedOutcome.set(ENCRYPTED_WITH_DIALBACK_AUTH, "Receiving Entity may choose to ignore Initiating Entities invalid certificate (for encryption purposes only) and choose to authenticate with Server Dialback (per RFC 7590 Section 3.4)");
+ } else {
+ expectedOutcome.set(NO_CONNECTION, "Initiating Entity provides an invalid TLS certificate. As Server Dialback is not available, authentication cannot occur.");
+ }
+ break;
+ case VALID:
+ // valid certs all around. Dialback is not needed.
+ expectedOutcome.set(ENCRYPTED_WITH_SASLEXTERNAL_AUTH, "TLS is usable for both encryption and authentication.");
+ break;
+ }
+ break;
+ }
+ break;
+ case required:
+ switch (receivingServer.certificateState) {
+ case MISSING:
+ expectedOutcome.set(NO_CONNECTION, "Receiving Entity does not provide a TLS certificate. As ANON cypher suites are expected to be unavailable, the Initiating Entity cannot negotiate TLS and therefor the required encrypted connection cannot be established.");
+ // TODO: should we take into account a manual configuration of an ANON cypher suite, so that encryption-without-authentication can occur via TLS, followed by a Dialback-based authentication?
+ break;
+ case INVALID:
+ if (initiatingServer.strictCertificateValidation) {
+ expectedOutcome.set(NO_CONNECTION, "Receiving Entity provides an invalid TLS certificate, which should cause Initiating Entity to abort TLS (as per RFC 6120 section 13.7.2).");
+ } else if (initiatingServer.dialbackSupported && receivingServer.dialbackSupported) {
+ expectedOutcome.set(ENCRYPTED_WITH_DIALBACK_AUTH, "Initiating Entity may choose to ignore Receiving Entities invalid certificate (for encryption purposes only) and choose to authenticate with Server Dialback (per RFC 7590 Section 3.4)");
+ } else {
+ expectedOutcome.set(NO_CONNECTION, "Receiving Entity provides an invalid TLS certificate. As Server Dialback is not available, authentication cannot occur.");
+ }
+ break;
+ case VALID:
+ switch (initiatingServer.certificateState) {
+ case MISSING:
+ if (initiatingServer.dialbackSupported && receivingServer.dialbackSupported) {
+ expectedOutcome.set(ENCRYPTED_WITH_DIALBACK_AUTH, "Initiating Entity can negotiate encryption, but does not provide a certificate. SASL EXTERNAL cannot be used, but Dialback is available, so authentication can occur.");
+ } else {
+ expectedOutcome.set(NO_CONNECTION, "Initiating Entity can negotiate encryption, but does not provide a certificate. As Dialback is not available, authentication cannot occur. Connection cannot be established.");
+ }
+ break;
+ case INVALID:
+ if (receivingServer.strictCertificateValidation) {
+ expectedOutcome.set(NO_CONNECTION, "Initiating Entity provides an invalid TLS certificate, which should cause Receiving Entity to abort TLS (as per RFC 6120 section 13.7.2).");
+ } else if (initiatingServer.dialbackSupported && receivingServer.dialbackSupported) {
+ expectedOutcome.set(ENCRYPTED_WITH_DIALBACK_AUTH, "Receiving Entity may choose to ignore Initiating Entities invalid certificate (for encryption purposes only) and choose to authenticate with Server Dialback (per RFC 7590 Section 3.4)");
+ } else {
+ expectedOutcome.set(NO_CONNECTION, "Initiating Entity provides an invalid TLS certificate. As Server Dialback is not available, authentication cannot occur.");
+ }
+ break;
+ case VALID:
+ expectedOutcome.set(ENCRYPTED_WITH_SASLEXTERNAL_AUTH, "Initiating Entity can establish encryption and authenticate using TLS.");
+ break;
+ }
+ break;
+ }
+ break;
+ }
+ break;
+ }
+
+ // FIXME: add support for the DirectTLS TLS policy.
+ return expectedOutcome;
+ }
+}
diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/session/LocalIncomingServerSessionTest.java b/xmppserver/src/test/java/org/jivesoftware/openfire/session/LocalIncomingServerSessionTest.java
new file mode 100644
index 0000000000..31cdf2004c
--- /dev/null
+++ b/xmppserver/src/test/java/org/jivesoftware/openfire/session/LocalIncomingServerSessionTest.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2023 Ignite Realtime Foundation. All rights reserved.
+ *
+ * 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.openfire.session;
+
+import org.jivesoftware.Fixtures;
+import org.jivesoftware.openfire.Connection;
+import org.jivesoftware.openfire.ConnectionManager;
+import org.jivesoftware.openfire.SessionManager;
+import org.jivesoftware.openfire.XMPPServer;
+import org.jivesoftware.openfire.keystore.*;
+import org.jivesoftware.openfire.net.BlockingAcceptingMode;
+import org.jivesoftware.openfire.net.DNSUtil;
+import org.jivesoftware.openfire.net.SocketAcceptThread;
+import org.jivesoftware.openfire.net.SocketReader;
+import org.jivesoftware.openfire.spi.ConnectionConfiguration;
+import org.jivesoftware.openfire.spi.ConnectionListener;
+import org.jivesoftware.openfire.spi.ConnectionType;
+import org.jivesoftware.util.JiveGlobals;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.stubbing.Answer;
+
+import java.io.File;
+import java.security.cert.X509Certificate;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Unit tests that verify if an inbound server-to-server socket connection can be created (and where applicable:
+ * encrypted and authenticated), verifying the implementation of {@link LocalIncomingServerSession}
+ *
+ * These tests assume the following constants:
+ * - TLS certificate validation is implemented correctly.
+ * - The domain name in the certificate matches that of the server.
+ *
+ * This implementation uses instances of {@link RemoteInitiatingServerDummy} to represent the remote server that
+ * initiates a server-to-server connection.
+ *
+ * @author Guus der Kinderen, guus@goodbytes.nl
+ * @author Alex Gidman, alex.gidman@surevine.com
+ */
+@ExtendWith(MockitoExtension.class)
+public class LocalIncomingServerSessionTest
+{
+ private RemoteInitiatingServerDummy remoteInitiatingServerDummy;
+ private File tmpIdentityStoreFile;
+ private IdentityStore identityStore;
+ private File tmpTrustStoreFile;
+ private TrustStore trustStore;
+
+ /**
+ * Prepares the local server for operation. This mostly involves preparing the test fixture by mocking parts of the
+ * API that {@link LocalOutgoingServerSession#createOutgoingSession(DomainPair, int)} uses when establishing a
+ * connection.
+ */
+ @BeforeEach
+ public void setUpClass() throws Exception {
+ Fixtures.reconfigureOpenfireHome();
+ JiveGlobals.setProperty("xmpp.domain", Fixtures.XMPP_DOMAIN);
+ final XMPPServer xmppServer = Fixtures.mockXMPPServer();
+ XMPPServer.setInstance(xmppServer);
+
+ final File tmpDir = new File(System.getProperty("java.io.tmpdir"));
+
+ // Use a temporary file to hold the identity store that is used by the tests.
+ final CertificateStoreManager certificateStoreManager = mock(CertificateStoreManager.class, withSettings().lenient());
+ tmpIdentityStoreFile = new File(tmpDir, "unittest-identitystore-" + System.currentTimeMillis() + ".jks");
+ tmpIdentityStoreFile.deleteOnExit();
+ final CertificateStoreConfiguration identityStoreConfig = new CertificateStoreConfiguration("jks", tmpIdentityStoreFile, "secret".toCharArray(), tmpDir);
+ identityStore = new IdentityStore(identityStoreConfig, true);
+ doReturn(identityStore).when(certificateStoreManager).getIdentityStore(any());
+
+ // Use a temporary file to hold the trust store that is used by the tests.
+ tmpTrustStoreFile = new File(tmpDir, "unittest-truststore-" + System.currentTimeMillis() + ".jks");
+ tmpTrustStoreFile.deleteOnExit();
+ final CertificateStoreConfiguration trustStoreConfig = new CertificateStoreConfiguration("jks", tmpTrustStoreFile, "secret".toCharArray(), tmpDir);
+ trustStore = new TrustStore(trustStoreConfig, true);
+ doReturn(trustStore).when(certificateStoreManager).getTrustStore(any());
+
+ // Mock the connection configuration.
+ doReturn(certificateStoreManager).when(xmppServer).getCertificateStoreManager();
+
+ final SessionManager sessionManager = new SessionManager(); // This is the system under test. We do not want to use a mock for this test!
+ sessionManager.initialize(xmppServer);
+ doReturn(sessionManager).when(xmppServer).getSessionManager();
+
+ final ConnectionManager connectionManager = Fixtures.mockConnectionManager();
+ final ConnectionListener connectionListener = Fixtures.mockConnectionListener();
+ doAnswer(new ConnectionConfigurationAnswer(identityStoreConfig, trustStoreConfig)).when(connectionListener).generateConnectionConfiguration();
+ doReturn(Set.of(connectionListener)).when(connectionManager).getListeners(any(ConnectionType.class));
+ doReturn(connectionListener).when(connectionManager).getListener(any(ConnectionType.class), anyBoolean());
+ doReturn(connectionManager).when(xmppServer).getConnectionManager();
+ setUp();
+ }
+
+ /**
+ * Dynamically generate a ConnectionConfiguration answer, as used by the Mock ConnectionListener.
+ *
+ * A dynamic answer is needed, as the value of the ConnectionSettings.Server.TLS_POLICY property needs to be
+ * evaluated at run-time (this value is changed in the setup of many of the unit tests in this file).
+ *
+ */
+ private static class ConnectionConfigurationAnswer implements Answer {
+
+ private CertificateStoreConfiguration identityStoreConfig;
+ private CertificateStoreConfiguration trustStoreConfig;
+
+ private ConnectionConfigurationAnswer(CertificateStoreConfiguration identityStoreConfig, CertificateStoreConfiguration trustStoreConfig)
+ {
+ this.identityStoreConfig = identityStoreConfig;
+ this.trustStoreConfig = trustStoreConfig;
+ }
+
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable
+ {
+ final Connection.TLSPolicy tlsPolicy = Connection.TLSPolicy.valueOf(JiveGlobals.getProperty(ConnectionSettings.Server.TLS_POLICY, Connection.TLSPolicy.optional.toString()));
+ final Set suites = Set.of("TLS_AES_256_GCM_SHA384","TLS_AES_128_GCM_SHA256","TLS_CHACHA20_POLY1305_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256","TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256","TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256","TLS_DHE_RSA_WITH_AES_256_GCM_SHA384","TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256","TLS_DHE_DSS_WITH_AES_256_GCM_SHA384","TLS_DHE_RSA_WITH_AES_128_GCM_SHA256","TLS_DHE_DSS_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256","TLS_DHE_RSA_WITH_AES_256_CBC_SHA256","TLS_DHE_DSS_WITH_AES_256_CBC_SHA256","TLS_DHE_RSA_WITH_AES_128_CBC_SHA256","TLS_DHE_DSS_WITH_AES_128_CBC_SHA256","TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384","TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384","TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256","TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA","TLS_DHE_RSA_WITH_AES_256_CBC_SHA","TLS_DHE_DSS_WITH_AES_256_CBC_SHA","TLS_DHE_RSA_WITH_AES_128_CBC_SHA","TLS_DHE_DSS_WITH_AES_128_CBC_SHA","TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA","TLS_ECDH_RSA_WITH_AES_256_CBC_SHA","TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA","TLS_ECDH_RSA_WITH_AES_128_CBC_SHA","TLS_RSA_WITH_AES_256_GCM_SHA384","TLS_RSA_WITH_AES_128_GCM_SHA256","TLS_RSA_WITH_AES_256_CBC_SHA256","TLS_RSA_WITH_AES_128_CBC_SHA256","TLS_RSA_WITH_AES_256_CBC_SHA","TLS_RSA_WITH_AES_128_CBC_SHA","TLS_EMPTY_RENEGOTIATION_INFO_SCSV");
+ final Set protocols = Set.of("TLSv1.2");
+ return new ConnectionConfiguration(ConnectionType.SOCKET_S2S, true, 10, -1, Connection.ClientAuth.wanted, null, 9999, tlsPolicy, identityStoreConfig, trustStoreConfig, true, true, protocols, suites, Connection.CompressionPolicy.optional, true );
+ }
+ }
+
+ public void setUp() throws Exception
+ {
+ remoteInitiatingServerDummy = new RemoteInitiatingServerDummy(Fixtures.XMPP_DOMAIN);
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception
+ {
+ tmpIdentityStoreFile.delete();
+ tmpTrustStoreFile.delete();
+ identityStore = null;
+ trustStore = null;
+ DNSUtil.setDnsOverride(null);
+
+ if (remoteInitiatingServerDummy != null) {
+ remoteInitiatingServerDummy.disconnect();
+ remoteInitiatingServerDummy = null;
+ }
+
+ Fixtures.clearExistingProperties();
+ }
+
+ /**
+ * Unit test in which Openfire reacts to an inbound server-to-server connection attempt.
+ *
+ * This test is parameterized, meaning that the configuration of both the local server and the remote mock server is
+ * passed as an argument to this method. These configurations are used to both initialize and execute the test, but
+ * also to calculate the expected outcome of the test, given the provided configuration. This expected outcome is
+ * asserted by the test implementation
+ *
+ * @param localServerSettings Server settings for the system under test (the 'local' server).
+ * @param remoteServerSettings Server settings for the mock server that is used as a peer in this test.
+ */
+ @ParameterizedTest
+ @MethodSource("arguments")
+ public void incomingTest(final ServerSettings localServerSettings, final ServerSettings remoteServerSettings)
+ throws Exception
+ {
+ final ExpectedOutcome expected = ExpectedOutcome.generateExpectedOutcome(remoteServerSettings, localServerSettings);
+ if (RemoteInitiatingServerDummy.doLog) System.out.println("Executing test:\n - Local Server, Recipient, System Under Test Settings: " + localServerSettings + "\n - Remote Server, Initiator, dummy/mock server Settings: " + remoteServerSettings + "\nExpected outcome: " + expected.getConnectionState());
+
+ JiveGlobals.setProperty("xmpp.domain", Fixtures.XMPP_DOMAIN);
+ final TrustStore trustStore = XMPPServer.getInstance().getCertificateStoreManager().getTrustStore(ConnectionType.SOCKET_S2S);
+ final IdentityStore identityStore = XMPPServer.getInstance().getCertificateStoreManager().getIdentityStore(ConnectionType.SOCKET_S2S);
+ try {
+ // Setup test fixture.
+
+ // Remote server TLS policy.
+ remoteInitiatingServerDummy.setEncryptionPolicy(remoteServerSettings.encryptionPolicy);
+
+ // Remote server dialback
+ remoteInitiatingServerDummy.setDisableDialback(!remoteServerSettings.dialbackSupported);
+
+ // Remote server certificate state
+ switch (remoteServerSettings.certificateState) {
+ case INVALID:
+ remoteInitiatingServerDummy.setUseExpiredEndEntityCertificate(true);
+ // Intended fall-through
+ case VALID:
+ remoteInitiatingServerDummy.preparePKIX();
+
+ // Install in local server's truststore.
+ final X509Certificate[] chain = remoteInitiatingServerDummy.getGeneratedPKIX().getCertificateChain();
+ final X509Certificate caCert = chain[chain.length-1];
+ trustStore.installCertificate("unit-test", KeystoreTestUtils.toPemFormat(caCert));
+ break;
+ case MISSING:
+ break;
+ default:
+ throw new IllegalStateException("Unsupported remote certificate state");
+ }
+
+ // Local server TLS policy.
+ JiveGlobals.setProperty(ConnectionSettings.Server.TLS_POLICY, localServerSettings.encryptionPolicy.toString());
+
+ // Local server dialback.
+ JiveGlobals.setProperty(ConnectionSettings.Server.DIALBACK_ENABLED, localServerSettings.dialbackSupported ? "true" : "false");
+
+ // Local server certificate state
+ switch (localServerSettings.certificateState) {
+ case MISSING:
+ // Do not install domain certificate.
+ break;
+ case INVALID:
+ // Insert an expired certificate into the identity store
+ identityStore.installCertificate(Fixtures.expiredX509Certificate, Fixtures.privateKeyForExpiredCert, "");
+ break;
+ case VALID:
+ // Generate a valid certificate and insert into identity store
+ identityStore.ensureDomainCertificate();
+ break;
+ }
+
+ remoteInitiatingServerDummy.init();
+ if (remoteInitiatingServerDummy.getDialbackAuthoritativeServerPort() > 0) {
+ DNSUtil.setDnsOverride(Map.of(RemoteInitiatingServerDummy.XMPP_DOMAIN, new DNSUtil.HostAddress("localhost", remoteInitiatingServerDummy.getDialbackAuthoritativeServerPort(), false)));
+ }
+
+ // execute system under test.
+ final SocketAcceptThread socketAcceptThread = new SocketAcceptThread(0, null, false);
+ socketAcceptThread.setDaemon(true);
+ socketAcceptThread.setPriority(Thread.MAX_PRIORITY);
+ socketAcceptThread.start();
+
+ // now, make the remote server connect.
+ remoteInitiatingServerDummy.connect(socketAcceptThread.getPort());
+ remoteInitiatingServerDummy.blockUntilDone(1, TimeUnit.MINUTES);
+
+ // get the incoming server session object.
+ final LocalIncomingServerSession result;
+ BlockingAcceptingMode mode = ((BlockingAcceptingMode) socketAcceptThread.getAcceptingMode());
+ final SocketReader socketReader = mode == null ? null : mode.getLastReader();
+ result = socketReader == null ? null : (LocalIncomingServerSession) socketReader.getSession();
+
+ // Verify results
+ if (RemoteInitiatingServerDummy.doLog) System.out.println("Expect: " + expected.getConnectionState() + ", Result: " + result);
+ switch (expected.getConnectionState())
+ {
+ case NO_CONNECTION:
+ if (result == null) {
+ assertNull(result); // Yes, this is silly.
+ } else {
+ assertFalse(result.isAuthenticated());
+ }
+ break;
+ case NON_ENCRYPTED_WITH_DIALBACK_AUTH:
+ assertNotNull(result);
+ assertFalse(result.isClosed());
+ assertFalse(result.isEncrypted());
+ assertTrue(result.isAuthenticated());
+ assertEquals(ServerSession.AuthenticationMethod.DIALBACK, result.getAuthenticationMethod());
+ break;
+ case ENCRYPTED_WITH_DIALBACK_AUTH:
+ assertNotNull(result);
+ assertFalse(result.isClosed());
+ assertTrue(result.isEncrypted());
+ assertTrue(result.isAuthenticated());
+ assertEquals(ServerSession.AuthenticationMethod.DIALBACK, result.getAuthenticationMethod());
+ break;
+ case ENCRYPTED_WITH_SASLEXTERNAL_AUTH:
+ assertNotNull(result);
+ assertFalse(result.isClosed());
+ assertTrue(result.isEncrypted());
+ assertTrue(result.isAuthenticated());
+ assertEquals(ServerSession.AuthenticationMethod.SASL_EXTERNAL, result.getAuthenticationMethod());
+ break;
+ }
+ if (RemoteInitiatingServerDummy.doLog) System.out.println("Expectation met.");
+ } finally {
+ // Teardown test fixture.
+ trustStore.delete("unit-test");
+ }
+ }
+
+ /**
+ * Provides the arguments for the method that implements the unit test.
+ * @return Unit test arguments
+ */
+ private static Iterable arguments() {
+ final Collection result = new LinkedList<>();
+
+ final Set localServerSettings = new LinkedHashSet<>();
+ final Set remoteServerSettings = new LinkedHashSet<>();
+
+ for (final ServerSettings.CertificateState certificateState : ServerSettings.CertificateState.values()) {
+ for (final boolean dialbackSupported : List.of(true, false)) {
+ for (final Connection.TLSPolicy tlsPolicy : Connection.TLSPolicy.values()) {
+ if (tlsPolicy == Connection.TLSPolicy.legacyMode) {
+ continue; // TODO add support for DirectTLS in this unit test!
+ }
+ final ServerSettings serverSettings = new ServerSettings(tlsPolicy, certificateState,true, dialbackSupported); // TODO add support for both strict certificate validation settings.
+ localServerSettings.add(serverSettings);
+ remoteServerSettings.add(serverSettings);
+ }
+ }
+ }
+
+ for (final ServerSettings local : localServerSettings) {
+ for (final ServerSettings remote : remoteServerSettings) {
+ result.add(Arguments.arguments(local, remote));
+ }
+ }
+
+ // Not all test-runners easily identify the parameters that are used to run each test iteration. Those that do
+ // not, typically show a number. By outputting the numbered arguments, they can be cross-referenced with any
+ // failed test case.
+ int i = 1;
+ for (Arguments arguments : result) {
+ System.out.println("Test [" + i++ + "]: " + arguments.get()[0] + ", " + arguments.get()[1]);
+ }
+ return result;
+ }
+}
diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/session/LocalOutgoingServerSessionTest.java b/xmppserver/src/test/java/org/jivesoftware/openfire/session/LocalOutgoingServerSessionTest.java
new file mode 100644
index 0000000000..947ad9989c
--- /dev/null
+++ b/xmppserver/src/test/java/org/jivesoftware/openfire/session/LocalOutgoingServerSessionTest.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2023 Ignite Realtime Foundation. All rights reserved.
+ *
+ * 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.openfire.session;
+
+import org.jivesoftware.Fixtures;
+import org.jivesoftware.openfire.*;
+import org.jivesoftware.openfire.keystore.*;
+import org.jivesoftware.openfire.net.*;
+import org.jivesoftware.openfire.spi.ConnectionConfiguration;
+import org.jivesoftware.openfire.spi.ConnectionListener;
+import org.jivesoftware.openfire.spi.ConnectionType;
+import org.jivesoftware.util.JiveGlobals;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.stubbing.Answer;
+
+import java.io.File;
+import java.security.cert.X509Certificate;
+import java.util.*;
+
+import static org.jivesoftware.openfire.session.ExpectedOutcome.ConnectionState.NON_ENCRYPTED_WITH_DIALBACK_AUTH;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Unit tests that verify if an outbound server-to-server socket connection can be created (and where applicable:
+ * encrypted and authenticated), verifying the implementation of {@link LocalOutgoingServerSession#createOutgoingSession(DomainPair, int)}
+ *
+ * These tests assume the following constants:
+ * - TLS certificate validation is implemented correctly.
+ * - The domain name in the certificate matches that of the server.
+ *
+ * This implementation uses instances of {@link RemoteReceivingServerDummy} to represent the remote server to which a connection
+ * is being made.
+ *
+ * @author Guus der Kinderen, guus@goodbytes.nl
+ * @author Alex Gidman, alex.gidman@surevine.com
+ */
+@ExtendWith(MockitoExtension.class)
+public class LocalOutgoingServerSessionTest
+{
+ private RemoteReceivingServerDummy remoteReceivingServerDummy;
+ private File tmpIdentityStoreFile;
+ private IdentityStore identityStore;
+ private File tmpTrustStoreFile;
+ private TrustStore trustStore;
+
+ /**
+ * Prepares the local server for operation. This mostly involves preparing the test fixture by mocking parts of the
+ * API that {@link LocalOutgoingServerSession#createOutgoingSession(DomainPair, int)} uses when establishing a
+ * connection.
+ */
+ @BeforeEach
+ public void setUpClass() throws Exception {
+ Fixtures.reconfigureOpenfireHome();
+ JiveGlobals.setProperty("xmpp.domain", Fixtures.XMPP_DOMAIN);
+ final XMPPServer xmppServer = Fixtures.mockXMPPServer();
+ XMPPServer.setInstance(xmppServer);
+
+ final File tmpDir = new File(System.getProperty("java.io.tmpdir"));
+
+ // Use a temporary file to hold the identity store that is used by the tests.
+ final CertificateStoreManager certificateStoreManager = mock(CertificateStoreManager.class, withSettings().lenient());
+ tmpIdentityStoreFile = new File(tmpDir, "unittest-identitystore-" + System.currentTimeMillis() + ".jks");
+ tmpIdentityStoreFile.deleteOnExit();
+ final CertificateStoreConfiguration identityStoreConfig = new CertificateStoreConfiguration("jks", tmpIdentityStoreFile, "secret".toCharArray(), tmpDir);
+ identityStore = new IdentityStore(identityStoreConfig, true);
+ doReturn(identityStore).when(certificateStoreManager).getIdentityStore(any());
+
+ // Use a temporary file to hold the trust store that is used by the tests.
+ tmpTrustStoreFile = new File(tmpDir, "unittest-truststore-" + System.currentTimeMillis() + ".jks");
+ tmpTrustStoreFile.deleteOnExit();
+ final CertificateStoreConfiguration trustStoreConfig = new CertificateStoreConfiguration("jks", tmpTrustStoreFile, "secret".toCharArray(), tmpDir);
+ trustStore = new TrustStore(trustStoreConfig, true);
+ doReturn(trustStore).when(certificateStoreManager).getTrustStore(any());
+
+ // Mock the connection configuration.
+ doReturn(certificateStoreManager).when(xmppServer).getCertificateStoreManager();
+
+ final ConnectionManager connectionManager = Fixtures.mockConnectionManager();
+ final ConnectionListener connectionListener = Fixtures.mockConnectionListener();
+ doAnswer(new ConnectionConfigurationAnswer(identityStoreConfig, trustStoreConfig)).when(connectionListener).generateConnectionConfiguration();
+ doReturn(Set.of(connectionListener)).when(connectionManager).getListeners(any(ConnectionType.class));
+ doReturn(connectionListener).when(connectionManager).getListener(any(ConnectionType.class), anyBoolean());
+ doReturn(connectionManager).when(xmppServer).getConnectionManager();
+ setUp();
+ }
+
+ /**
+ * Dynamically generate a ConnectionConfiguration answer, as used by the Mock ConnectionListener.
+ *
+ * A dynamic answer is needed, as the value of the ConnectionSettings.Server.TLS_POLICY property needs to be
+ * evaluated at run-time (this value is changed in the setup of many of the unit tests in this file).
+ *
+ */
+ private static class ConnectionConfigurationAnswer implements Answer {
+
+ private CertificateStoreConfiguration identityStoreConfig;
+ private CertificateStoreConfiguration trustStoreConfig;
+
+ private ConnectionConfigurationAnswer(CertificateStoreConfiguration identityStoreConfig, CertificateStoreConfiguration trustStoreConfig)
+ {
+ this.identityStoreConfig = identityStoreConfig;
+ this.trustStoreConfig = trustStoreConfig;
+ }
+
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable
+ {
+ final Connection.TLSPolicy tlsPolicy = Connection.TLSPolicy.valueOf(JiveGlobals.getProperty(ConnectionSettings.Server.TLS_POLICY, Connection.TLSPolicy.optional.toString()));
+ final Set suites = Set.of("TLS_AES_256_GCM_SHA384","TLS_AES_128_GCM_SHA256","TLS_CHACHA20_POLY1305_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256","TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256","TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256","TLS_DHE_RSA_WITH_AES_256_GCM_SHA384","TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256","TLS_DHE_DSS_WITH_AES_256_GCM_SHA384","TLS_DHE_RSA_WITH_AES_128_GCM_SHA256","TLS_DHE_DSS_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256","TLS_DHE_RSA_WITH_AES_256_CBC_SHA256","TLS_DHE_DSS_WITH_AES_256_CBC_SHA256","TLS_DHE_RSA_WITH_AES_128_CBC_SHA256","TLS_DHE_DSS_WITH_AES_128_CBC_SHA256","TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384","TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384","TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256","TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA","TLS_DHE_RSA_WITH_AES_256_CBC_SHA","TLS_DHE_DSS_WITH_AES_256_CBC_SHA","TLS_DHE_RSA_WITH_AES_128_CBC_SHA","TLS_DHE_DSS_WITH_AES_128_CBC_SHA","TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA","TLS_ECDH_RSA_WITH_AES_256_CBC_SHA","TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA","TLS_ECDH_RSA_WITH_AES_128_CBC_SHA","TLS_RSA_WITH_AES_256_GCM_SHA384","TLS_RSA_WITH_AES_128_GCM_SHA256","TLS_RSA_WITH_AES_256_CBC_SHA256","TLS_RSA_WITH_AES_128_CBC_SHA256","TLS_RSA_WITH_AES_256_CBC_SHA","TLS_RSA_WITH_AES_128_CBC_SHA","TLS_EMPTY_RENEGOTIATION_INFO_SCSV");
+ final Set protocols = Set.of("TLSv1.2");
+ return new ConnectionConfiguration(ConnectionType.SOCKET_S2S, true, 10, -1, Connection.ClientAuth.wanted, null, 9999, tlsPolicy, identityStoreConfig, trustStoreConfig, true, true, protocols, suites, Connection.CompressionPolicy.optional, true );
+ }
+ }
+
+ public void setUp() throws Exception
+ {
+ remoteReceivingServerDummy = new RemoteReceivingServerDummy();
+ remoteReceivingServerDummy.open();
+ DNSUtil.setDnsOverride(Map.of(RemoteReceivingServerDummy.XMPP_DOMAIN, new DNSUtil.HostAddress("localhost", remoteReceivingServerDummy.getPort(), false)));
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception
+ {
+ tmpIdentityStoreFile.delete();
+ tmpTrustStoreFile.delete();
+ identityStore = null;
+ trustStore = null;
+ DNSUtil.setDnsOverride(null);
+
+ if (remoteReceivingServerDummy != null) {
+ remoteReceivingServerDummy.close();
+ remoteReceivingServerDummy = null;
+ }
+
+ Fixtures.clearExistingProperties();
+ }
+
+ /**
+ * Unit test in which Openfire initiates an outgoing server-to-server connection.
+ *
+ * This test is parameterized, meaning that the configuration of both the local server and the remote mock server is
+ * passed as an argument to this method. These configurations are used to both initialize and execute the test, but
+ * also to calculate the expected outcome of the test, given the provided configuration. This expected outcome is
+ * asserted by the test implementation
+ *
+ * @param localServerSettings Server settings for the system under test (the 'local' server).
+ * @param remoteServerSettings Server settings for the mock server that is used as a peer in this test.
+ */
+ @ParameterizedTest
+ @MethodSource("arguments")
+ public void outgoingTest(final ServerSettings localServerSettings, final ServerSettings remoteServerSettings)
+ throws Exception
+ {
+ final ExpectedOutcome expected;
+ if (localServerSettings.encryptionPolicy == Connection.TLSPolicy.optional && localServerSettings.certificateState == ServerSettings.CertificateState.INVALID && localServerSettings.dialbackSupported &&
+ remoteServerSettings.encryptionPolicy == Connection.TLSPolicy.optional && remoteServerSettings.certificateState == ServerSettings.CertificateState.VALID && remoteServerSettings.dialbackSupported) {
+ // TODO: can we improve on this test to explicitly verify the outcome if the first connection?
+ expected = new ExpectedOutcome();
+ expected.set(NON_ENCRYPTED_WITH_DIALBACK_AUTH, "For this very specific configuration, the expected outcome is 'NO_CONNECTION'. However, Openfire will (and should) immediately make a new connection attempt (without TLS), that SHOULD succeed.");
+ } else {
+ expected = ExpectedOutcome.generateExpectedOutcome(localServerSettings, remoteServerSettings);
+ }
+ if (RemoteReceivingServerDummy.doLog) System.out.println("Executing test:\n - Local Server (Openfire, System under test) Settings: " + localServerSettings + "\n - Remote Server (dummy/mock server) Settings: " + remoteServerSettings + "\nExpected outcome: " + expected.getConnectionState());
+
+ JiveGlobals.setProperty("xmpp.domain", Fixtures.XMPP_DOMAIN);
+ final TrustStore trustStore = XMPPServer.getInstance().getCertificateStoreManager().getTrustStore(ConnectionType.SOCKET_S2S);
+ final IdentityStore identityStore = XMPPServer.getInstance().getCertificateStoreManager().getIdentityStore(ConnectionType.SOCKET_S2S);
+
+ try {
+ // Setup test fixture.
+
+ // Remote server TLS policy.
+ remoteReceivingServerDummy.setEncryptionPolicy(remoteServerSettings.encryptionPolicy);
+
+ // Remote server dialback
+ remoteReceivingServerDummy.setDisableDialback(!remoteServerSettings.dialbackSupported);
+
+ // Remote server certificate state
+ switch (remoteServerSettings.certificateState) {
+ case INVALID:
+ remoteReceivingServerDummy.setUseExpiredEndEntityCertificate(true);
+ // Intended fall-through
+ case VALID:
+ remoteReceivingServerDummy.preparePKIX();
+
+ // Install in local server's truststore.
+ final X509Certificate[] chain = remoteReceivingServerDummy.getGeneratedPKIX().getCertificateChain();
+ final X509Certificate caCert = chain[chain.length-1];
+ trustStore.installCertificate("unit-test", KeystoreTestUtils.toPemFormat(caCert));
+ break;
+ case MISSING:
+ break;
+ default:
+ throw new IllegalStateException("Unsupported remote certificate state");
+ }
+
+ // Local server TLS policy.
+ JiveGlobals.setProperty(ConnectionSettings.Server.TLS_POLICY, localServerSettings.encryptionPolicy.toString());
+
+ // Local server dialback.
+ JiveGlobals.setProperty(ConnectionSettings.Server.DIALBACK_ENABLED, localServerSettings.dialbackSupported ? "true" : "false");
+
+ // Local server certificate state
+ switch (localServerSettings.certificateState) {
+ case MISSING:
+ // Do not install domain certificate.
+ break;
+ case INVALID:
+ // Insert an expired certificate into the identity store
+ identityStore.installCertificate(Fixtures.expiredX509Certificate, Fixtures.privateKeyForExpiredCert, "");
+ break;
+ case VALID:
+ // Generate a valid certificate and insert into identity store
+ identityStore.ensureDomainCertificate();
+ break;
+ }
+
+ final DomainPair domainPair = new DomainPair(Fixtures.XMPP_DOMAIN, RemoteReceivingServerDummy.XMPP_DOMAIN);
+ final int port = remoteReceivingServerDummy.getPort();
+
+ // Execute system under test.
+ final LocalOutgoingServerSession result = LocalOutgoingServerSession.createOutgoingSession(domainPair, port);
+
+ // Verify results
+ if (RemoteReceivingServerDummy.doLog) System.out.println("Expect: " + expected.getConnectionState() + ", Result: " + result);
+ switch (expected.getConnectionState())
+ {
+ case NO_CONNECTION:
+ assertNull(result);
+ break;
+ case NON_ENCRYPTED_WITH_DIALBACK_AUTH:
+ assertNotNull(result);
+ assertFalse(result.isClosed());
+ assertFalse(result.isEncrypted());
+ assertTrue(result.isAuthenticated());
+ assertEquals(ServerSession.AuthenticationMethod.DIALBACK, result.getAuthenticationMethod());
+ break;
+ case ENCRYPTED_WITH_DIALBACK_AUTH:
+ assertNotNull(result);
+ assertFalse(result.isClosed());
+ assertTrue(result.isEncrypted());
+ assertTrue(result.isAuthenticated());
+ assertEquals(ServerSession.AuthenticationMethod.DIALBACK, result.getAuthenticationMethod());
+ break;
+ case ENCRYPTED_WITH_SASLEXTERNAL_AUTH:
+ assertNotNull(result);
+ assertFalse(result.isClosed());
+ assertTrue(result.isEncrypted());
+ assertTrue(result.isAuthenticated());
+ assertEquals(ServerSession.AuthenticationMethod.SASL_EXTERNAL, result.getAuthenticationMethod());
+ break;
+ }
+ } finally {
+ // Teardown test fixture.
+ trustStore.delete("unit-test");
+ }
+ }
+
+ /**
+ * Provides the arguments for the method that implements the unit test.
+ * @return Unit test arguments
+ */
+ private static Iterable arguments() {
+ final Collection result = new LinkedList<>();
+
+ final Set localServerSettings = new LinkedHashSet<>();
+ final Set remoteServerSettings = new LinkedHashSet<>();
+
+ for (final ServerSettings.CertificateState certificateState : ServerSettings.CertificateState.values()) {
+ for (final boolean dialbackSupported : List.of(true, false)) {
+ for (final Connection.TLSPolicy tlsPolicy : Connection.TLSPolicy.values()) {
+ if (tlsPolicy == Connection.TLSPolicy.legacyMode) {
+ continue; // TODO add support for DirectTLS in this unit test!
+ }
+ final ServerSettings serverSettings = new ServerSettings(tlsPolicy, certificateState, true, dialbackSupported); // TODO add support for both strict certificate validation settings.
+ localServerSettings.add(serverSettings);
+ remoteServerSettings.add(serverSettings);
+ }
+ }
+ }
+
+ for (final ServerSettings local : localServerSettings) {
+ for (final ServerSettings remote : remoteServerSettings) {
+ result.add(Arguments.arguments(local, remote));
+ }
+ }
+
+ // Not all test-runners easily identify the parameters that are used to run each test iteration. Those that do
+ // not, typically show a number. By outputting the numbered arguments, they can be cross-referenced with any
+ // failed test case.
+ int i = 1;
+ for (Arguments arguments : result) {
+ System.out.println("Test [" + i++ + "]: " + arguments.get()[0] + ", " + arguments.get()[1]);
+ }
+ return result;
+ }
+}
diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/session/RemoteInitiatingServerDummy.java b/xmppserver/src/test/java/org/jivesoftware/openfire/session/RemoteInitiatingServerDummy.java
new file mode 100644
index 0000000000..7614839b0b
--- /dev/null
+++ b/xmppserver/src/test/java/org/jivesoftware/openfire/session/RemoteInitiatingServerDummy.java
@@ -0,0 +1,555 @@
+package org.jivesoftware.openfire.session;
+
+import org.dom4j.*;
+import org.jivesoftware.openfire.Connection;
+import org.jivesoftware.util.Base64;
+
+import javax.net.ssl.*;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.net.*;
+import java.security.KeyManagementException;
+import java.security.KeyPair;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.concurrent.*;
+
+public class RemoteInitiatingServerDummy extends AbstractRemoteServerDummy
+{
+ /**
+ * When switched to 'true', most XMPP interaction will be printed to standard-out.
+ */
+ public static final boolean doLog = false;
+
+ private ServerSocket dialbackAuthoritativeServer;
+ private Thread dialbackAcceptThread;
+ private DialbackAcceptor dialbackAcceptor = new DialbackAcceptor();
+
+ private final String connectTo;
+ boolean attemptedEncryptionNegotiation = false;
+ boolean alreadyTriedSaslExternal = false;
+ boolean peerSupportsDialback;
+ private ExecutorService processingService;
+
+ /**
+ * A monitor that is used to flag when this dummy has finished trying to set up a connection to Openfire. This is to
+ * help the unit test know when it can start verifying the test outcome.
+ */
+ private final Phaser phaser = new Phaser(0);
+
+ public RemoteInitiatingServerDummy(final String connectTo)
+ {
+ this.connectTo = connectTo;
+ }
+
+ public void init() throws IOException
+ {
+ if (!disableDialback) {
+ dialbackAuthoritativeServer = new ServerSocket(0);
+ }
+ }
+
+ public void connect(int port) throws IOException, InterruptedException
+ {
+ if (doLog) System.out.println("connect");
+ processingService = Executors.newCachedThreadPool();
+
+ if (dialbackAuthoritativeServer != null) {
+ dialbackAcceptThread = new Thread(dialbackAcceptor);
+ dialbackAcceptThread.start();
+ }
+
+ phaser.register();
+
+ final SocketProcessor socketProcessor = new SocketProcessor(port);
+ processingService.submit(socketProcessor);
+ }
+
+ public void blockUntilDone(final long timeout, final TimeUnit unit) {
+ try {
+ phaser.awaitAdvanceInterruptibly(0, timeout, unit);
+ } catch (InterruptedException | TimeoutException e) {
+ throw new RuntimeException("Test scenario never reached 'done' state");
+ }
+ }
+
+ protected void done() {
+ phaser.arriveAndDeregister();
+ }
+
+ public void disconnect() throws InterruptedException, IOException
+ {
+ if (doLog) System.out.println("disconnect");
+ stopProcessingService();
+ stopDialbackAcceptThread();
+ if (dialbackAuthoritativeServer != null) {
+ dialbackAuthoritativeServer.close();
+ dialbackAuthoritativeServer = null;
+ }
+ }
+
+ public synchronized void stopProcessingService() throws InterruptedException
+ {
+ processingService.shutdown();
+ final Instant end = Instant.now().plus(SO_TIMEOUT.multipliedBy(20));
+ while (Instant.now().isBefore(end) && !processingService.isTerminated()) {
+ Thread.sleep(Math.max(100, SO_TIMEOUT.dividedBy(50).toMillis()));
+ }
+ if (!processingService.isTerminated()) {
+ processingService.shutdownNow();
+ }
+ }
+
+ public synchronized void stopDialbackAcceptThread() throws InterruptedException
+ {
+ if (dialbackAcceptThread == null) {
+ return;
+ }
+ dialbackAcceptor.stop();
+ dialbackAcceptThread.interrupt();
+ final Instant end = Instant.now().plus(SO_TIMEOUT.multipliedBy(20));
+ while (Instant.now().isBefore(end) && dialbackAcceptThread.getState() != Thread.State.TERMINATED) {
+ Thread.sleep(Math.max(10, SO_TIMEOUT.dividedBy(50).toMillis()));
+ }
+ final Thread.State finalState = dialbackAcceptThread.getState();
+ if (finalState != Thread.State.TERMINATED) {
+ if (doLog) System.err.println("Dialback Accept thread not terminating after it was stopped. Current state: " + finalState);
+ if (doLog) Arrays.stream(dialbackAcceptThread.getStackTrace()).forEach(System.err::println);
+ dialbackAcceptThread.stop();
+ }
+ dialbackAcceptThread = null;
+ }
+
+ public int getDialbackAuthoritativeServerPort()
+ {
+ return dialbackAuthoritativeServer != null ? dialbackAuthoritativeServer.getLocalPort() : -1;
+ }
+
+ private class DialbackAcceptor implements Runnable
+ {
+ boolean shouldStop = false;
+
+ void stop() {
+ shouldStop = true;
+ }
+
+ @Override
+ public void run()
+ {
+ if (doLog) System.out.println("Start accepting socket connections (as Server Dialback Authoritative Server).");
+ while (!shouldStop) {
+ try {
+ dialbackAuthoritativeServer.setSoTimeout((int)SO_TIMEOUT.multipliedBy(10).toMillis());
+ final Socket socket = dialbackAuthoritativeServer.accept();
+ final InputStream is = socket.getInputStream();
+ final OutputStream os = socket.getOutputStream();
+ if (doLog) System.out.println("DIALBACK AUTH SERVER: Accepted new socket connection.");
+
+ Thread.sleep(100);
+ final byte[] buffer = new byte[1024 * 16];
+ int count;
+ while ((count = is.read(buffer)) > 0) {
+ String read = new String(buffer, 0, count);
+ if (doLog) System.out.println("# DIALBACK AUTH SERVER recv");
+ if (doLog) System.out.println(read);
+ if (doLog) System.out.println();
+
+ final Document outbound = DocumentHelper.createDocument();
+ final Namespace namespace = new Namespace("stream", "http://etherx.jabber.org/streams");
+ final Namespace dialbackNamespace = new Namespace("db", "jabber:server:dialback");
+ final Element stream = outbound.addElement(QName.get("stream", namespace));
+ stream.add(Namespace.get("jabber:server"));
+ stream.add(dialbackNamespace);
+ stream.addAttribute("from", XMPP_DOMAIN);
+ stream.addAttribute("to", connectTo);
+ stream.addAttribute("version", "1.0");
+
+ String response = null;
+ if (read.startsWith(""));
+ } else if (read.startsWith("")) {
+ response = "";
+ } else {
+ if (doLog) System.out.println("I don't know how to process this data.");
+ }
+
+ if (response != null) {
+ if (doLog) System.out.println("# DIALBACK AUTH SERVER send to Openfire");
+ if (doLog) System.out.println(response);
+ if (doLog) System.out.println();
+ os.write(response.getBytes());
+ os.flush();
+
+ if (response.equals("