/** * * Copyright 2018-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * 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.igniterealtime.smack.inttest; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; 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; import org.jivesoftware.smack.AbstractXMPPConnection; import org.jivesoftware.smack.ConnectionConfiguration; import org.jivesoftware.smack.SmackException; 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.util.MultiMap; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.websocket.java11.Java11WebSocketFactory; import org.jivesoftware.smack.websocket.okhttp.OkHttpWebSocketFactory; import org.jivesoftware.smackx.admin.ServiceAdministrationManager; import org.jivesoftware.smackx.iqregister.AccountManager; import org.igniterealtime.smack.inttest.Configuration.AccountRegistration; import org.igniterealtime.smack.inttest.SmackIntegrationTestFramework.AccountNum; import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Localpart; import org.jxmpp.stringprep.XmppStringprepException; public class XmppConnectionManager { private static final Logger LOGGER = Logger.getLogger(XmppConnectionManager.class.getName()); 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 { 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() ); addConnectionDescriptor( XmppConnectionDescriptor.buildWebsocketDescriptor("modular-websocket-okhttp", OkHttpWebSocketFactory.class) ); addConnectionDescriptor( XmppConnectionDescriptor.buildWebsocketDescriptor("modular-websocket-java11", Java11WebSocketFactory.class) ); } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new AssertionError(e); } } public static boolean addConnectionDescriptor( XmppConnectionDescriptor> connectionDescriptor) { String nickname = connectionDescriptor.getNickname(); Class connectionClass = connectionDescriptor.getConnectionClass(); boolean alreadyExisted; synchronized (CONNECTION_DESCRIPTORS) { alreadyExisted = removeConnectionDescriptor(nickname); CONNECTION_DESCRIPTORS.put(connectionClass, connectionDescriptor); NICKNAME_CONNECTION_DESCRIPTORS.put(connectionDescriptor.getNickname(), connectionDescriptor); } return alreadyExisted; } 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 Map>> nicknameConnectionDescriptors; private final MultiMap< Class, XmppConnectionDescriptor> > connectionDescriptors; private final SmackIntegrationTestFramework sinttestFramework; private final Configuration sinttestConfiguration; private final String testRunId; 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. */ AbstractXMPPConnection conOne, conTwo, conThree; /** * A pool of authenticated and free to use connections. */ private final MultiMap, AbstractXMPPConnection> connectionPool = new MultiMap<>(); /** * A list of all ever created connections. */ private final Map> connections = new ConcurrentHashMap<>(); 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; 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) { case serviceAdministration: case inBandRegistration: accountRegistrationConnection = defaultConnectionDescriptor.construct(sinttestConfiguration); accountRegistrationConnection.connect(); accountRegistrationConnection.login(sinttestConfiguration.adminAccountUsername, sinttestConfiguration.adminAccountPassword); if (sinttestConfiguration.accountRegistration == AccountRegistration.inBandRegistration) { adminManager = null; accountManager = AccountManager.getInstance(accountRegistrationConnection); } else { adminManager = ServiceAdministrationManager.getInstanceFor(accountRegistrationConnection); accountManager = null; } break; case disabled: accountRegistrationConnection = null; adminManager = null; accountManager = null; break; default: throw new AssertionError(); } } SmackIntegrationTestEnvironment prepareEnvironment() throws KeyManagementException, NoSuchAlgorithmException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, SmackException, IOException, XMPPException, InterruptedException { prepareMainConnections(); return new SmackIntegrationTestEnvironment(conOne, conTwo, conThree, sinttestFramework.testRunResult.testRunId, sinttestConfiguration, this); } private void prepareMainConnections() throws KeyManagementException, NoSuchAlgorithmException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, SmackException, IOException, XMPPException, InterruptedException { final int mainAccountCount = AccountNum.values().length; List connections = new ArrayList<>(mainAccountCount); for (AccountNum mainAccountNum : AccountNum.values()) { AbstractXMPPConnection mainConnection = getConnectedMainConnectionFor(mainAccountNum); connections.add(mainConnection); } conOne = connections.get(0); conTwo = connections.get(1); conThree = connections.get(2); } public XmppConnectionDescriptor> getDefaultConnectionDescriptor() { return defaultConnectionDescriptor; } public Collection>> getConnectionDescriptors() { return Collections.unmodifiableCollection(nicknameConnectionDescriptors.values()); } @SuppressWarnings("unchecked") public XmppConnectionDescriptor> getConnectionDescriptorFor( Class connectionClass) { return (XmppConnectionDescriptor>) connectionDescriptors.getFirst( connectionClass); } void disconnectAndCleanup() throws InterruptedException { int successfullyDeletedAccountsCount = 0; for (AbstractXMPPConnection connection : connections.keySet()) { if (sinttestConfiguration.accountRegistration == AccountRegistration.inBandRegistration) { // Note that we use the account manager from the to-be-deleted connection. AccountManager accountManager = AccountManager.getInstance(connection); try { accountManager.deleteAccount(); successfullyDeletedAccountsCount++; } catch (NoResponseException | XMPPErrorException | NotConnectedException e) { LOGGER.log(Level.WARNING, "Could not delete dynamically registered account", e); } } connection.disconnect(); if (sinttestConfiguration.accountRegistration == AccountRegistration.serviceAdministration) { String username = connection.getConfiguration().getUsername().toString(); Localpart usernameAsLocalpart; try { usernameAsLocalpart = Localpart.from(username); } catch (XmppStringprepException e) { throw new AssertionError(e); } EntityBareJid connectionAddress = JidCreate.entityBareFrom(usernameAsLocalpart, sinttestConfiguration.service); try { adminManager.deleteUser(connectionAddress); successfullyDeletedAccountsCount++; } catch (NoResponseException | XMPPErrorException | NotConnectedException e) { LOGGER.log(Level.WARNING, "Could not delete dynamically registered account", e); } } } if (sinttestConfiguration.isAccountRegistrationPossible()) { int unsuccessfullyDeletedAccountsCount = connections.size() - successfullyDeletedAccountsCount; if (unsuccessfullyDeletedAccountsCount == 0) { LOGGER.info("Successfully deleted all created accounts ✔"); } else { LOGGER.warning("Could not delete all created accounts, " + unsuccessfullyDeletedAccountsCount + " remainaing"); } } connections.clear(); if (accountRegistrationConnection != null) { accountRegistrationConnection.disconnect(); } } private static final String USERNAME_PREFIX = "smack-inttest"; private AbstractXMPPConnection getConnectedMainConnectionFor(AccountNum accountNum) throws SmackException, IOException, XMPPException, InterruptedException, KeyManagementException, NoSuchAlgorithmException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { String middlefix; String accountUsername; String accountPassword; switch (accountNum) { case One: accountUsername = sinttestConfiguration.accountOneUsername; accountPassword = sinttestConfiguration.accountOnePassword; middlefix = "one"; break; case Two: accountUsername = sinttestConfiguration.accountTwoUsername; accountPassword = sinttestConfiguration.accountTwoPassword; middlefix = "two"; break; case Three: accountUsername = sinttestConfiguration.accountThreeUsername; accountPassword = sinttestConfiguration.accountThreePassword; middlefix = "three"; break; default: throw new IllegalStateException(); } // Note that it is perfectly fine for account(Username|Password) to be 'null' at this point. final String finalAccountUsername = StringUtils.isNullOrEmpty(accountUsername) ? USERNAME_PREFIX + '-' + middlefix + '-' + testRunId : accountUsername; final String finalAccountPassword = StringUtils.isNullOrEmpty(accountPassword) ? StringUtils.insecureRandomString(16) : accountPassword; if (sinttestConfiguration.isAccountRegistrationPossible()) { registerAccount(finalAccountUsername, finalAccountPassword); } AbstractXMPPConnection mainConnection = defaultConnectionDescriptor.construct(sinttestConfiguration, builder -> { try { builder.setUsernameAndPassword(finalAccountUsername, finalAccountPassword) .setResource(middlefix + '-' + testRunId); } catch (XmppStringprepException e) { throw new IllegalArgumentException(e); } }); connections.put(mainConnection, defaultConnectionDescriptor); mainConnection.connect(); mainConnection.login(); return mainConnection; } private void registerAccount(String username, String password) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, XmppStringprepException { if (accountRegistrationConnection == null) { throw new IllegalStateException("Account registration not configured"); } switch (sinttestConfiguration.accountRegistration) { case serviceAdministration: EntityBareJid userJid = JidCreate.entityBareFrom(Localpart.from(username), accountRegistrationConnection.getXMPPServiceDomain()); adminManager.addUser(userJid, password); break; case inBandRegistration: if (!accountManager.supportsAccountCreation()) { throw new UnsupportedOperationException("Account creation/registation is not supported"); } Set requiredAttributes = accountManager.getAccountAttributes(); if (requiredAttributes.size() > 4) { throw new IllegalStateException("Unkown required attributes"); } Map additionalAttributes = new HashMap<>(); additionalAttributes.put("name", "Smack Integration Test"); additionalAttributes.put("email", "flow@igniterealtime.org"); Localpart usernameLocalpart = Localpart.from(username); accountManager.createAccount(usernameLocalpart, password, additionalAttributes); break; case disabled: throw new IllegalStateException("Account creation no possible"); } } List constructConnectedConnections(XmppConnectionDescriptor> connectionDescriptor, int count) throws InterruptedException, SmackException, IOException, XMPPException { List connections = new ArrayList<>(count); synchronized (connectionPool) { @SuppressWarnings("unchecked") List pooledConnections = (List) connectionPool.getAll(connectionDescriptor); while (count > 0 && !pooledConnections.isEmpty()) { C connection = pooledConnections.remove(pooledConnections.size() - 1); connections.add(connection); count--; } } for (int i = 0; i < count; i++) { C connection = constructConnectedConnection(connectionDescriptor); connections.add(connection); } return connections; } private C constructConnectedConnection( XmppConnectionDescriptor> connectionDescriptor) throws InterruptedException, SmackException, IOException, XMPPException { C connection = constructConnection(connectionDescriptor, null); connection.connect(); connection.login(); return connection; } AbstractXMPPConnection constructConnection() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { return constructConnection(defaultConnectionDescriptor); } C constructConnection( XmppConnectionDescriptor> connectionDescriptor) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { return constructConnection(connectionDescriptor, null); } private C constructConnection( XmppConnectionDescriptor> connectionDescriptor, Collection customConnectionConfigurationAppliers) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { String username = "sinttest-" + testRunId + '-' + (connections.size() + 1); String password = StringUtils.randomString(24); return constructConnection(username, password, connectionDescriptor, customConnectionConfigurationAppliers); } private C constructConnection(final String username, final String password, XmppConnectionDescriptor> connectionDescriptor, Collection customConnectionConfigurationAppliers) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { try { registerAccount(username, password); } catch (XmppStringprepException e) { throw new IllegalArgumentException(e); } ConnectionConfigurationBuilderApplier usernameAndPasswordApplier = configurationBuilder -> { configurationBuilder.setUsernameAndPassword(username, password); }; if (customConnectionConfigurationAppliers == null) { customConnectionConfigurationAppliers = Collections.singleton(usernameAndPasswordApplier); } else { customConnectionConfigurationAppliers.add(usernameAndPasswordApplier); } C connection; try { connection = connectionDescriptor.construct(sinttestConfiguration, customConnectionConfigurationAppliers); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new IllegalStateException(e); } connections.put(connection, connectionDescriptor); return connection; } void recycle(Collection connections) { for (AbstractXMPPConnection connection : connections) { recycle(connection); } } void recycle(AbstractXMPPConnection connection) { Class connectionClass = connection.getClass(); if (!connectionDescriptors.containsKey(connectionClass)) { throw new IllegalStateException("Attempt to recycle unknown connection of class '" + connectionClass + "'"); } if (connection.isAuthenticated()) { XmppConnectionDescriptor connectionDescriptor = connections.get(connection); if (connectionDescriptor == null) { throw new IllegalStateException("Attempt to recycle unknown connection: " + connection); } synchronized (connectionPool) { connectionPool.put(connectionDescriptor, connection); } } else { connection.disconnect(); } // Note that we do not delete the account of the unauthenticated connection here, as it is done at the end of // the test run together with all other dynamically created accounts. } }