All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openremote.manager.mqtt.MQTTBrokerService Maven / Gradle / Ivy

/*
 * Copyright 2022, OpenRemote Inc.
 *
 * See the CONTRIBUTORS.txt file in the distribution for a
 * full listing of individual contributors.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see .
 */
package org.openremote.manager.mqtt;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import io.micrometer.core.instrument.MeterRegistry;
import io.netty.channel.ChannelId;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.ActiveMQExceptionType;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.api.core.client.ActiveMQClient;
import org.apache.activemq.artemis.api.core.client.ClientSession;
import org.apache.activemq.artemis.api.core.client.ClientSessionFactory;
import org.apache.activemq.artemis.api.core.client.ServerLocator;
import org.apache.activemq.artemis.api.core.management.CoreNotificationType;
import org.apache.activemq.artemis.api.core.management.ManagementHelper;
import org.apache.activemq.artemis.core.client.impl.ClientSessionInternal;
import org.apache.activemq.artemis.core.config.Configuration;
import org.apache.activemq.artemis.core.config.MetricsConfiguration;
import org.apache.activemq.artemis.core.config.WildcardConfiguration;
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl;
import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTUtil;
import org.apache.activemq.artemis.core.remoting.FailureListener;
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnection;
import org.apache.activemq.artemis.core.security.impl.SecurityStoreImpl;
import org.apache.activemq.artemis.core.server.ServerSession;
import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ;
import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerConnectionPlugin;
import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerSessionPlugin;
import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy;
import org.apache.activemq.artemis.core.settings.impl.AddressSettings;
import org.apache.activemq.artemis.core.settings.impl.PageFullMessagePolicy;
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
import org.apache.activemq.artemis.spi.core.security.jaas.GuestLoginModule;
import org.apache.activemq.artemis.spi.core.security.jaas.PrincipalConversionLoginModule;
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal;
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal;
import org.apache.camel.builder.RouteBuilder;
import org.apache.http.client.utils.URIBuilder;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.adapters.jaas.AbstractKeycloakLoginModule;
import org.openremote.container.message.MessageBrokerService;
import org.openremote.container.security.keycloak.KeycloakIdentityProvider;
import org.openremote.container.timer.TimerService;
import org.openremote.manager.asset.AssetProcessingService;
import org.openremote.manager.asset.AssetStorageService;
import org.openremote.manager.event.ClientEventService;
import org.openremote.manager.security.AuthorisationService;
import org.openremote.manager.security.ManagerIdentityService;
import org.openremote.manager.security.ManagerKeycloakIdentityProvider;
import org.openremote.manager.security.MultiTenantClientCredentialsGrantsLoginModule;
import org.openremote.model.Constants;
import org.openremote.model.Container;
import org.openremote.model.ContainerService;
import org.openremote.model.PersistenceEvent;
import org.openremote.model.asset.UserAssetLink;
import org.openremote.model.security.User;
import org.openremote.model.util.Debouncer;
import org.openremote.model.util.TextUtil;
import org.openremote.model.util.UniqueIdentifierGenerator;

import javax.security.auth.Subject;
import javax.security.auth.login.AppConfigurationEntry;
import java.security.Principal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static java.lang.System.Logger.Level.*;
import static java.util.stream.StreamSupport.stream;
import static org.openremote.container.persistence.PersistenceService.PERSISTENCE_TOPIC;
import static org.openremote.container.util.MapAccess.getInteger;
import static org.openremote.container.util.MapAccess.getString;
import static org.openremote.model.Constants.KEYCLOAK_CLIENT_ID;
import static org.openremote.model.syslog.SyslogCategory.API;

// TODO: Add queue size limiting in canPublish of MQTTHandlers (needs to be done at auth time to allow pub to be rejected)
public class MQTTBrokerService extends RouteBuilder implements ContainerService, ActiveMQServerConnectionPlugin, ActiveMQServerSessionPlugin {

    public static final String MQTT_FORCE_USER_DISCONNECT_DEBOUNCE_MILLIS = "MQTT_FORCE_USER_DISCONNECT_DEBOUNCE_MILLIS";
    public static int MQTT_FORCE_USER_DISCONNECT_DEBOUNCE_MILLIS_DEFAULT = 5000;
    public static final int PRIORITY = MED_PRIORITY;
    public static final String MQTT_SERVER_LISTEN_HOST = "MQTT_SERVER_LISTEN_HOST";
    public static final String MQTT_SERVER_LISTEN_PORT = "MQTT_SERVER_LISTEN_PORT";
    public static final String ANONYMOUS_USERNAME = "anonymous";
    protected final WildcardConfiguration wildcardConfiguration = new WildcardConfiguration();
    protected static final System.Logger LOG = System.getLogger(MQTTBrokerService.class.getName() + "." + API.name());

    protected AssetStorageService assetStorageService;
    protected AuthorisationService authorisationService;
    protected ManagerKeycloakIdentityProvider identityProvider;
    protected ClientEventService clientEventService;
    protected MessageBrokerService messageBrokerService;
    protected ExecutorService executorService;
    protected TimerService timerService;
    protected AssetProcessingService assetProcessingService;
    protected List customHandlers = new ArrayList<>();
    protected ConcurrentMap clientIDConnectionMap = new ConcurrentHashMap<>();
    protected ConcurrentMap connectionIDConnectionMap = new ConcurrentHashMap<>();
    protected ConcurrentMap>> userAssetLinkChangeMap = new ConcurrentHashMap<>();
    protected Debouncer userAssetDisconnectDebouncer;
    // Stores disconnected connections for a short period to allow last will publishes to be processed
    protected Cache disconnectedConnectionCache;
    protected boolean active;
    protected String host;
    protected int port;
    protected Configuration serverConfiguration;
    protected EmbeddedActiveMQ server;
    protected ActiveMQORSecurityManager securityManager;
    protected ServerLocator serverLocator;
    protected ClientSessionFactory sessionFactory;

    @Override
    public int getPriority() {
        return PRIORITY;
    }

    @Override
    public void init(Container container) throws Exception {
        host = getString(container.getConfig(), MQTT_SERVER_LISTEN_HOST, "0.0.0.0");
        port = getInteger(container.getConfig(), MQTT_SERVER_LISTEN_PORT, 1883);
        int debounceMillis = getInteger(container.getConfig(), MQTT_FORCE_USER_DISCONNECT_DEBOUNCE_MILLIS, MQTT_FORCE_USER_DISCONNECT_DEBOUNCE_MILLIS_DEFAULT);
        assetStorageService = container.getService(AssetStorageService.class);
        authorisationService = container.getService(AuthorisationService.class);
        clientEventService = container.getService(ClientEventService.class);
        ManagerIdentityService identityService = container.getService(ManagerIdentityService.class);
        messageBrokerService = container.getService(MessageBrokerService.class);
        executorService = container.getExecutor();
        timerService = container.getService(TimerService.class);
        assetProcessingService = container.getService(AssetProcessingService.class);

        userAssetDisconnectDebouncer = new Debouncer<>(container.getScheduledExecutor(), id -> processUserAssetLinkChange(id, userAssetLinkChangeMap.remove(id)), debounceMillis);
        // This allows last will messages to be processed
        disconnectedConnectionCache = CacheBuilder.newBuilder()
                .maximumSize(10000)
                .expireAfterWrite(3000, TimeUnit.MILLISECONDS)
                .build();

        if (!identityService.isKeycloakEnabled()) {
            LOG.log(WARNING, "MQTT connections are not supported when not using Keycloak identity provider");
            active = false;
        } else {
            active = true;
            identityProvider = (ManagerKeycloakIdentityProvider) identityService.getIdentityProvider();
            container.getService(MessageBrokerService.class).getContext().addRoutes(this);
        }

        // Create server config
        serverConfiguration = new ConfigurationImpl();
        serverConfiguration.addAcceptorConfiguration("in-vm", "vm://0?protocols=core");
        String serverURI = new URIBuilder().setScheme("tcp").setHost(host).setPort(port)
            .setParameter("protocols", "MQTT")
            .setParameter("allowLinkStealing", "true")
            .setParameter("defaultMqttSessionExpiryInterval", "0") // Don't support retained sessions
            .build().toString();
        serverConfiguration.addAcceptorConfiguration("tcp", serverURI);
        serverConfiguration.registerBrokerPlugin(this);
        if (container.getMeterRegistry() != null) {
            serverConfiguration.setMetricsConfiguration(new MetricsConfiguration().setJvmMemory(false).setPlugin(new org.apache.activemq.artemis.core.server.metrics.plugins.SimpleMetricsPlugin() {
                @Override
                public MeterRegistry getRegistry() {
                    return container.getMeterRegistry();
                }
            }));
        }
        serverConfiguration.setWildCardConfiguration(wildcardConfiguration);
        serverConfiguration.setLiteralMatchMarkers("()");

        // Configure global address settings - aggressively cleanup queues (don't support resumable sessions)
        serverConfiguration.addAddressSetting(wildcardConfiguration.getAnyWordsString(),
            new AddressSettings()
                .setDeadLetterAddress(SimpleString.of("ActiveMQ.DLQ"))
                .setExpiryAddress(SimpleString.of("ActiveMQ.expired"))
                .setAutoDeleteCreatedQueues(true)
                .setAutoDeleteAddresses(true)
                // Auto delete MQTT addresses after 1 day as they never get flagged as used so will linger otherwise
                .setAutoDeleteAddressesSkipUsageCheck(true)
                .setAutoDeleteAddressesDelay(86400000)
                .setAutoDeleteQueuesMessageCount(-1L)
                .setAutoDeleteQueuesDelay(0)
                // This has a negative impact on performance if set to 0
                .setDefaultConsumerWindowSize(-1)
                .setPageLimitMessages(0L)
                .setAddressFullMessagePolicy(AddressFullMessagePolicy.FAIL)
                .setPageFullMessagePolicy(PageFullMessagePolicy.FAIL)
                // We don't want excessive metrics so only enable metrics for each custom handler address
                .setEnableMetrics(false)
        );

        // The below is an example of rate limiting at the address level the FAIL policy will cause an exception
        // that is handled by the MQTTProtocolHandler which will disconnect the client (MQTT doesn't have a nice
        // way of handling rejected publishes)
//        serverConfiguration.addAddressSetting("*.*.writeattributevalue.#",
//            new AddressSettings()
//                .setMaxSizeMessages(3)
//                .setAddressFullMessagePolicy(AddressFullMessagePolicy.FAIL)
//                .setMaxSizeBytes(1L)
//        );

        serverConfiguration.setPersistenceEnabled(false);

        // TODO: Make auto provisioning clients disconnect and reconnect with credentials or pass through X.509 certificates for auth
        // Cannot use authentication or authorisation cache as auto provisioning MQTT clients will authenticate as anonymous and this is then baked into the created ServerSession and cannot be modified
        // so all anonymous sessions will use the same username/password for key lookups in the caches - Can possibly use caching if ActiveMQ makes changes and/or we move to using X.509 TLS with ActiveMQ
        //config.setSecurityInvalidationInterval(600000); // Long cache as we force clear it when needed
        serverConfiguration.setAuthenticationCacheSize(0);
        serverConfiguration.setAuthorizationCacheSize(0);

        // Load custom handlers
        this.customHandlers = stream(ServiceLoader.load(MQTTHandler.class).spliterator(), false)
                .sorted(Comparator.comparingInt(MQTTHandler::getPriority))
                .collect(Collectors.toList());

        // Init each custom handler
        for (MQTTHandler handler : customHandlers) {
            try {
                handler.init(container, serverConfiguration);
            } catch (Exception e) {
                LOG.log(WARNING, "MQTT custom handler threw an exception whilst initialising: handler=" + handler.getName(), e);
                throw e;
            }
        }
    }

    @Override
    public void start(Container container) throws Exception {

        if (!active) {
            return;
        }

        // Start the broker
        server = new EmbeddedActiveMQ();
        server.setConfiguration(serverConfiguration);

        securityManager = new ActiveMQORSecurityManager(authorisationService, this, realm -> identityProvider.getKeycloakDeployment(realm, KEYCLOAK_CLIENT_ID), "", new SecurityConfiguration() {
            @Override
            public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
                return new AppConfigurationEntry[]{
                    new AppConfigurationEntry(GuestLoginModule.class.getName(), AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT, Map.of("debug", "true", "credentialsInvalidate", "true", "org.apache.activemq.jaas.guest.user", ANONYMOUS_USERNAME, "org.apache.activemq.jaas.guest.role", ANONYMOUS_USERNAME)),
                    new AppConfigurationEntry(MultiTenantClientCredentialsGrantsLoginModule.class.getName(), AppConfigurationEntry.LoginModuleControlFlag.REQUISITE, Map.of(
                        MultiTenantClientCredentialsGrantsLoginModule.INCLUDE_REALM_ROLES_OPTION, "true",
                        AbstractKeycloakLoginModule.ROLE_PRINCIPAL_CLASS_OPTION, RolePrincipal.class.getName()
                    )),
                    new AppConfigurationEntry(PrincipalConversionLoginModule.class.getName(), AppConfigurationEntry.LoginModuleControlFlag.REQUISITE, Map.of(PrincipalConversionLoginModule.PRINCIPAL_CLASS_LIST, KeycloakPrincipal.class.getName()))
                };
            }
        });

        server.setSecurityManager(securityManager);
        server.start();
        LOG.log(DEBUG, "Started MQTT broker");

        // Add notification handler for subscribe/unsubscribe and publish events
        server.getActiveMQServer().getManagementService().addNotificationListener(notification -> {
            if (notification.getType() == CoreNotificationType.CONSUMER_CREATED || notification.getType() == CoreNotificationType.CONSUMER_CLOSED) {
                boolean isSubscribe = notification.getType() == CoreNotificationType.CONSUMER_CREATED;
                String sessionId = notification.getProperties().getSimpleStringProperty(ManagementHelper.HDR_SESSION_NAME).toString();
                String topic = notification.getProperties().getSimpleStringProperty(ManagementHelper.HDR_ADDRESS).toString();
                ServerSession session = server.getActiveMQServer().getSessionByID(sessionId);

                // Ignore internal subscriptions
                boolean isInternal = session.getRemotingConnection().getTransportConnection() instanceof InVMConnection;
                if (isInternal) {
                    return;
                }

                if (isSubscribe) {
                    onSubscribe(session.getRemotingConnection(), MQTTUtil.getMqttTopicFromCoreAddress(topic, wildcardConfiguration));
                } else {
                    onUnsubscribe(session.getRemotingConnection(), MQTTUtil.getMqttTopicFromCoreAddress(topic, wildcardConfiguration));
                }
            }
        });

        // Don't use producer flow control
        serverLocator = ActiveMQClient.createServerLocator("vm://0").setProducerWindowSize(-1);
        sessionFactory = serverLocator.createSessionFactory();

        // Start each custom handler
        for (MQTTHandler handler : customHandlers) {
            try {
                handler.start(container);
            } catch (Exception e) {
                LOG.log(WARNING, "MQTT custom handler threw an exception whilst starting: handler=" + handler.getName(), e);
                throw e;
            }
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public void configure() throws Exception {
        from(PERSISTENCE_TOPIC)
                .routeId("Persistence-UserAndAssetLink")
                .filter(body().isInstanceOf(PersistenceEvent.class))
                .process(exchange -> {
                    PersistenceEvent persistenceEvent = (PersistenceEvent) exchange.getIn().getBody(PersistenceEvent.class);

                    if (persistenceEvent.getEntity() instanceof User user) {

                        if (!user.isServiceAccount()) {
                            return;
                        }

                        boolean forceDisconnect = persistenceEvent.getCause() == PersistenceEvent.Cause.DELETE;

                        if (persistenceEvent.getCause() == PersistenceEvent.Cause.UPDATE) {
                            // Force disconnect if certain properties have changed
                            forceDisconnect = persistenceEvent.hasPropertyChanged("enabled")
                                || persistenceEvent.hasPropertyChanged("username")
                                || persistenceEvent.hasPropertyChanged("secret");
                        }

                        if (forceDisconnect) {
                            LOG.log(TRACE, "User modified or deleted so force closing any sessions for this user: " + user);
                            // Find existing connection for this user
                            getUserConnections(user.getId()).forEach(this::doForceDisconnect);
                        }

                    } else if (persistenceEvent.getEntity() instanceof UserAssetLink userAssetLink) {
                        String userID = userAssetLink.getId().getUserId();
                        // Debounce force disconnect check of this user's sessions as there could be many asset links changing
                        List> changedUserAssetLinks = userAssetLinkChangeMap.computeIfAbsent(userID, id -> Collections.synchronizedList(new ArrayList<>()));
                        changedUserAssetLinks.add((PersistenceEvent) persistenceEvent);
                        userAssetDisconnectDebouncer.call(userID);
                    }
                });
    }

    @Override
    public void stop(Container container) throws Exception {

        userAssetDisconnectDebouncer.cancelAll(true);

        server.stop();
        LOG.log(DEBUG, "Stopped MQTT broker");

        stream(ServiceLoader.load(MQTTHandler.class).spliterator(), false)
                .sorted(Comparator.comparingInt(MQTTHandler::getPriority).reversed())
                .forEach(handler -> {
                    try {
                        handler.stop();
                    } catch (Exception e) {
                        LOG.log(WARNING, "MQTT custom handler threw an exception whilst stopping: handler=" + handler.getName(), e);
                    }
                });
    }

    @Override
    public void afterCreateConnection(RemotingConnection connection) throws ActiveMQException {

        // MQTT seems to only use failure callback even if closed gracefully (need to look at the exception for details)
        connection.addFailureListener(new FailureListener() {
            @Override
            public void connectionFailed(ActiveMQException exception, boolean failedOver) {
                connectionFailed(exception, failedOver, null);
            }

            @Override
            public void connectionFailed(ActiveMQException exception, boolean failedOver, String scaleDownTargetNodeID) {
                // TODO: Force delete session (don't allow retained/durable sessions)

                connectionIDConnectionMap.remove(getConnectionIDString(connection));

                if (connection.getClientID() != null) {
                    RemotingConnection remotingConnection = clientIDConnectionMap.remove(connection.getClientID());
                    if (remotingConnection != null) {
                        disconnectedConnectionCache.put(connection.getClientID(), remotingConnection);
                    }
                }

                if (exception.getType() == ActiveMQExceptionType.REMOTE_DISCONNECT) {// Seems to be the type for graceful close of connection
                    // Notify handlers of connection close
                    LOG.log(DEBUG, () -> "Client disconnected: " + connectionToString(connection));
                    for (MQTTHandler handler : getCustomHandlers()) {
                        handler.onDisconnect(connection);
                    }
                } else {
                    // Notify handlers of connection failure
                    LOG.log(DEBUG, () -> "Client disconnected (failure=" + exception.getMessage() + "): " + connectionToString(connection));
                    for (MQTTHandler handler : getCustomHandlers()) {
                        handler.onConnectionLost(connection);
                    }
                }
            }
        });
    }

    // We do this here as connection plugin afterCreateConnection callback fires before client ID and subject are populated
    // For each connection multiple sessions are created but we only want to call custom handlers onConnect once
    @Override
    public void afterCreateSession(ServerSession session) throws ActiveMQException {
        RemotingConnection remotingConnection = session.getRemotingConnection();

        // Ignore internal connections or ones without a client ID
        if (remotingConnection == null || remotingConnection.getClientID() == null || remotingConnection.getTransportConnection() instanceof InVMConnection) {
            return;
        }

        String connectionID = getConnectionIDString(remotingConnection);
        clientIDConnectionMap.put(remotingConnection.getClientID(), remotingConnection);

        if (!connectionIDConnectionMap.containsKey(connectionID)) {
            LOG.log(DEBUG, () -> "Client connected: " + connectionToString(remotingConnection));
            connectionIDConnectionMap.put(connectionID, remotingConnection);
            for (MQTTHandler handler : getCustomHandlers()) {
                handler.onConnect(remotingConnection);
            }
        }
    }

    @Override
    public void afterDestroyConnection(RemotingConnection connection) throws ActiveMQException {
    }

    public void onSubscribe(RemotingConnection connection, String topicStr) {
        Topic topic = Topic.parse(topicStr);
        LOG.log(TRACE, () -> "onSubscribe '" + topicStr + "': " + connectionToString(connection));
        for (MQTTHandler handler : getCustomHandlers()) {
            if (handler.handlesTopic(topic)) {
                LOG.log(DEBUG, () -> "Client subscribed '" + topicStr + "': " + connectionToString(connection));
                handler.onSubscribe(connection, topic);
                break;
            }
        }
    }

    public void onUnsubscribe(RemotingConnection connection, String topicStr) {
        Topic topic = Topic.parse(topicStr);
        LOG.log(TRACE, () -> "onUnsubscribe '" + topicStr + "': " + connectionToString(connection));
        for (MQTTHandler handler : getCustomHandlers()) {
            if (handler.handlesTopic(topic)) {
                LOG.log(DEBUG, () -> "Client unsubscribed '" + topicStr + "': " + connectionToString(connection));
                handler.onUnsubscribe(connection, topic);
                break;
            }
        }
    }

    public Iterable getCustomHandlers() {
        return customHandlers;
    }

    public void processUserAssetLinkChange(String userID, List> changes) {
        if (TextUtil.isNullOrEmpty(userID)) {
            return;
        }

        // Check if user has any active connections
        Set userConnections = getUserConnections(userID);
        Subject subject = userConnections.stream().filter(connection -> connection.getSubject() != null).findFirst().map(RemotingConnection::getSubject).orElse(null);

        // Only notify handlers if subject is a restricted user
        if (subject != null && KeycloakIdentityProvider.getSecurityContext(subject).getToken().getRealmAccess().isUserInRole(Constants.RESTRICTED_USER_REALM_ROLE)) {
            LOG.log(TRACE, "User asset links modified for connected restricted user so passing to handlers to decide what to do: user=" + subject);
            // Pass to handlers to decide what to do
            userConnections.forEach(connection -> {
                for (MQTTHandler handler : customHandlers) {
                    connection.setSubject(subject);
                    handler.onUserAssetLinksChanged(connection, changes);
                }
            });
        }
    }

    /**
     * Get active connections for the specified user ID
     */
    public Set getUserConnections(String userID) {
        if (TextUtil.isNullOrEmpty(userID)) {
            return Collections.emptySet();
        }

        return server.getActiveMQServer().getRemotingService().getConnections().stream().filter(connection -> {
            Subject subject = connection.getSubject();
            String subjectID = KeycloakIdentityProvider.getSubjectId(subject);
            return userID.equals(subjectID);
        }).collect(Collectors.toSet());
    }

    protected void doForceDisconnect(RemotingConnection connection) {
        LOG.log(DEBUG, "Force disconnecting client connection: " + connectionToString(connection));
        connection.disconnect(false);
        ((SecurityStoreImpl)server.getActiveMQServer().getSecurityStore()).invalidateAuthorizationCache();
    }

    public boolean disconnectSession(String connectionID) {
        RemotingConnection connection = connectionIDConnectionMap.get(connectionID);
        if (connection != null) {
            LOG.log(DEBUG, "Force disconnecting client connection: " + connectionToString(connection));
            doForceDisconnect(connection);
            return true;
        }

        return false;
    }

    public WildcardConfiguration getWildcardConfiguration() {
        return wildcardConfiguration;
    }

    public static String getConnectionIDString(RemotingConnection connection) {
        if (connection == null) {
            return null;
        }

        Object ID = connection.getID();
        return ID instanceof ChannelId ? ((ChannelId) ID).asLongText() : ID.toString();
    }

    public static String connectionToString(RemotingConnection connection) {
        if (connection == null) {
            return "";
        }

        String username = null;
        Subject subject = connection.getSubject();

        if (subject != null) {
            username = getSubjectName(subject);
        }

        return "connection=" + connection.getRemoteAddress() + ", clientID=" + connection.getClientID() + ", subject=" + username;
    }

    public static String getSubjectName(Subject subject) {
        return subject.getPrincipals().stream().filter(principal -> principal instanceof UserPrincipal)
            .findFirst()
            .map(Principal::getName)
            .orElse(KeycloakIdentityProvider.getSubjectNameAndRealm(subject));
    }

    public RemotingConnection getConnectionFromClientID(String clientID) {
        if (TextUtil.isNullOrEmpty(clientID)) {
            return null;
        }

        // This logic is needed because the connection clientID isn't populated when afterCreateConnection is called
        RemotingConnection connection = clientIDConnectionMap.get(clientID);

        if (connection == null) {
            // Try and find the client ID from the sessions
            for (RemotingConnection remotingConnection : server.getActiveMQServer().getRemotingService().getConnections()) {
                if (Objects.equals(clientID, remotingConnection.getClientID())) {
                    connection = remotingConnection;
                    clientIDConnectionMap.put(clientID, connection);
                    break;
                }
            }
        }

        if (connection == null) {
            // Look in the recently disconnected cache
            connection = disconnectedConnectionCache.getIfPresent(clientID);
        }

        return connection;
    }

    protected void notifyConnectionAuthenticated(RemotingConnection connection) {
        if (connection.getSubject() != null) {
            // Notify handlers that connection authenticated
            LOG.log(DEBUG, "Client connection authenticated: " + connectionToString(connection));
            for (MQTTHandler handler : getCustomHandlers()) {
                handler.onConnectionAuthenticated(connection);
            }
        }
    }

    /**
     * Create a client session for communicating with the broker
     */
    protected ClientSession createSession() throws Exception {

        ClientSessionInternal session = null;

        try {
            String internalClientID = UniqueIdentifierGenerator.generateId("Internal client");
            session = (ClientSessionInternal) sessionFactory.createSession(null, null, false, true, true, true, serverLocator.getAckBatchSize(), internalClientID);
            session.addMetaData(ClientSession.JMS_SESSION_IDENTIFIER_PROPERTY, "Internal session");
            ServerSession serverSession = server.getActiveMQServer().getSessionByID(session.getName());
            serverSession.disableSecurity();
            session.start();
        } catch (Exception e) {
            LOG.log(WARNING, "Failed to create MQTT client session", e);
        }

        return session;
    }

    protected WildcardConfiguration getServerWildcardConfiguration() {
        return server.getConfiguration().getWildcardConfiguration();
    }

    public void authenticateConnection(RemotingConnection connection, String realm, String username, String password) {
        if (connection != null) {
            connection.setSubject(null); // Clear existing subject
            securityManager.authenticate(realm + ":" + username, password, connection, null);
            notifyConnectionAuthenticated(connection);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy