Removed all the old ProtocolProcessor stuff

This commit is contained in:
andsel
2018-11-08 22:18:11 +01:00
parent 24e9d9f83c
commit e10619813a
79 changed files with 483 additions and 6043 deletions
@@ -76,4 +76,7 @@ final class Authorizator {
return policy.canWrite(topic, user, client);
}
boolean canRead(Topic topic, String user, String client) {
return policy.canRead(topic, user, client);
}
}
@@ -128,7 +128,7 @@ public class InflightResender extends ChannelDuplexHandler {
// Avoid the case where destroy() is called before scheduling timeouts.
// See: https://github.com/netty/netty/issues/143
if (LOG.isDebugEnabled()) {
LOG.debug("Initializing autoflush handler. MqttClientId = {}.", NettyUtils.clientID(ctx.channel()));
LOG.debug("Initializing autoflush handler on channel {}", ctx.channel());
}
switch (state) {
case 1:
@@ -153,9 +153,9 @@ public class InflightResender extends ChannelDuplexHandler {
}
}
private void resendNotAcked(ChannelHandlerContext ctx/* , IdleStateEvent evt */) throws Exception {
if (LOG.isDebugEnabled()) {
LOG.debug("Flushing idle Netty channel. MqttClientId = {}.", NettyUtils.clientID(ctx.channel()));
private void resendNotAcked(ChannelHandlerContext ctx/* , IdleStateEvent evt */) {
if (LOG.isTraceEnabled()) {
LOG.trace("Flushing idle Netty channel {} for clientId: {}", ctx.channel(), NettyUtils.clientID(ctx.channel()));
}
ctx.fireUserEventTriggered(new ResendNotAckedPublishes());
}
@@ -18,6 +18,7 @@ package io.moquette.broker;
import io.moquette.BrokerConstants;
import io.moquette.server.config.IConfig;
import io.moquette.server.netty.AutoFlushHandler;
import io.moquette.server.netty.BugSnagErrorsHandler;
import io.moquette.server.netty.MoquetteIdleTimeoutHandler;
import io.moquette.server.netty.metrics.*;
@@ -56,7 +57,7 @@ import java.util.concurrent.TimeUnit;
import static io.moquette.BrokerConstants.*;
import static io.netty.channel.ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE;
public class NewNettyAcceptor {
class NewNettyAcceptor {
private static final String MQTT_SUBPROTOCOL_CSV_LIST = "mqtt, mqttv3.1, mqttv3.1.1";
@@ -215,6 +216,7 @@ public class NewNettyAcceptor {
pipeline.addLast("bugsnagCatcher", errorsCather.get());
}
pipeline.addFirst("bytemetrics", new BytesMetricsHandler(m_bytesMetricsCollector));
pipeline.addLast("autoflush", new AutoFlushHandler(1, TimeUnit.SECONDS));
pipeline.addLast("decoder", new MqttDecoder(maxBytesInMessage));
pipeline.addLast("encoder", MqttEncoder.INSTANCE);
pipeline.addLast("metrics", new MessageMetricsHandler(m_metricsCollector));
@@ -255,6 +257,7 @@ public class NewNettyAcceptor {
pipeline.addFirst("idleStateHandler", new IdleStateHandler(nettyChannelTimeoutSeconds, 0, 0));
pipeline.addAfter("idleStateHandler", "idleEventHandler", timeoutHandler);
pipeline.addFirst("bytemetrics", new BytesMetricsHandler(m_bytesMetricsCollector));
pipeline.addLast("autoflush", new AutoFlushHandler(1, TimeUnit.SECONDS));
pipeline.addLast("decoder", new MqttDecoder(maxBytesInMessage));
pipeline.addLast("encoder", MqttEncoder.INSTANCE);
pipeline.addLast("metrics", new MessageMetricsHandler(m_metricsCollector));
@@ -291,6 +294,7 @@ public class NewNettyAcceptor {
pipeline.addAfter("idleStateHandler", "idleEventHandler", timeoutHandler);
// pipeline.addLast("logger", new LoggingHandler("Netty", LogLevel.ERROR));
pipeline.addFirst("bytemetrics", new BytesMetricsHandler(m_bytesMetricsCollector));
pipeline.addLast("autoflush", new AutoFlushHandler(1, TimeUnit.SECONDS));
pipeline.addLast("decoder", new MqttDecoder(maxBytesInMessage));
pipeline.addLast("encoder", MqttEncoder.INSTANCE);
pipeline.addLast("metrics", new MessageMetricsHandler(m_metricsCollector));
@@ -330,6 +334,7 @@ public class NewNettyAcceptor {
pipeline.addFirst("idleStateHandler", new IdleStateHandler(nettyChannelTimeoutSeconds, 0, 0));
pipeline.addAfter("idleStateHandler", "idleEventHandler", timeoutHandler);
pipeline.addFirst("bytemetrics", new BytesMetricsHandler(m_bytesMetricsCollector));
pipeline.addLast("autoflush", new AutoFlushHandler(1, TimeUnit.SECONDS));
pipeline.addLast("decoder", new MqttDecoder(maxBytesInMessage));
pipeline.addLast("encoder", MqttEncoder.INSTANCE);
pipeline.addLast("metrics", new MessageMetricsHandler(m_metricsCollector));
@@ -104,7 +104,7 @@ public class NewNettyMQTTHandler extends ChannelInboundHandlerAdapter {
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (evt instanceof InflightResender.ResendNotAckedPublishes) {
final MQTTConnection mqttConnection = mqttConnection(ctx.channel());
mqttConnection.resendNotAckedPublishes();
@@ -202,6 +202,12 @@ class PostOffice {
if (isSessionPresent) {
LOG.debug("Sending PUBLISH message to active subscriber CId: {}, topicFilter: {}, qos: {}",
sub.getClientId(), sub.getTopicFilter(), qos);
//TODO determine the user bounded to targetSession
if (!authorizator.canRead(topic, "TODO", sub.getClientId())) {
LOG.debug("Authorizator prohibit Client {} to be notified on {}", sub.getClientId(), topic);
return;
}
// we need to retain because duplicate only copy r/w indexes and don't retain() causing refCnt = 0
ByteBuf payload = origPayload.retainedDuplicate();
targetSession.sendPublishOnSessionAtQos(topic, qos, payload);
@@ -218,10 +224,17 @@ class PostOffice {
* First phase of a publish QoS2 protocol, sent by publisher to the broker. Publish to all interested
* subscribers.
*/
void receivedPublishQos2(MQTTConnection connection, MqttPublishMessage mqttPublishMessage) {
void receivedPublishQos2(MQTTConnection connection, MqttPublishMessage mqttPublishMessage, String username) {
LOG.trace("Processing PUBREL message on connection: {}", connection);
final Topic topic = new Topic(mqttPublishMessage.variableHeader().topicName());
final ByteBuf payload = mqttPublishMessage.payload();
final String clientId = connection.getClientId();
if (!authorizator.canWrite(topic, username, clientId)) {
LOG.error("MQTT client is not authorized to publish on topic. CId={}, topic: {}", clientId, topic);
return;
}
publish2Subscribers(payload, topic, EXACTLY_ONCE);
final boolean retained = mqttPublishMessage.fixedHeader().isRetain();
@@ -235,7 +248,6 @@ class PostOffice {
}
String clientID = connection.getClientId();
String username = NettyUtils.userName(connection.channel);
interceptor.notifyTopicPublished(mqttPublishMessage, clientID, username);
}
@@ -7,7 +7,6 @@ import io.moquette.server.DefaultMoquetteSslContextCreator;
import io.moquette.server.config.*;
import io.moquette.spi.ISessionsStore;
import io.moquette.spi.impl.BrokerInterceptor;
import io.moquette.spi.impl.SessionsRepository;
import io.moquette.spi.impl.security.*;
import io.moquette.spi.impl.subscriptions.CTrieSubscriptionDirectory;
import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory;
@@ -150,11 +149,11 @@ public class Server {
// TODO user real implementation DBG
MemoryStorageService memStorage = new MemoryStorageService(null, null);
ISessionsStore sessionStore = memStorage.sessionsStore();
SessionsRepository sessionsRepository = new SessionsRepository(sessionStore, null);
ISubscriptionsRepository subscriptionsRepository = new MemorySubscriptionsRepository();
// DBG
ISubscriptionsDirectory subscriptions = new CTrieSubscriptionDirectory();
subscriptions.init(sessionsRepository);
subscriptions.init(subscriptionsRepository);
SessionRegistry sessions = new SessionRegistry(subscriptions, interceptor);
dispatcher = new PostOffice(subscriptions, authorizatorPolicy, new MemoryRetainedRepository(), sessions,
interceptor);
@@ -288,13 +287,13 @@ public class Server {
* @throws IllegalStateException if the server is not yet started
*/
public void internalPublish(MqttPublishMessage msg, final String clientId) {
final int messageID = msg.variableHeader().messageId();
final int messageID = msg.variableHeader().packetId();
if (!initialized) {
LOG.error("Moquette is not started, internal message cannot be published. CId={}, messageId={}", clientId,
messageID);
LOG.error("Moquette is not started, internal message cannot be published. CId: {}, messageId: {}", clientId,
messageID);
throw new IllegalStateException("Can't publish on a server is not yet started");
}
LOG.debug("Publishing message. CId={}, messageId={}", clientId, messageID);
LOG.trace("Internal publishing message CId: {}, messageId: {}", clientId, messageID);
dispatcher.internalPublish(msg);
}
@@ -302,7 +301,6 @@ public class Server {
LOG.info("Unbinding server from the configured ports");
acceptor.close();
LOG.trace("Stopping MQTT protocol processor");
// m_processorBootstrapper.shutdown();
initialized = false;
// calling shutdown() does not actually stop tasks that are not cancelled,
@@ -165,7 +165,7 @@ class Session {
inflightSlots.incrementAndGet();
if (canSkipQueue()) {
inflightSlots.decrementAndGet();
int pubRelPacketId = mqttConnection.nextPacketId();
int pubRelPacketId = packetId/*mqttConnection.nextPacketId()*/;
inflightWindow.put(pubRelPacketId, new SessionRegistry.PubRelMarker());
inflightTimeouts.add(new InFlightPacket(pubRelPacketId, FLIGHT_BEFORE_RESEND_MS));
MqttMessage pubRel = MQTTConnection.pubrel(pubRelPacketId);
@@ -262,19 +262,33 @@ class Session {
Collection<InFlightPacket> expired = new ArrayList<>(INFLIGHT_WINDOW_SIZE);
inflightTimeouts.drainTo(expired);
debugLogPacketIds(expired);
for (InFlightPacket notAckPacketId : expired) {
if (inflightWindow.containsKey(notAckPacketId.packetId)) {
final SessionRegistry.PublishedMessage msg = (SessionRegistry.PublishedMessage) inflightWindow.get(notAckPacketId.packetId);
final Topic topic = msg.topic;
final MqttQoS qos = msg.publishingQos;
final ByteBuf payload = msg.payload;
MqttPublishMessage publishMsg = publishNotRetainedDuplicated(notAckPacketId, topic, qos, payload);
final ByteBuf copiedPayload = payload.retainedDuplicate();
MqttPublishMessage publishMsg = publishNotRetainedDuplicated(notAckPacketId, topic, qos, copiedPayload);
mqttConnection.sendPublish(publishMsg);
}
}
}
private void debugLogPacketIds(Collection<InFlightPacket> expired) {
if (!LOG.isDebugEnabled() || expired.isEmpty()) {
return;
}
StringBuilder sb = new StringBuilder();
for (InFlightPacket packet : expired) {
sb.append(packet.packetId).append(", ");
}
LOG.debug("Resending {} in flight packets [{}]", expired.size(), sb);
}
private MqttPublishMessage publishNotRetainedDuplicated(InFlightPacket notAckPacketId, Topic topic, MqttQoS qos, ByteBuf payload) {
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, true, qos, false, 0);
MqttPublishVariableHeader varHeader = new MqttPublishVariableHeader(topic.toString(), notAckPacketId.packetId);
@@ -1,56 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.connections;
import io.moquette.server.ConnectionDescriptor;
import java.util.Collection;
import java.util.Optional;
/**
* Repository for active connections
*/
public interface IConnectionsManager {
/**
* Returns the number of physical connections
*
* @return
*/
int countActiveConnections();
Optional<ConnectionDescriptor> lookupDescriptor(String clientID);
ConnectionDescriptor addConnection(ConnectionDescriptor descriptor);
boolean removeConnection(ConnectionDescriptor descriptor);
/**
* Determines weather a MQTT client is connected to the broker.
*
* @param clientID
* @return
*/
boolean isConnected(String clientID);
/**
* Returns the identifiers of the MQTT clients that are connected to the broker.
*
* @return
*/
Collection<String> getConnectedClientIds();
}
@@ -1,147 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.server;
import io.moquette.server.netty.NettyUtils;
import io.moquette.server.netty.metrics.BytesMetrics;
import io.moquette.server.netty.metrics.BytesMetricsHandler;
import io.moquette.server.netty.metrics.MessageMetrics;
import io.moquette.server.netty.metrics.MessageMetricsHandler;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.SocketAddress;
import java.util.concurrent.atomic.AtomicReference;
import static io.netty.channel.ChannelFutureListener.CLOSE_ON_FAILURE;
import static io.netty.channel.ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE;
/**
* Value object to maintain the information of single connection, like ClientID, Channel, and clean
* session flag.
*/
public class ConnectionDescriptor {
private static final Logger LOG = LoggerFactory.getLogger(ConnectionDescriptor.class);
public enum ConnectionState {
// Connection states
DISCONNECTED,
INIT_SESSION,
SENDACK,
MESSAGES_REPUBLISHED,
ESTABLISHED,
// Disconnection states
SUBSCRIPTIONS_REMOVED,
MESSAGES_DROPPED,
INTERCEPTORS_NOTIFIED;
}
public final String clientID;
private final Channel channel;
public final boolean cleanSession;
private final AtomicReference<ConnectionState> channelState = new AtomicReference<>(ConnectionState.DISCONNECTED);
public ConnectionDescriptor(String clientID, Channel session, boolean cleanSession) {
this.clientID = clientID;
this.channel = session;
this.cleanSession = cleanSession;
}
public void writeAndFlush(Object payload) {
this.channel.writeAndFlush(payload).addListener(FIRE_EXCEPTION_ON_FAILURE);
}
public void writeAndFlush(Object payload, ChannelFutureListener listener) {
this.channel.writeAndFlush(payload).addListener(listener);
}
public boolean doesNotUseChannel(Channel channel) {
return !(this.channel.equals(channel));
}
public boolean close() {
LOG.debug("Closing connection descriptor. MqttClientId = {}.", clientID);
final boolean success = assignState(ConnectionState.INTERCEPTORS_NOTIFIED, ConnectionState.DISCONNECTED);
if (!success) {
return false;
}
this.channel.close().addListener(CLOSE_ON_FAILURE);
return true;
}
public String getUsername() {
return NettyUtils.userName(this.channel);
}
public void abort() {
LOG.info("Closing connection descriptor clientId = {}.", clientID);
this.channel.close().addListener(CLOSE_ON_FAILURE);
}
public boolean assignState(ConnectionState expected, ConnectionState newState) {
LOG.debug("Updating state of connection descriptor. MqttClientId = {}, expectedState = {}, newState = {}.",
clientID, expected, newState);
boolean retval = channelState.compareAndSet(expected, newState);
if (!retval) {
LOG.error("Unable to update state of connection descriptor. CId = {}, expectedState = {}, newState = {}.",
clientID, expected, newState);
}
return retval;
}
public SocketAddress getRemoteAddress() {
return this.channel.remoteAddress();
}
@Override
public String toString() {
return "ConnectionDescriptor{" + "clientID=" + clientID + ", removeTemporaryQoS2=" + cleanSession + ", state="
+ channelState.get() + '}';
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
ConnectionDescriptor that = (ConnectionDescriptor) o;
if (clientID != null ? !clientID.equals(that.clientID) : that.clientID != null)
return false;
return !(channel != null ? !channel.equals(that.channel) : that.channel != null);
}
public BytesMetrics getBytesMetrics() {
return BytesMetricsHandler.getBytesMetrics(channel);
}
public MessageMetrics getMessageMetrics() {
return MessageMetricsHandler.getMessageMetrics(channel);
}
@Override
public int hashCode() {
int result = clientID != null ? clientID.hashCode() : 0;
result = 31 * result + (channel != null ? channel.hashCode() : 0);
return result;
}
}
@@ -1,79 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.server;
import io.moquette.connections.IConnectionsManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class ConnectionDescriptorStore implements IConnectionsManager {
private static final Logger LOG = LoggerFactory.getLogger(ConnectionDescriptorStore.class);
private final ConcurrentMap<String, ConnectionDescriptor> connectionDescriptors;
public ConnectionDescriptorStore() {
this.connectionDescriptors = new ConcurrentHashMap<>();
}
@Override
public Optional<ConnectionDescriptor> lookupDescriptor(String clientID) {
if (clientID == null) {
return Optional.empty();
}
ConnectionDescriptor descriptor = connectionDescriptors.get(clientID);
if (descriptor == null) {
/*
* If the client has just disconnected, its connection descriptor will be null. We
* don't have to make the broker crash: we'll just discard the PUBACK message.
*/
return Optional.empty();
}
return Optional.of(descriptor);
}
@Override
public ConnectionDescriptor addConnection(ConnectionDescriptor descriptor) {
return connectionDescriptors.putIfAbsent(descriptor.clientID, descriptor);
}
@Override
public boolean removeConnection(ConnectionDescriptor descriptor) {
return connectionDescriptors.remove(descriptor.clientID, descriptor);
}
@Override
public boolean isConnected(String clientID) {
return connectionDescriptors.containsKey(clientID);
}
@Override
public int countActiveConnections() {
return connectionDescriptors.size();
}
@Override
public Collection<String> getConnectedClientIds() {
return connectionDescriptors.keySet();
}
}
@@ -1,273 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.server;
import io.moquette.BrokerConstants;
import io.moquette.connections.IConnectionsManager;
import io.moquette.interception.InterceptHandler;
import io.moquette.server.config.*;
import io.moquette.server.netty.NettyAcceptor;
import io.moquette.spi.impl.ProtocolProcessor;
import io.moquette.spi.impl.ProtocolProcessorBootstrapper;
import io.moquette.spi.impl.subscriptions.Subscription;
import io.moquette.spi.security.IAuthenticator;
import io.moquette.spi.security.IAuthorizatorPolicy;
import io.moquette.spi.security.ISslContextCreator;
import io.netty.handler.codec.mqtt.MqttPublishMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import static io.moquette.logging.LoggingUtils.getInterceptorIds;
/**
* Launch a configured version of the server.
*
* @deprecated use io.moquette.broker.Server
*/
@Deprecated
public class Server {
private static final Logger LOG = LoggerFactory.getLogger(Server.class);
private ServerAcceptor m_acceptor;
private volatile boolean m_initialized;
private ProtocolProcessor m_processor;
private ProtocolProcessorBootstrapper m_processorBootstrapper;
private ScheduledExecutorService scheduler;
public static void main(String[] args) throws IOException {
final Server server = new Server();
server.startServer();
System.out.println("Server started, version 0.12-SNAPSHOT");
//Bind a shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(server::stopServer));
}
/**
* Starts Moquette bringing the configuration from the file located at m_config/moquette.conf
*
* @throws IOException in case of any IO error.
*/
public void startServer() throws IOException {
File defaultConfigurationFile = defaultConfigFile();
LOG.info("Starting Moquette server. Configuration file path={}", defaultConfigurationFile.getAbsolutePath());
IResourceLoader filesystemLoader = new FileResourceLoader(defaultConfigurationFile);
final IConfig config = new ResourceLoaderConfig(filesystemLoader);
startServer(config);
}
private static File defaultConfigFile() {
String configPath = System.getProperty("moquette.path", null);
return new File(configPath, IConfig.DEFAULT_CONFIG);
}
/**
* Starts Moquette bringing the configuration from the given file
*
* @param configFile text file that contains the configuration.
* @throws IOException in case of any IO Error.
*/
public void startServer(File configFile) throws IOException {
LOG.info("Starting Moquette server. Configuration file path={}", configFile.getAbsolutePath());
IResourceLoader filesystemLoader = new FileResourceLoader(configFile);
final IConfig config = new ResourceLoaderConfig(filesystemLoader);
startServer(config);
}
/**
* Starts the server with the given properties.
* <p>
* Its suggested to at least have the following properties:
* <ul>
* <li>port</li>
* <li>password_file</li>
* </ul>
*
* @param configProps the properties map to use as configuration.
* @throws IOException in case of any IO Error.
*/
public void startServer(Properties configProps) throws IOException {
LOG.debug("Starting Moquette server using properties object");
final IConfig config = new MemoryConfig(configProps);
startServer(config);
}
/**
* Starts Moquette bringing the configuration files from the given Config implementation.
*
* @param config the configuration to use to start the broker.
* @throws IOException in case of any IO Error.
*/
public void startServer(IConfig config) throws IOException {
LOG.debug("Starting Moquette server using IConfig instance");
startServer(config, null);
}
/**
* Starts Moquette with config provided by an implementation of IConfig class and with the set
* of InterceptHandler.
*
* @param config the configuration to use to start the broker.
* @param handlers the handlers to install in the broker.
* @throws IOException in case of any IO Error.
*/
public void startServer(IConfig config, List<? extends InterceptHandler> handlers) throws IOException {
LOG.debug("Starting moquette server using IConfig instance and intercept handlers");
startServer(config, handlers, null, null, null);
}
public void startServer(IConfig config, List<? extends InterceptHandler> handlers, ISslContextCreator sslCtxCreator,
IAuthenticator authenticator, IAuthorizatorPolicy authorizator) throws IOException {
final long start = System.currentTimeMillis();
if (handlers == null) {
handlers = Collections.emptyList();
}
LOG.trace("Starting Moquette Server. MQTT message interceptors={}", getInterceptorIds(handlers));
scheduler = Executors.newScheduledThreadPool(1);
final String handlerProp = System.getProperty(BrokerConstants.INTERCEPT_HANDLER_PROPERTY_NAME);
if (handlerProp != null) {
config.setProperty(BrokerConstants.INTERCEPT_HANDLER_PROPERTY_NAME, handlerProp);
}
final String persistencePath = config.getProperty(BrokerConstants.PERSISTENT_STORE_PROPERTY_NAME);
LOG.debug("Configuring Using persistent store file, path={}", persistencePath);
m_processorBootstrapper = new ProtocolProcessorBootstrapper();
final ProtocolProcessor processor = m_processorBootstrapper.init(config, handlers, authenticator, authorizator,
this);
LOG.debug("Initialized MQTT protocol processor");
if (sslCtxCreator == null) {
LOG.info("Using default SSL context creator");
sslCtxCreator = new DefaultMoquetteSslContextCreator(config);
}
m_acceptor = new NettyAcceptor();
m_acceptor.initialize(processor, config, sslCtxCreator);
m_processor = processor;
final long startTime = System.currentTimeMillis() - start;
LOG.info("Moquette server has been started successfully in {} ms", startTime);
m_initialized = true;
}
/**
* Use the broker to publish a message. It's intended for embedding applications. It can be used
* only after the server is correctly started with startServer.
*
* @param msg the message to forward.
* @param clientId the id of the sending server.
* @throws IllegalStateException if the server is not yet started
*/
public void internalPublish(MqttPublishMessage msg, final String clientId) {
final int messageID = msg.variableHeader().messageId();
if (!m_initialized) {
LOG.error("Moquette is not started, internal message cannot be published. CId={}, messageId={}", clientId,
messageID);
throw new IllegalStateException("Can't publish on a server is not yet started");
}
LOG.debug("Publishing message. CId={}, messageId={}", clientId, messageID);
m_processor.internalPublish(msg, clientId);
}
public void stopServer() {
LOG.info("Unbinding server from the configured ports");
m_acceptor.close();
LOG.trace("Stopping MQTT protocol processor");
m_processorBootstrapper.shutdown();
m_initialized = false;
// calling shutdown() does not actually stop tasks that are not cancelled,
// and SessionsRepository does not stop its tasks. Thus shutdownNow().
scheduler.shutdownNow();
LOG.info("Moquette server has been stopped.");
}
/**
* SPI method used by Broker embedded applications to get list of subscribers. Returns null if
* the broker is not started.
*
* @return list of subscriptions.
*/
public List<Subscription> getSubscriptions() {
if (m_processorBootstrapper == null) {
return null;
}
return m_processorBootstrapper.getSubscriptions();
}
/**
* SPI method used by Broker embedded applications to add intercept handlers.
*
* @param interceptHandler the handler to add.
*/
public void addInterceptHandler(InterceptHandler interceptHandler) {
if (!m_initialized) {
LOG.error("Moquette is not started, MQTT message interceptor cannot be added. InterceptorId={}",
interceptHandler.getID());
throw new IllegalStateException("Can't register interceptors on a server that is not yet started");
}
LOG.info("Adding MQTT message interceptor. InterceptorId={}", interceptHandler.getID());
m_processor.addInterceptHandler(interceptHandler);
}
/**
* SPI method used by Broker embedded applications to remove intercept handlers.
*
* @param interceptHandler the handler to remove.
*/
public void removeInterceptHandler(InterceptHandler interceptHandler) {
if (!m_initialized) {
LOG.error("Moquette is not started, MQTT message interceptor cannot be removed. InterceptorId={}",
interceptHandler.getID());
throw new IllegalStateException("Can't deregister interceptors from a server that is not yet started");
}
LOG.info("Removing MQTT message interceptor. InterceptorId={}", interceptHandler.getID());
m_processor.removeInterceptHandler(interceptHandler);
}
/**
* Returns the connections manager of this broker.
*
* @return IConnectionsManager the instance used bt the broker.
*/
public IConnectionsManager getConnectionsManager() {
return m_processorBootstrapper.getConnectionDescriptors();
}
public ProtocolProcessor getProcessor() {
return m_processor;
}
public ScheduledExecutorService getScheduler() {
return scheduler;
}
}
@@ -1,29 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.server;
import java.io.IOException;
import io.moquette.server.config.IConfig;
import io.moquette.spi.impl.ProtocolProcessor;
import io.moquette.spi.security.ISslContextCreator;
public interface ServerAcceptor {
void initialize(ProtocolProcessor processor, IConfig props, ISslContextCreator sslCtxCreator) throws IOException;
void close();
}
@@ -49,7 +49,7 @@ public class AutoFlushHandler extends ChannelDuplexHandler {
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
public void handlerAdded(ChannelHandlerContext ctx) {
if (ctx.channel().isActive() && ctx.channel().isRegistered()) {
// channelActive() event has been fired already, which means this.channelActive() will
// not be invoked. We have to initialize here instead.
@@ -61,7 +61,7 @@ public class AutoFlushHandler extends ChannelDuplexHandler {
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
public void handlerRemoved(ChannelHandlerContext ctx) {
destroy();
}
@@ -111,7 +111,7 @@ public class AutoFlushHandler extends ChannelDuplexHandler {
// Avoid the case where destroy() is called before scheduling timeouts.
// See: https://github.com/netty/netty/issues/143
if (LOG.isDebugEnabled()) {
LOG.debug("Initializing autoflush handler. MqttClientId = {}.", NettyUtils.clientID(ctx.channel()));
LOG.debug("Initializing autoflush handler on channel {} Cid: {}", ctx.channel(), NettyUtils.clientID(ctx.channel()));
}
switch (state) {
case 1:
@@ -144,10 +144,10 @@ public class AutoFlushHandler extends ChannelDuplexHandler {
* @throws Exception
* in case of any IO error.
*/
protected void channelIdle(ChannelHandlerContext ctx/* , IdleStateEvent evt */) throws Exception {
protected void channelIdle(ChannelHandlerContext ctx/* , IdleStateEvent evt */) {
// ctx.fireUserEventTriggered(evt);
if (LOG.isDebugEnabled()) {
LOG.debug("Flushing idle Netty channel. MqttClientId = {}.", NettyUtils.clientID(ctx.channel()));
LOG.debug("Flushing idle Netty channel {} Cid: {}", ctx.channel(), NettyUtils.clientID(ctx.channel()));
}
ctx.channel().flush();
}
@@ -38,15 +38,14 @@ public class MoquetteIdleTimeoutHandler extends ChannelDuplexHandler {
if (e == IdleState.READER_IDLE) {
LOG.info("Firing channel inactive event. MqttClientId = {}.", NettyUtils.clientID(ctx.channel()));
// fire a channelInactive to trigger publish of Will
ctx.fireChannelInactive();
//??
// ctx.fireChannelInactive();
ctx.close().addListener(CLOSE_ON_FAILURE);
}
} else {
if (LOG.isDebugEnabled()) {
LOG.debug(
"Firing Netty event. MqttClientId = {}, eventClass = {}.",
NettyUtils.clientID(ctx.channel()),
evt.getClass().getName());
if (LOG.isTraceEnabled()) {
LOG.trace("Firing Netty event CId = {}, eventClass = {}", NettyUtils.clientID(ctx.channel()),
evt.getClass().getName());
}
super.userEventTriggered(ctx, evt);
}
@@ -1,409 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.server.netty;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import io.moquette.BrokerConstants;
import io.moquette.server.ServerAcceptor;
import io.moquette.server.config.IConfig;
import io.moquette.server.netty.metrics.BytesMetrics;
import io.moquette.server.netty.metrics.BytesMetricsCollector;
import io.moquette.server.netty.metrics.BytesMetricsHandler;
import io.moquette.server.netty.metrics.DropWizardMetricsHandler;
import io.moquette.server.netty.metrics.MQTTMessageLogger;
import io.moquette.server.netty.metrics.MessageMetrics;
import io.moquette.server.netty.metrics.MessageMetricsCollector;
import io.moquette.server.netty.metrics.MessageMetricsHandler;
import io.moquette.spi.impl.ProtocolProcessor;
import io.moquette.spi.security.ISslContextCreator;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.MessageToMessageDecoder;
import io.netty.handler.codec.MessageToMessageEncoder;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.mqtt.MqttDecoder;
import io.netty.handler.codec.mqtt.MqttEncoder;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.concurrent.Future;
import javax.net.ssl.SSLEngine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static io.moquette.BrokerConstants.*;
import static io.netty.channel.ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE;
public class NettyAcceptor implements ServerAcceptor {
private static final String MQTT_SUBPROTOCOL_CSV_LIST = "mqtt, mqttv3.1, mqttv3.1.1";
static class WebSocketFrameToByteBufDecoder extends MessageToMessageDecoder<BinaryWebSocketFrame> {
@Override
protected void decode(ChannelHandlerContext chc, BinaryWebSocketFrame frame, List<Object> out)
throws Exception {
// convert the frame to a ByteBuf
ByteBuf bb = frame.content();
// System.out.println("WebSocketFrameToByteBufDecoder decode - " +
// ByteBufUtil.hexDump(bb));
bb.retain();
out.add(bb);
}
}
static class ByteBufToWebSocketFrameEncoder extends MessageToMessageEncoder<ByteBuf> {
@Override
protected void encode(ChannelHandlerContext chc, ByteBuf bb, List<Object> out) throws Exception {
// convert the ByteBuf to a WebSocketFrame
BinaryWebSocketFrame result = new BinaryWebSocketFrame();
// System.out.println("ByteBufToWebSocketFrameEncoder encode - " +
// ByteBufUtil.hexDump(bb));
result.content().writeBytes(bb);
out.add(result);
}
}
private abstract static class PipelineInitializer {
abstract void init(SocketChannel channel) throws Exception;
}
private static final Logger LOG = LoggerFactory.getLogger(NettyAcceptor.class);
EventLoopGroup m_bossGroup;
EventLoopGroup m_workerGroup;
BytesMetricsCollector m_bytesMetricsCollector = new BytesMetricsCollector();
MessageMetricsCollector m_metricsCollector = new MessageMetricsCollector();
private Optional<? extends ChannelInboundHandler> metrics;
private Optional<? extends ChannelInboundHandler> errorsCather;
private int nettySoBacklog;
private boolean nettySoReuseaddr;
private boolean nettyTcpNodelay;
private boolean nettySoKeepalive;
private int nettyChannelTimeoutSeconds;
private int maxBytesInMessage;
private Class<? extends ServerSocketChannel> channelClass;
@Override
public void initialize(ProtocolProcessor processor, IConfig props, ISslContextCreator sslCtxCreator) {
LOG.debug("Initializing Netty acceptor");
nettySoBacklog = props.intProp(BrokerConstants.NETTY_SO_BACKLOG_PROPERTY_NAME, 128);
nettySoReuseaddr = props.boolProp(BrokerConstants.NETTY_SO_REUSEADDR_PROPERTY_NAME, true);
nettyTcpNodelay = props.boolProp(BrokerConstants.NETTY_TCP_NODELAY_PROPERTY_NAME, true);
nettySoKeepalive = props.boolProp(BrokerConstants.NETTY_SO_KEEPALIVE_PROPERTY_NAME, true);
nettyChannelTimeoutSeconds = props.intProp(BrokerConstants.NETTY_CHANNEL_TIMEOUT_SECONDS_PROPERTY_NAME, 10);
maxBytesInMessage = props.intProp(BrokerConstants.NETTY_MAX_BYTES_PROPERTY_NAME,
BrokerConstants.DEFAULT_NETTY_MAX_BYTES_IN_MESSAGE);
boolean epoll = props.boolProp(BrokerConstants.NETTY_EPOLL_PROPERTY_NAME, false);
if (epoll) {
LOG.info("Netty is using Epoll");
m_bossGroup = new EpollEventLoopGroup();
m_workerGroup = new EpollEventLoopGroup();
channelClass = EpollServerSocketChannel.class;
} else {
LOG.info("Netty is using NIO");
m_bossGroup = new NioEventLoopGroup();
m_workerGroup = new NioEventLoopGroup();
channelClass = NioServerSocketChannel.class;
}
final NettyMQTTHandler mqttHandler = new NettyMQTTHandler(processor);
final boolean useFineMetrics = props.boolProp(METRICS_ENABLE_PROPERTY_NAME, false);
if (useFineMetrics) {
DropWizardMetricsHandler metricsHandler = new DropWizardMetricsHandler();
metricsHandler.init(props);
this.metrics = Optional.of(metricsHandler);
} else {
this.metrics = Optional.empty();
}
final boolean useBugSnag = props.boolProp(BUGSNAG_ENABLE_PROPERTY_NAME, false);
if (useBugSnag) {
BugSnagErrorsHandler bugSnagHandler = new BugSnagErrorsHandler();
bugSnagHandler.init(props);
this.errorsCather = Optional.of(bugSnagHandler);
} else {
this.errorsCather = Optional.empty();
}
initializePlainTCPTransport(mqttHandler, props);
initializeWebSocketTransport(mqttHandler, props);
String sslTcpPortProp = props.getProperty(BrokerConstants.SSL_PORT_PROPERTY_NAME);
String wssPortProp = props.getProperty(BrokerConstants.WSS_PORT_PROPERTY_NAME);
if (sslTcpPortProp != null || wssPortProp != null) {
SslContext sslContext = sslCtxCreator.initSSLContext();
if (sslContext == null) {
LOG.error("Can't initialize SSLHandler layer! Exiting, check your configuration of jks");
return;
}
initializeSSLTCPTransport(mqttHandler, props, sslContext);
initializeWSSTransport(mqttHandler, props, sslContext);
}
}
private void initFactory(String host, int port, String protocol, final PipelineInitializer pipeliner) {
LOG.debug("Initializing server. Protocol={}", protocol);
ServerBootstrap b = new ServerBootstrap();
b.group(m_bossGroup, m_workerGroup).channel(channelClass)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
pipeliner.init(ch);
}
})
.option(ChannelOption.SO_BACKLOG, nettySoBacklog)
.option(ChannelOption.SO_REUSEADDR, nettySoReuseaddr)
.childOption(ChannelOption.TCP_NODELAY, nettyTcpNodelay)
.childOption(ChannelOption.SO_KEEPALIVE, nettySoKeepalive);
try {
LOG.debug("Binding server. host={}, port={}", host, port);
// Bind and start to accept incoming connections.
ChannelFuture f = b.bind(host, port);
LOG.info("Server bound to host={}, port={}, protocol={}", host, port, protocol);
f.sync().addListener(FIRE_EXCEPTION_ON_FAILURE);
} catch (InterruptedException ex) {
LOG.error("An interruptedException was caught while initializing server. Protocol={}", protocol, ex);
}
}
private void initializePlainTCPTransport(NettyMQTTHandler handler, IConfig props) {
LOG.debug("Configuring TCP MQTT transport");
final MoquetteIdleTimeoutHandler timeoutHandler = new MoquetteIdleTimeoutHandler();
String host = props.getProperty(BrokerConstants.HOST_PROPERTY_NAME);
String tcpPortProp = props.getProperty(PORT_PROPERTY_NAME, DISABLED_PORT_BIND);
if (DISABLED_PORT_BIND.equals(tcpPortProp)) {
LOG.info("Property {} has been set to {}. TCP MQTT will be disabled", BrokerConstants.PORT_PROPERTY_NAME,
DISABLED_PORT_BIND);
return;
}
int port = Integer.parseInt(tcpPortProp);
initFactory(host, port, "TCP MQTT", new PipelineInitializer() {
@Override
void init(SocketChannel channel) {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addFirst("idleStateHandler", new IdleStateHandler(nettyChannelTimeoutSeconds, 0, 0));
pipeline.addAfter("idleStateHandler", "idleEventHandler", timeoutHandler);
// pipeline.addLast("logger", new LoggingHandler("Netty", LogLevel.ERROR));
if (errorsCather.isPresent()) {
pipeline.addLast("bugsnagCatcher", errorsCather.get());
}
pipeline.addFirst("bytemetrics", new BytesMetricsHandler(m_bytesMetricsCollector));
pipeline.addLast("decoder", new MqttDecoder(maxBytesInMessage));
pipeline.addLast("encoder", MqttEncoder.INSTANCE);
pipeline.addLast("metrics", new MessageMetricsHandler(m_metricsCollector));
pipeline.addLast("messageLogger", new MQTTMessageLogger());
if (metrics.isPresent()) {
pipeline.addLast("wizardMetrics", metrics.get());
}
pipeline.addLast("handler", handler);
}
});
}
private void initializeWebSocketTransport(final NettyMQTTHandler handler, IConfig props) {
LOG.debug("Configuring Websocket MQTT transport");
String webSocketPortProp = props.getProperty(WEB_SOCKET_PORT_PROPERTY_NAME, DISABLED_PORT_BIND);
if (DISABLED_PORT_BIND.equals(webSocketPortProp)) {
// Do nothing no WebSocket configured
LOG.info("Property {} has been setted to {}. Websocket MQTT will be disabled",
BrokerConstants.WEB_SOCKET_PORT_PROPERTY_NAME, DISABLED_PORT_BIND);
return;
}
int port = Integer.parseInt(webSocketPortProp);
final MoquetteIdleTimeoutHandler timeoutHandler = new MoquetteIdleTimeoutHandler();
String host = props.getProperty(BrokerConstants.HOST_PROPERTY_NAME);
initFactory(host, port, "Websocket MQTT", new PipelineInitializer() {
@Override
void init(SocketChannel channel) {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
pipeline.addLast("webSocketHandler",
new WebSocketServerProtocolHandler("/mqtt", MQTT_SUBPROTOCOL_CSV_LIST));
pipeline.addLast("ws2bytebufDecoder", new WebSocketFrameToByteBufDecoder());
pipeline.addLast("bytebuf2wsEncoder", new ByteBufToWebSocketFrameEncoder());
pipeline.addFirst("idleStateHandler", new IdleStateHandler(nettyChannelTimeoutSeconds, 0, 0));
pipeline.addAfter("idleStateHandler", "idleEventHandler", timeoutHandler);
pipeline.addFirst("bytemetrics", new BytesMetricsHandler(m_bytesMetricsCollector));
pipeline.addLast("decoder", new MqttDecoder(maxBytesInMessage));
pipeline.addLast("encoder", MqttEncoder.INSTANCE);
pipeline.addLast("metrics", new MessageMetricsHandler(m_metricsCollector));
pipeline.addLast("messageLogger", new MQTTMessageLogger());
pipeline.addLast("handler", handler);
}
});
}
private void initializeSSLTCPTransport(NettyMQTTHandler handler, IConfig props, SslContext sslContext) {
LOG.debug("Configuring SSL MQTT transport");
String sslPortProp = props.getProperty(SSL_PORT_PROPERTY_NAME, DISABLED_PORT_BIND);
if (DISABLED_PORT_BIND.equals(sslPortProp)) {
// Do nothing no SSL configured
LOG.info("Property {} has been set to {}. SSL MQTT will be disabled",
BrokerConstants.SSL_PORT_PROPERTY_NAME, DISABLED_PORT_BIND);
return;
}
int sslPort = Integer.parseInt(sslPortProp);
LOG.debug("Starting SSL on port {}", sslPort);
final MoquetteIdleTimeoutHandler timeoutHandler = new MoquetteIdleTimeoutHandler();
String host = props.getProperty(BrokerConstants.HOST_PROPERTY_NAME);
String sNeedsClientAuth = props.getProperty(BrokerConstants.NEED_CLIENT_AUTH, "false");
final boolean needsClientAuth = Boolean.valueOf(sNeedsClientAuth);
initFactory(host, sslPort, "SSL MQTT", new PipelineInitializer() {
@Override
void init(SocketChannel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast("ssl", createSslHandler(channel, sslContext, needsClientAuth));
pipeline.addFirst("idleStateHandler", new IdleStateHandler(nettyChannelTimeoutSeconds, 0, 0));
pipeline.addAfter("idleStateHandler", "idleEventHandler", timeoutHandler);
// pipeline.addLast("logger", new LoggingHandler("Netty", LogLevel.ERROR));
pipeline.addFirst("bytemetrics", new BytesMetricsHandler(m_bytesMetricsCollector));
pipeline.addLast("decoder", new MqttDecoder(maxBytesInMessage));
pipeline.addLast("encoder", MqttEncoder.INSTANCE);
pipeline.addLast("metrics", new MessageMetricsHandler(m_metricsCollector));
pipeline.addLast("messageLogger", new MQTTMessageLogger());
pipeline.addLast("handler", handler);
}
});
}
private void initializeWSSTransport(NettyMQTTHandler handler, IConfig props, SslContext sslContext) {
LOG.debug("Configuring secure websocket MQTT transport");
String sslPortProp = props.getProperty(WSS_PORT_PROPERTY_NAME, DISABLED_PORT_BIND);
if (DISABLED_PORT_BIND.equals(sslPortProp)) {
// Do nothing no SSL configured
LOG.info("Property {} has been set to {}. Secure websocket MQTT will be disabled",
BrokerConstants.WSS_PORT_PROPERTY_NAME, DISABLED_PORT_BIND);
return;
}
int sslPort = Integer.parseInt(sslPortProp);
final MoquetteIdleTimeoutHandler timeoutHandler = new MoquetteIdleTimeoutHandler();
String host = props.getProperty(BrokerConstants.HOST_PROPERTY_NAME);
String sNeedsClientAuth = props.getProperty(BrokerConstants.NEED_CLIENT_AUTH, "false");
final boolean needsClientAuth = Boolean.valueOf(sNeedsClientAuth);
initFactory(host, sslPort, "Secure websocket", new PipelineInitializer() {
@Override
void init(SocketChannel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast("ssl", createSslHandler(channel, sslContext, needsClientAuth));
pipeline.addLast("httpEncoder", new HttpResponseEncoder());
pipeline.addLast("httpDecoder", new HttpRequestDecoder());
pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
pipeline.addLast("webSocketHandler",
new WebSocketServerProtocolHandler("/mqtt", MQTT_SUBPROTOCOL_CSV_LIST));
pipeline.addLast("ws2bytebufDecoder", new WebSocketFrameToByteBufDecoder());
pipeline.addLast("bytebuf2wsEncoder", new ByteBufToWebSocketFrameEncoder());
pipeline.addFirst("idleStateHandler", new IdleStateHandler(nettyChannelTimeoutSeconds, 0, 0));
pipeline.addAfter("idleStateHandler", "idleEventHandler", timeoutHandler);
pipeline.addFirst("bytemetrics", new BytesMetricsHandler(m_bytesMetricsCollector));
pipeline.addLast("decoder", new MqttDecoder(maxBytesInMessage));
pipeline.addLast("encoder", MqttEncoder.INSTANCE);
pipeline.addLast("metrics", new MessageMetricsHandler(m_metricsCollector));
pipeline.addLast("messageLogger", new MQTTMessageLogger());
pipeline.addLast("handler", handler);
}
});
}
@SuppressWarnings("FutureReturnValueIgnored")
@Override
public void close() {
LOG.debug("Closing Netty acceptor...");
if (m_workerGroup == null || m_bossGroup == null) {
LOG.error("Netty acceptor is not initialized");
throw new IllegalStateException("Invoked close on an Acceptor that wasn't initialized");
}
Future<?> workerWaiter = m_workerGroup.shutdownGracefully();
Future<?> bossWaiter = m_bossGroup.shutdownGracefully();
/*
* We shouldn't raise an IllegalStateException if we are interrupted. If we did so, the
* broker is not shut down properly.
*/
LOG.info("Waiting for worker and boss event loop groups to terminate...");
try {
workerWaiter.await(10, TimeUnit.SECONDS);
bossWaiter.await(10, TimeUnit.SECONDS);
} catch (InterruptedException iex) {
LOG.warn("An InterruptedException was caught while waiting for event loops to terminate...");
}
if (!m_workerGroup.isTerminated()) {
LOG.warn("Forcing shutdown of worker event loop...");
m_workerGroup.shutdownGracefully(0L, 0L, TimeUnit.MILLISECONDS);
}
if (!m_bossGroup.isTerminated()) {
LOG.warn("Forcing shutdown of boss event loop...");
m_bossGroup.shutdownGracefully(0L, 0L, TimeUnit.MILLISECONDS);
}
MessageMetrics metrics = m_metricsCollector.computeMetrics();
BytesMetrics bytesMetrics = m_bytesMetricsCollector.computeMetrics();
LOG.info("Metrics messages[read={}, write={}] bytes[read={}, write={}]", metrics.messagesRead(),
metrics.messagesWrote(), bytesMetrics.readBytes(), bytesMetrics.wroteBytes());
}
private ChannelHandler createSslHandler(SocketChannel channel, SslContext sslContext, boolean needsClientAuth) {
SSLEngine sslEngine = sslContext.newEngine(
channel.alloc(),
channel.remoteAddress().getHostString(),
channel.remoteAddress().getPort());
sslEngine.setUseClientMode(false);
if (needsClientAuth) {
sslEngine.setNeedClientAuth(true);
}
return new SslHandler(sslEngine);
}
}
@@ -1,137 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.server.netty;
import io.moquette.spi.impl.ProtocolProcessor;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.mqtt.*;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static io.netty.channel.ChannelFutureListener.CLOSE_ON_FAILURE;
import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE;
import java.io.IOException;
@Sharable
public class NettyMQTTHandler extends ChannelInboundHandlerAdapter {
private static final Logger LOG = LoggerFactory.getLogger(NettyMQTTHandler.class);
private final ProtocolProcessor m_processor;
public NettyMQTTHandler(ProtocolProcessor processor) {
m_processor = processor;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object message) throws Exception {
MqttMessage msg = (MqttMessage) message;
if (msg.fixedHeader() == null) {
throw new IOException("Unknown packet");
}
MqttMessageType messageType = msg.fixedHeader().messageType();
LOG.debug("Processing MQTT message, type: {}", messageType);
try {
switch (messageType) {
case CONNECT:
m_processor.processConnect(ctx.channel(), (MqttConnectMessage) msg);
break;
case SUBSCRIBE:
m_processor.processSubscribe(ctx.channel(), (MqttSubscribeMessage) msg);
break;
case UNSUBSCRIBE:
m_processor.processUnsubscribe(ctx.channel(), (MqttUnsubscribeMessage) msg);
break;
case PUBLISH:
m_processor.processPublish(ctx.channel(), (MqttPublishMessage) msg);
break;
case PUBREC:
m_processor.processPubRec(ctx.channel(), msg);
break;
case PUBCOMP:
m_processor.processPubComp(ctx.channel(), msg);
break;
case PUBREL:
m_processor.processPubRel(ctx.channel(), msg);
break;
case DISCONNECT:
m_processor.processDisconnect(ctx.channel());
break;
case PUBACK:
m_processor.processPubAck(ctx.channel(), (MqttPubAckMessage) msg);
break;
case PINGREQ:
MqttFixedHeader pingHeader = new MqttFixedHeader(
MqttMessageType.PINGRESP,
false,
AT_MOST_ONCE,
false,
0);
MqttMessage pingResp = new MqttMessage(pingHeader);
ctx.writeAndFlush(pingResp).addListener(CLOSE_ON_FAILURE);
break;
default:
LOG.error("Unknown MessageType: {}", messageType);
break;
}
} catch (Throwable ex) {
//ctx.fireExceptionCaught(ex);
LOG.error("Error processing protocol message: {}", messageType, ex);
ctx.channel().close().addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
LOG.info("Closed client channel due to exception in processing");
}
});
} finally {
ReferenceCountUtil.release(msg);
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
String clientID = NettyUtils.clientID(ctx.channel());
if (clientID != null && !clientID.isEmpty()) {
LOG.info("Notifying connection lost event. MqttClientId = {}", clientID);
m_processor.processConnectionLost(clientID, ctx.channel());
}
ctx.close().addListener(CLOSE_ON_FAILURE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
LOG.error("Unexpected exception while processing MQTT message. Closing Netty channel. CId={}",
NettyUtils.clientID(ctx.channel()), cause);
ctx.close().addListener(CLOSE_ON_FAILURE);
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) {
if (ctx.channel().isWritable()) {
m_processor.notifyChannelWritable(ctx.channel());
}
ctx.fireChannelWritabilityChanged();
}
}
@@ -1,129 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi;
import io.moquette.spi.impl.WillMessage;
import io.moquette.spi.impl.subscriptions.Subscription;
import io.moquette.spi.impl.subscriptions.Topic;
import io.netty.handler.codec.mqtt.MqttConnectMessage;
import java.util.HashSet;
import java.util.Set;
/**
* Model a Session like describe on page 25 of MQTT 3.1.1 specification:
* The Session state in the Server consists of:
* <ul>
* <li>The existence of a Session, even if the rest of the Session state is empty.</li>
* <li>The Client’s subscriptions.</li>
* <li>QoS 1 and QoS 2 messages which have been sent to the Client, but have not been
* completely acknowledged.</li>
* <li>QoS 1 and QoS 2 messages pending transmission to the Client.</li>
* <li>QoS 2 messages which have been received from the Client, but have not been
* completely acknowledged.</li>
* <li>Optionally, QoS 0 messages pending transmission to the Client.</li>
* </ul>
*/
@Deprecated
public abstract class ClientSession {
public final String clientID;
protected Set<Subscription> subscriptions = new HashSet<>();
public ClientSession(String clientID) {
this.clientID = clientID;
}
public Subscription findSubscriptionByTopicFilter(Subscription matchingSub) {
for (Subscription sub : this.subscriptions) {
if (matchingSub.equals(sub)) {
return sub;
}
}
return null;
}
public Set<Subscription> getSubscriptions() {
return subscriptions;
}
public abstract boolean isCleanSession();
public abstract boolean subscribe(Subscription newSubscription);
public abstract void unsubscribeFrom(Topic topicFilter);
public abstract void disconnect();
public abstract void cleanSession();
protected abstract int nextPacketId();
public abstract IMessagesStore.StoredMessage inFlightAcknowledged(int messageID);
/**
* Mark the message as publish in flight.
*
* @return the packetID for the message in flight.
* */
public abstract int inFlightAckWaiting(IMessagesStore.StoredMessage msg);
public abstract IMessagesStore.StoredMessage completeReleasedPublish(int messageID);
/**
* Enqueue a message to be sent to the client.
*
* @param message
* the message to enqueue.
*/
public abstract void enqueue(IMessagesStore.StoredMessage message);
/**
* @return the next message queued to this client or null if empty.
* */
public abstract EnqueuedMessage poll();
public abstract boolean isEmptyQueue();
public abstract void dropQueue();
public abstract IMessagesStore.StoredMessage inboundInflight(int messageID);
public abstract void markAsInboundInflight(int messageID, IMessagesStore.StoredMessage msg);
public abstract void moveInFlightToSecondPhaseAckWaiting(int messageID, IMessagesStore.StoredMessage msg);
public abstract int getPendingPublishMessagesNo();
public abstract int countPubReleaseWaitingPubComplete();
public abstract int getInflightMessagesNo();
public abstract void wipeSubscriptions();
public abstract void storeWillMessage(MqttConnectMessage msg, String clientId);
public abstract void removeWill();
public abstract WillMessage willMessage();
@Override
public String toString() {
return getClass().getSimpleName() + "{clientID='" + clientID + "'}";
}
}
@@ -1,254 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi;
import io.moquette.spi.impl.WillMessage;
import io.moquette.spi.impl.subscriptions.Subscription;
import io.moquette.spi.impl.subscriptions.Topic;
import io.netty.handler.codec.mqtt.MqttConnectMessage;
import io.netty.handler.codec.mqtt.MqttQoS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
@Deprecated
public class DurableSession extends ClientSession {
class OutboundFlightZone {
/**
* Save the binding messageID, clientID - message
*
* @param messageID the packet ID used in transmission
* @param msg the message to put in flight zone
*/
void waitingAck(int messageID, IMessagesStore.StoredMessage msg) {
if (LOG.isTraceEnabled()) {
LOG.trace("Adding to inflight {}, guid <{}>", messageID, msg.getGuid());
}
sessionsStore.inFlight(clientID, messageID, msg);
}
IMessagesStore.StoredMessage acknowledged(int messageID) {
if (LOG.isTraceEnabled()) {
LOG.trace("Acknowledging inflight, clientID <{}> messageID {}", clientID, messageID);
}
return sessionsStore.inFlightAck(clientID, messageID);
}
}
private static final Logger LOG = LoggerFactory.getLogger(DurableSession.class);
private final ISessionsStore sessionsStore;
private final ISubscriptionsStore subscriptionsStore;
private final OutboundFlightZone outboundFlightZone;
private final InboundFlightZone inboundFlightZone;
private final ConcurrentMap<String, WillMessage> willMessages;
public DurableSession(String clientID, ISessionsStore sessions, ISubscriptionsStore subscriptionsStore,
ConcurrentMap<String, WillMessage> willMessages) {
super(clientID);
this.subscriptionsStore = subscriptionsStore;
this.sessionsStore = sessions;
this.willMessages = willMessages;
this.outboundFlightZone = new OutboundFlightZone();
this.inboundFlightZone = new InboundFlightZone();
}
public void reloadAllSubscriptionsFromStore() {
Collection<Subscription> reloadedSubscriptions = this.subscriptionsStore.listClientSubscriptions(this.clientID);
this.subscriptions.addAll(reloadedSubscriptions);
}
@Override
public boolean isCleanSession() {
return false;
}
@Override
public boolean subscribe(Subscription newSubscription) {
LOG.info("Adding new subscription. CId={}, topics={}, qos={}", newSubscription.getClientId(),
newSubscription.getTopicFilter(), newSubscription.getRequestedQos());
boolean validTopic = newSubscription.getTopicFilter().isValid();
if (!validTopic) {
LOG.warn("The topic filter is not valid. CId={}, topics={}", newSubscription.getClientId(),
newSubscription.getTopicFilter());
// send SUBACK with 0x80 for this topic filter
return false;
}
Subscription existingSub = subscriptionsStore.reload(newSubscription);
// update the selected subscriptions if not present or if has a greater qos
if (existingSub == null || existingSub.qosLessThan(newSubscription)) {
if (existingSub != null) {
LOG.info("Subscription already existed with a lower QoS value. It will be updated. CId={}, " +
"topics={}, existingQos={}, newQos={}", newSubscription.getClientId(),
newSubscription.getTopicFilter(), existingSub.getRequestedQos(), newSubscription.getRequestedQos());
subscriptions.remove(newSubscription);
}
subscriptions.add(newSubscription);
subscriptionsStore.addNewSubscription(newSubscription);
}
return true;
}
@Override
public void unsubscribeFrom(Topic topicFilter) {
LOG.info("Removing subscription. CId={}, topics={}", clientID, topicFilter);
subscriptionsStore.removeSubscription(topicFilter, clientID);
Set<Subscription> subscriptionsToRemove = new HashSet<>();
for (Subscription sub : this.subscriptions) {
if (sub.getTopicFilter().equals(topicFilter)) {
subscriptionsToRemove.add(sub);
}
}
subscriptions.removeAll(subscriptionsToRemove);
}
@Override
public void cleanSession() {
sessionsStore.removeTemporaryQoS2(this.clientID);
LOG.info("Wiping existing subscriptions. ClientId={}", clientID);
subscriptionsStore.wipeSubscriptions(clientID);
LOG.info("Removing queues. ClientId={}", clientID);
sessionsStore.dropQueue(clientID);
}
@Override
public void disconnect() {
LOG.info("Client disconnected. Removing its subscriptions. CId={}", clientID);
// cleanup topic subscriptions
cleanSession();
}
@Override
protected int nextPacketId() {
return this.sessionsStore.nextPacketID(this.clientID);
}
@Override
public IMessagesStore.StoredMessage inFlightAcknowledged(int messageID) {
return outboundFlightZone.acknowledged(messageID);
}
@Override
public int inFlightAckWaiting(IMessagesStore.StoredMessage msg) {
LOG.debug("Adding message to inflight zone. CId={}", clientID);
int messageId = this.nextPacketId();
outboundFlightZone.waitingAck(messageId, msg);
return messageId;
}
@Override
public IMessagesStore.StoredMessage inboundInflight(int messageID) {
return inboundFlightZone.lookup(messageID);
}
@Override
public void markAsInboundInflight(int messageID, IMessagesStore.StoredMessage msg) {
inboundFlightZone.waitingRel(messageID, msg);
}
@Override
public void moveInFlightToSecondPhaseAckWaiting(int messageID, IMessagesStore.StoredMessage msg) {
this.sessionsStore.moveInFlightToSecondPhaseAckWaiting(this.clientID, messageID, msg);
}
@Override
public boolean isEmptyQueue() {
return this.sessionsStore.queue(clientID).isEmpty();
}
@Override
public void dropQueue() {
LOG.debug("Removing messages of session. CId={}", this.clientID);
this.sessionsStore.dropQueue(this.clientID);
LOG.debug("Messages of the session have been removed. CId={}", this.clientID);
}
@Override
public EnqueuedMessage poll() {
IMessagesStore.StoredMessage msg = this.sessionsStore.queue(clientID).poll();
if (msg == null) {
return null;
}
int messageId = this.inFlightAckWaiting(msg);
return new EnqueuedMessage(msg, messageId);
}
@Override
public void enqueue(IMessagesStore.StoredMessage message) {
this.sessionsStore.queue(this.clientID).add(message);
}
@Override
public IMessagesStore.StoredMessage completeReleasedPublish(int messageID) {
return this.sessionsStore.completeReleasedPublish(clientID, messageID);
}
@Override
public int getPendingPublishMessagesNo() {
return this.sessionsStore.queue(clientID).size();
}
@Override
public int countPubReleaseWaitingPubComplete() {
return this.sessionsStore.countPubReleaseWaitingPubComplete(clientID);
}
@Override
public int getInflightMessagesNo() {
return this.sessionsStore.getInflightMessagesNo(clientID);
}
@Override
public void wipeSubscriptions() {
this.subscriptionsStore.wipeSubscriptions(clientID);
}
@Override
public void storeWillMessage(MqttConnectMessage msg, String clientId) {
// Handle will flag
if (msg.variableHeader().isWillFlag()) {
MqttQoS willQos = MqttQoS.valueOf(msg.variableHeader().willQos());
LOG.debug("Configuring MQTT last will and testament CId={}, willQos={}, willTopic={}, willRetain={}",
clientId, willQos, msg.payload().willTopic(), msg.variableHeader().isWillRetain());
byte[] willPayload = msg.payload().willMessage().getBytes(StandardCharsets.UTF_8);
ByteBuffer bb = (ByteBuffer) ByteBuffer.allocate(willPayload.length).put(willPayload).flip();
// save the will testament in the clientID store
WillMessage will = new WillMessage(msg.payload().willTopic(), bb, msg.variableHeader().isWillRetain(), willQos);
willMessages.put(clientId, will);
LOG.debug("MQTT last will and testament has been configured. CId={}", clientId);
}
}
@Override
public void removeWill() {
willMessages.remove(clientID);
}
@Override
public WillMessage willMessage() {
return willMessages.get(clientID);
}
}
@@ -1,29 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi;
@Deprecated
public class EnqueuedMessage {
public final IMessagesStore.StoredMessage msg;
public final int messageId;
EnqueuedMessage(IMessagesStore.StoredMessage msg, int messageId) {
this.msg = msg;
this.messageId = messageId;
}
}
@@ -1,25 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi;
/**
* Callback interface used to be notified of some events from the input event queue.
*
* It's the abstraction of the messaging stuff attached in after the front protocol parsing stuff.
*/
public interface IMessaging {
}
@@ -18,7 +18,6 @@ package io.moquette.spi;
import io.moquette.persistence.PersistentSession;
import io.moquette.spi.IMessagesStore.StoredMessage;
import io.moquette.spi.impl.WillMessage;
import java.time.LocalDateTime;
import java.util.Collection;
@@ -22,31 +22,32 @@ import io.moquette.spi.impl.subscriptions.Topic;
import java.util.Collection;
import java.util.List;
/**
* @deprecated look at ISubscriptionsRepository
*/
@Deprecated
public interface ISubscriptionsStore {
/**
* Add a new subscription to the session
*
* @param newSubscription
* the subscription to add.
* @param newSubscription the subscription to add.
*/
void addNewSubscription(Subscription newSubscription);
/**
* Removed a specific subscription
*
* @param topic
* the topic of the subscription.
* @param clientID
* the session client.
* @param topic the topic of the subscription.
* @param clientID the session client.
*/
void removeSubscription(Topic topic, String clientID);
/**
* Remove all the subscriptions of the session
*
* @param clientID
* the client ID
* @param clientID the client ID
*/
void wipeSubscriptions(String clientID);
@@ -57,12 +58,11 @@ public interface ISubscriptionsStore {
/**
* Load from storage all the subscriptions of the specified client.
* */
*/
Collection<Subscription> listClientSubscriptions(String clientID);
/**
* @param subcription
* the subscription to reaload from storage.
* @param subcription the subscription to reaload from storage.
* @return the subscription stored by clientID and topicFilter, if any else null;
*/
Subscription reload(Subscription subcription);
@@ -1,33 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Deprecated
class InboundFlightZone {
private final Map<Integer, IMessagesStore.StoredMessage> inboundFlightMessages = new ConcurrentHashMap<>();
IMessagesStore.StoredMessage lookup(int messageID) {
return inboundFlightMessages.get(messageID);
}
void waitingRel(int messageID, IMessagesStore.StoredMessage msg) {
inboundFlightMessages.put(messageID, msg);
}
}
@@ -1,226 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi;
import io.moquette.spi.IMessagesStore.StoredMessage;
import io.moquette.spi.impl.ProtocolProcessor;
import io.moquette.spi.impl.WillMessage;
import io.moquette.spi.impl.subscriptions.Subscription;
import io.moquette.spi.impl.subscriptions.Topic;
import io.netty.handler.codec.mqtt.MqttConnectMessage;
import io.netty.handler.codec.mqtt.MqttQoS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
@Deprecated
public class TransientSession extends ClientSession {
private static final Logger LOG = LoggerFactory.getLogger(TransientSession.class);
private Queue<StoredMessage> messagesQueue = new ArrayDeque<>();
private final AtomicInteger packetGenerator = new AtomicInteger(1);
private final ConcurrentMap<Integer, StoredMessage> inboundInflightMap = new ConcurrentHashMap<>();
private final ConcurrentMap<Integer, StoredMessage> outboundInflightMap = new ConcurrentHashMap<>();
private final Map<Integer, StoredMessage> secondPhaseStore = new ConcurrentHashMap<>();
private WillMessage will;
private final ConcurrentMap<String, WillMessage> willMessages;
public TransientSession(String clientID, ConcurrentMap<String, WillMessage> willMessages) {
super(clientID);
this.willMessages = willMessages;
}
@Override
public void disconnect() { }
@Override
public boolean isCleanSession() {
return true;
}
@Override
public boolean subscribe(Subscription newSubscription) {
LOG.trace("Adding new subscription. CId={}, topics={}, qos={}", newSubscription.getClientId(),
newSubscription.getTopicFilter(), newSubscription.getRequestedQos());
boolean validTopic = newSubscription.getTopicFilter().isValid();
if (!validTopic) {
LOG.warn("Invalid topic filter. CId={}, topicFilter={}", newSubscription.getClientId(),
newSubscription.getTopicFilter());
// send SUBACK with 0x80 for this topic filter
return false;
}
final Optional<Subscription> mathingExisting = this.subscriptions.stream()
.filter(s -> s.equals(newSubscription))
.findFirst();
if (mathingExisting.isPresent()) {
Subscription existingSub = mathingExisting.get();
if (existingSub.qosLessThan(newSubscription)) {
LOG.debug("Subscription already existed with a lower QoS value. It will be updated. CId={}, " +
"topics={}, existingQos={}, newQos={}", newSubscription.getClientId(),
newSubscription.getTopicFilter(), existingSub.getRequestedQos(), newSubscription.getRequestedQos());
subscriptions.remove(newSubscription);
}
}
subscriptions.add(newSubscription);
return true;
}
@Override
public void unsubscribeFrom(Topic topicFilter) {
LOG.trace("Removing subscription. CId={}, topics={}", clientID, topicFilter);
Set<Subscription> subscriptionsToRemove = new HashSet<>();
for (Subscription sub : this.subscriptions) {
if (sub.getTopicFilter().equals(topicFilter)) {
subscriptionsToRemove.add(sub);
}
}
subscriptions.removeAll(subscriptionsToRemove);
}
@Override
public boolean isEmptyQueue() {
return this.messagesQueue.isEmpty();
}
@Override
public void enqueue(StoredMessage message) {
this.messagesQueue.offer(message);
}
@Override
public EnqueuedMessage poll() {
IMessagesStore.StoredMessage msg = this.messagesQueue.poll();
if (msg == null) {
return null;
}
int messageId = this.inFlightAckWaiting(msg);
return new EnqueuedMessage(msg, messageId);
}
@Override
public void dropQueue() {
this.messagesQueue = null;
LOG.trace("Messages of the session have been removed. CId={}", this.clientID);
}
@Override
public void cleanSession() {
LOG.trace("Transient session, wiping existing subscriptions. ClientId={}", clientID);
this.subscriptions.clear();
//TODO
// LOG.info("Removing queues. ClientId={}", clientID);
// m_sessionsStore.dropQueue(clientID);
}
@Override
protected int nextPacketId() {
return this.packetGenerator.getAndIncrement();
}
@Override
public int inFlightAckWaiting(StoredMessage msg) {
LOG.trace("Adding message to inflight zone. CId={}", clientID);
int messageId = this.nextPacketId();
this.outboundInflightMap.put(messageId, msg);
return messageId;
}
@Override
public IMessagesStore.StoredMessage inFlightAcknowledged(int messageID) {
LOG.trace("Removing message to inflight zone. CId={}, messageID={}", clientID, messageID);
return this.outboundInflightMap.remove(messageID);
}
@Override
public void markAsInboundInflight(int messageID, IMessagesStore.StoredMessage msg) {
this.inboundInflightMap.put(messageID, msg);
}
@Override
public IMessagesStore.StoredMessage inboundInflight(int messageID) {
return this.inboundInflightMap.remove(messageID);
}
@Override
public void moveInFlightToSecondPhaseAckWaiting(int messageID, IMessagesStore.StoredMessage msg) {
this.secondPhaseStore.put(messageID, msg);
this.outboundInflightMap.put(messageID, msg);
}
@Override
public IMessagesStore.StoredMessage completeReleasedPublish(int messageID) {
LOG.trace("Acknowledged message in second phase, clientID <{}> messageID {}", clientID, messageID);
return this.secondPhaseStore.remove(messageID);
}
@Override
public int getPendingPublishMessagesNo() {
return this.messagesQueue.size();
}
@Override
public int countPubReleaseWaitingPubComplete() {
return this.outboundInflightMap.size();
}
@Override
public int getInflightMessagesNo() {
return this.inboundInflightMap.size();
}
@Override
public void wipeSubscriptions() {
// it's transient, no need to wide subs because wasn't stored
}
@Override
public void storeWillMessage(MqttConnectMessage msg, String clientId) {
// Handle will flag
if (msg.variableHeader().isWillFlag()) {
MqttQoS willQos = MqttQoS.valueOf(msg.variableHeader().willQos());
LOG.debug("Configuring MQTT last will and testament CId={}, willQos={}, willTopic={}, willRetain={}",
clientId, willQos, msg.payload().willTopic(), msg.variableHeader().isWillRetain());
byte[] willPayload = msg.payload().willMessage().getBytes(StandardCharsets.UTF_8);
ByteBuffer bb = (ByteBuffer) ByteBuffer.allocate(willPayload.length).put(willPayload).flip();
// save the will testament in the clientID store
will = new WillMessage(msg.payload().willTopic(), bb, msg.variableHeader().isWillRetain(), willQos);
willMessages.put(clientId, will);
LOG.debug("MQTT last will and testament has been configured. CId={}", clientId);
}
}
@Override
public void removeWill() {
willMessages.remove(clientID);
will = null;
}
@Override
public WillMessage willMessage() {
return will;
}
}
@@ -1,285 +0,0 @@
package io.moquette.spi.impl;
import io.moquette.connections.IConnectionsManager;
import io.moquette.server.ConnectionDescriptor;
import io.moquette.server.netty.AutoFlushHandler;
import io.moquette.server.netty.NettyUtils;
import io.moquette.spi.ClientSession;
import io.moquette.spi.ISessionsStore;
import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory;
import io.moquette.spi.impl.subscriptions.Subscription;
import io.moquette.spi.impl.subscriptions.Topic;
import io.moquette.spi.security.IAuthenticator;
import io.moquette.spi.security.IAuthorizatorPolicy;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.mqtt.*;
import io.netty.handler.timeout.IdleStateHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.NoSuchElementException;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static io.moquette.server.ConnectionDescriptor.ConnectionState.*;
import static io.netty.channel.ChannelFutureListener.CLOSE_ON_FAILURE;
import static io.netty.channel.ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE;
import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.*;
public class ConnectHandler {
private static final Logger LOG = LoggerFactory.getLogger(ConnectHandler.class);
private IConnectionsManager connectionDescriptors;
private BrokerInterceptor m_interceptor;
private SessionsRepository sessionsRepository;
private ISessionsStore m_sessionsStore;
private IAuthenticator m_authenticator;
private IAuthorizatorPolicy m_authorizator;
private ISubscriptionsDirectory subscriptions;
private boolean allowAnonymous;
private boolean allowZeroByteClientId;
private boolean reauthorizeSubscriptionsOnConnect;
private InternalRepublisher internalRepublisher;
ConnectHandler(IConnectionsManager connectedClients, BrokerInterceptor interceptor, SessionsRepository sessionsRepository,
ISessionsStore sessionsStore, IAuthenticator authenticator, IAuthorizatorPolicy authorizator,
ISubscriptionsDirectory subscriptions, boolean allowAnonymous, boolean allowZeroByteClientId,
boolean reauthorizeSubscriptionsOnConnect, InternalRepublisher internalRepublisher) {
this.connectionDescriptors = connectedClients;
this.m_interceptor = interceptor;
this.sessionsRepository = sessionsRepository;
this.m_sessionsStore = sessionsStore;
this.m_authenticator = authenticator;
this.m_authorizator = authorizator;
this.subscriptions = subscriptions;
this.allowAnonymous = allowAnonymous;
this.allowZeroByteClientId = allowZeroByteClientId;
this.reauthorizeSubscriptionsOnConnect = reauthorizeSubscriptionsOnConnect;
this.internalRepublisher = internalRepublisher;
}
public void processConnect(Channel channel, MqttConnectMessage msg) {
MqttConnectPayload payload = msg.payload();
String clientId = payload.clientIdentifier();
final String username = payload.userName();
LOG.debug("Processing CONNECT message. CId={}, username={}", clientId, username);
if (isNotProtocolVersion(msg, MqttVersion.MQTT_3_1) && isNotProtocolVersion(msg, MqttVersion.MQTT_3_1_1)) {
MqttConnAckMessage badProto = connAck(CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION);
LOG.error("MQTT protocol version is not valid. CId={}", clientId);
channel.writeAndFlush(badProto).addListener(FIRE_EXCEPTION_ON_FAILURE);
channel.close().addListener(CLOSE_ON_FAILURE);
return;
}
final boolean cleanSession = msg.variableHeader().isCleanSession();
if (clientId == null || clientId.length() == 0) {
if (!cleanSession || !this.allowZeroByteClientId) {
MqttConnAckMessage badId = connAck(CONNECTION_REFUSED_IDENTIFIER_REJECTED);
channel.writeAndFlush(badId).addListener(FIRE_EXCEPTION_ON_FAILURE);
channel.close().addListener(CLOSE_ON_FAILURE);
LOG.error("MQTT client ID cannot be empty. Username={}", username);
return;
}
// Generating client id.
clientId = UUID.randomUUID().toString().replace("-", "");
LOG.info("Client has connected with server generated id={}, username={}", clientId, username);
}
if (!login(channel, msg, clientId)) {
channel.close().addListener(CLOSE_ON_FAILURE);
return;
}
ConnectionDescriptor descriptor = new ConnectionDescriptor(clientId, channel, cleanSession);
final ConnectionDescriptor existing = this.connectionDescriptors.addConnection(descriptor);
if (existing != null) {
LOG.info("Client ID is being used in an existing connection, force to be closed. CId={}", clientId);
existing.abort();
//return;
this.connectionDescriptors.removeConnection(existing);
this.connectionDescriptors.addConnection(descriptor);
}
if (!descriptor.assignState(DISCONNECTED, INIT_SESSION)) {
channel.close().addListener(CLOSE_ON_FAILURE);
return;
}
LOG.debug("Initializing client session {}", clientId);
ClientSession existingSession = this.sessionsRepository.sessionForClient(clientId);
boolean isSessionAlreadyStored = existingSession != null;
final boolean msgCleanSessionFlag = msg.variableHeader().isCleanSession();
if (isSessionAlreadyStored && msgCleanSessionFlag) {
for (Subscription existingSub : existingSession.getSubscriptions()) {
this.subscriptions.removeSubscription(existingSub.getTopicFilter(), clientId);
}
}
initializeKeepAliveTimeout(channel, msg, clientId);
final ClientSession clientSession = this.sessionsRepository.createOrLoadClientSession(clientId, cleanSession);
clientSession.storeWillMessage(msg, clientId);
int flushIntervalMs = 500/* (keepAlive * 1000) / 2 */;
setupAutoFlusher(channel, flushIntervalMs);
if (!cleanSession && reauthorizeSubscriptionsOnConnect) {
reauthorizeOnExistingSubscriptions(clientId, username);
}
if (!descriptor.assignState(INIT_SESSION, SENDACK)) {
channel.close().addListener(CLOSE_ON_FAILURE);
return;
}
LOG.debug("Sending CONNACK. CId={}", clientId);
MqttConnAckMessage okResp = createConnectAck(msg, clientId);
final String connectClientId = clientId;
descriptor.writeAndFlush(okResp, new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
if (future.isSuccess()) {
LOG.debug("CONNACK has been sent. CId={}", connectClientId);
if (!descriptor.assignState(SENDACK, MESSAGES_REPUBLISHED)) {
channel.close().addListener(CLOSE_ON_FAILURE);
return;
}
m_interceptor.notifyClientConnected(msg);
if (!msg.variableHeader().isCleanSession()) {
// force the republish of stored QoS1 and QoS2
internalRepublisher.publishStored(clientSession);
}
if (!descriptor.assignState(MESSAGES_REPUBLISHED, ESTABLISHED)) {
channel.close().addListener(CLOSE_ON_FAILURE);
}
LOG.info("Connected client <{}> with login <{}>", connectClientId, username);
} else {
future.channel().pipeline().fireExceptionCaught(future.cause());
}
}
});
}
private boolean isNotProtocolVersion(MqttConnectMessage msg, MqttVersion version) {
return msg.variableHeader().version() != version.protocolLevel();
}
private void reauthorizeOnExistingSubscriptions(String clientId, String username) {
if (!m_sessionsStore.contains(clientId)) {
return;
}
final Collection<Subscription> clientSubscriptions = m_sessionsStore.subscriptionStore()
.listClientSubscriptions(clientId);
for (Subscription sub : clientSubscriptions) {
final Topic topicToReauthorize = sub.getTopicFilter();
final boolean readAuthorized = m_authorizator.canRead(topicToReauthorize, username, clientId);
if (!readAuthorized) {
subscriptions.removeSubscription(topicToReauthorize, clientId);
}
}
}
private void setupAutoFlusher(Channel channel, int flushIntervalMs) {
try {
channel.pipeline().addAfter(
"idleEventHandler",
"autoFlusher",
new AutoFlushHandler(flushIntervalMs, TimeUnit.MILLISECONDS));
} catch (NoSuchElementException nseex) {
// the idleEventHandler is not present on the pipeline
channel.pipeline()
.addFirst("autoFlusher", new AutoFlushHandler(flushIntervalMs, TimeUnit.MILLISECONDS));
}
}
private MqttConnAckMessage connAck(MqttConnectReturnCode returnCode) {
return connAck(returnCode, false);
}
private MqttConnAckMessage connAckWithSessionPresent(MqttConnectReturnCode returnCode) {
return connAck(returnCode, true);
}
private MqttConnAckMessage connAck(MqttConnectReturnCode returnCode, boolean sessionPresent) {
MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE,
false, 0);
MqttConnAckVariableHeader mqttConnAckVariableHeader = new MqttConnAckVariableHeader(returnCode, sessionPresent);
return new MqttConnAckMessage(mqttFixedHeader, mqttConnAckVariableHeader);
}
private boolean login(Channel channel, MqttConnectMessage msg, final String clientId) {
// handle user authentication
if (msg.variableHeader().hasUserName()) {
byte[] pwd = null;
if (msg.variableHeader().hasPassword()) {
pwd = msg.payload().password().getBytes(StandardCharsets.UTF_8);
} else if (!this.allowAnonymous) {
LOG.error("Client didn't supply any password and MQTT anonymous mode is disabled CId={}", clientId);
failedCredentials(channel);
return false;
}
final String login = msg.payload().userName();
if (!m_authenticator.checkValid(clientId, login, pwd)) {
LOG.error("Authenticator has rejected the MQTT credentials CId={}, username={}", clientId, login);
failedCredentials(channel);
return false;
}
NettyUtils.userName(channel, login);
} else if (!this.allowAnonymous) {
LOG.error("Client didn't supply any credentials and MQTT anonymous mode is disabled. CId={}", clientId);
failedCredentials(channel);
return false;
}
return true;
}
private MqttConnAckMessage createConnectAck(MqttConnectMessage msg, String clientId) {
MqttConnAckMessage okResp;
ClientSession clientSession = this.sessionsRepository.sessionForClient(clientId);
boolean isSessionAlreadyStored = clientSession != null;
final boolean msgCleanSessionFlag = msg.variableHeader().isCleanSession();
if (!msgCleanSessionFlag && isSessionAlreadyStored) {
okResp = connAckWithSessionPresent(CONNECTION_ACCEPTED);
} else {
okResp = connAck(CONNECTION_ACCEPTED);
}
return okResp;
}
private void initializeKeepAliveTimeout(Channel channel, MqttConnectMessage msg, String clientId) {
int keepAlive = msg.variableHeader().keepAliveTimeSeconds();
NettyUtils.keepAlive(channel, keepAlive);
NettyUtils.cleanSession(channel, msg.variableHeader().isCleanSession());
NettyUtils.clientID(channel, clientId);
int idleTime = Math.round(keepAlive * 1.5f);
setIdleTime(channel.pipeline(), idleTime);
LOG.debug("Connection has been configured CId={}, keepAlive={}, removeTemporaryQoS2={}, idleTime={}",
clientId, keepAlive, msg.variableHeader().isCleanSession(), idleTime);
}
private void failedCredentials(Channel session) {
session.writeAndFlush(connAck(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD))
.addListener(FIRE_EXCEPTION_ON_FAILURE);
LOG.info("Client {} failed to connect with bad username or password.", session);
}
private void setIdleTime(ChannelPipeline pipeline, int idleTime) {
if (pipeline.names().contains("idleStateHandler")) {
pipeline.remove("idleStateHandler");
}
pipeline.addFirst("idleStateHandler", new IdleStateHandler(idleTime, 0, 0));
}
}
@@ -1,133 +0,0 @@
package io.moquette.spi.impl;
import io.moquette.connections.IConnectionsManager;
import io.moquette.server.ConnectionDescriptor;
import io.moquette.server.netty.NettyUtils;
import io.moquette.spi.ClientSession;
import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory;
import io.moquette.spi.impl.subscriptions.Subscription;
import io.netty.channel.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Optional;
import static io.moquette.server.ConnectionDescriptor.ConnectionState.*;
import static io.netty.channel.ChannelFutureListener.CLOSE_ON_FAILURE;
public class DisconnectHandler {
private static final Logger LOG = LoggerFactory.getLogger(DisconnectHandler.class);
private final IConnectionsManager connectionDescriptors;
private final SessionsRepository sessionsRepository;
private final ISubscriptionsDirectory subscriptions;
private final BrokerInterceptor m_interceptor;
public DisconnectHandler(IConnectionsManager connectionDescriptors, SessionsRepository sessionsRepository,
ISubscriptionsDirectory subscriptions, BrokerInterceptor interceptor) {
this.connectionDescriptors = connectionDescriptors;
this.sessionsRepository = sessionsRepository;
this.subscriptions = subscriptions;
this.m_interceptor = interceptor;
}
public void processDisconnect(Channel channel, String clientID) {
final Optional<ConnectionDescriptor> existingDescriptorOpt = this.connectionDescriptors.lookupDescriptor(clientID);
if (!existingDescriptorOpt.isPresent()) {
// another client with same ID removed the descriptor, we must exit
channel.close().addListener(CLOSE_ON_FAILURE);
return;
}
final ConnectionDescriptor existingDescriptor = existingDescriptorOpt.get();
if (existingDescriptor.doesNotUseChannel(channel)) {
// another client saved it's descriptor, exit
LOG.warn("Another client is using the connection descriptor. Closing connection. CId={}", clientID);
existingDescriptor.abort();
return;
}
if (!removeSubscriptions(existingDescriptor, clientID)) {
LOG.warn("Unable to remove subscriptions. Closing connection. CId={}", clientID);
existingDescriptor.abort();
return;
}
if (!dropStoredMessages(existingDescriptor, clientID)) {
LOG.warn("Unable to drop stored messages. Closing connection. CId={}", clientID);
existingDescriptor.abort();
return;
}
if (!cleanWillMessageAndNotifyInterceptor(existingDescriptor, clientID)) {
LOG.warn("Unable to drop will message. Closing connection. CId={}", clientID);
existingDescriptor.abort();
return;
}
if (!existingDescriptor.close()) {
LOG.debug("Connection has been closed. CId={}", clientID);
return;
}
boolean stillPresent = this.connectionDescriptors.removeConnection(existingDescriptor);
if (!stillPresent) {
// another descriptor was inserted
LOG.warn("Another descriptor has been inserted. CId={}", clientID);
return;
}
this.sessionsRepository.disconnect(clientID);
LOG.info("Client <{}> disconnected", clientID);
}
private boolean removeSubscriptions(ConnectionDescriptor descriptor, String clientID) {
final boolean success = descriptor.assignState(ESTABLISHED, SUBSCRIPTIONS_REMOVED);
if (!success) {
return false;
}
if (descriptor.cleanSession) {
LOG.trace("Removing saved subscriptions. CId={}", descriptor.clientID);
final ClientSession session = this.sessionsRepository.sessionForClient(clientID);
session.wipeSubscriptions();
for (Subscription existingSub : session.getSubscriptions()) {
this.subscriptions.removeSubscription(existingSub.getTopicFilter(), clientID);
}
LOG.trace("Saved subscriptions have been removed. CId={}", descriptor.clientID);
}
return true;
}
private boolean dropStoredMessages(ConnectionDescriptor descriptor, String clientID) {
final boolean success = descriptor.assignState(SUBSCRIPTIONS_REMOVED, MESSAGES_DROPPED);
if (!success) {
return false;
}
final ClientSession clientSession = this.sessionsRepository.sessionForClient(clientID);
if (clientSession.isCleanSession()) {
clientSession.dropQueue();
}
return true;
}
private boolean cleanWillMessageAndNotifyInterceptor(ConnectionDescriptor descriptor, String clientID) {
final boolean success = descriptor.assignState(MESSAGES_DROPPED, INTERCEPTORS_NOTIFIED);
if (!success) {
return false;
}
LOG.trace("Removing will message. ClientId={}", descriptor.clientID);
// cleanup the will store
final ClientSession clientSession = this.sessionsRepository.sessionForClient(clientID);
clientSession.removeWill();
String username = descriptor.getUsername();
m_interceptor.notifyClientDisconnected(clientID, username);
return true;
}
}
@@ -1,104 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl;
import io.moquette.spi.ClientSession;
import io.moquette.spi.EnqueuedMessage;
import io.moquette.spi.IMessagesStore;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.mqtt.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
class InternalRepublisher {
private static final Logger LOG = LoggerFactory.getLogger(InternalRepublisher.class);
private final PersistentQueueMessageSender messageSender;
InternalRepublisher(PersistentQueueMessageSender messageSender) {
this.messageSender = messageSender;
}
void publishRetained(ClientSession targetSession, Collection<IMessagesStore.StoredMessage> messages) {
for (IMessagesStore.StoredMessage storedMsg : messages) {
// fire as retained the message
MqttPublishMessage publishMsg = retainedPublish(storedMsg);
if (storedMsg.getQos() != MqttQoS.AT_MOST_ONCE) {
LOG.debug("Adding message to inflight zone. ClientId={}, topic={}", targetSession.clientID,
storedMsg.getTopic());
int packetID = targetSession.inFlightAckWaiting(storedMsg);
// set the PacketIdentifier only for QoS > 0
publishMsg = retainedPublish(storedMsg, packetID);
}
this.messageSender.sendPublish(targetSession, publishMsg);
}
}
void publishStored(ClientSession clientSession) {
if (clientSession.isEmptyQueue()) {
LOG.info("There are no stored publish events to CId={}", clientSession.clientID);
return;
}
LOG.info("Republishing stored publish events. CId={}", clientSession.clientID);
EnqueuedMessage pubEvt;
while ((pubEvt = clientSession.poll()) != null) {
// put in flight zone
LOG.debug("Adding message ot inflight zone. ClientId={}, guid={}, topic={}", clientSession.clientID,
pubEvt.msg.getGuid(), pubEvt.msg.getTopic());
MqttPublishMessage publishMsg = notRetainedPublish(pubEvt.msg);
// set the PacketIdentifier only for QoS > 0
if (publishMsg.fixedHeader().qosLevel() != MqttQoS.AT_MOST_ONCE) {
publishMsg = notRetainedPublish(pubEvt.msg, pubEvt.messageId);
}
this.messageSender.sendPublish(clientSession, publishMsg);
}
}
private MqttPublishMessage notRetainedPublish(IMessagesStore.StoredMessage storedMessage, Integer messageID) {
return createPublishForQos(storedMessage.getTopic(), storedMessage.getQos(), storedMessage.getPayload(), false,
messageID);
}
private MqttPublishMessage notRetainedPublish(IMessagesStore.StoredMessage storedMessage) {
return createPublishForQos(storedMessage.getTopic(), storedMessage.getQos(), storedMessage.getPayload(), false,
0);
}
private MqttPublishMessage retainedPublish(IMessagesStore.StoredMessage storedMessage) {
return createPublishForQos(storedMessage.getTopic(), storedMessage.getQos(), storedMessage.getPayload(), true,
0);
}
private MqttPublishMessage retainedPublish(IMessagesStore.StoredMessage storedMessage, Integer packetID) {
return createPublishForQos(storedMessage.getTopic(), storedMessage.getQos(), storedMessage.getPayload(), true,
packetID);
}
public static MqttPublishMessage createPublishForQos(String topic, MqttQoS qos, ByteBuf message, boolean retained,
int messageId) {
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, false, qos, retained, 0);
MqttPublishVariableHeader varHeader = new MqttPublishVariableHeader(topic, messageId);
return new MqttPublishMessage(fixedHeader, varHeader, message);
}
}
@@ -1,115 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl;
import io.moquette.connections.IConnectionsManager;
import io.moquette.spi.ClientSession;
import io.moquette.spi.IMessagesStore;
import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory;
import io.moquette.spi.impl.subscriptions.Subscription;
import io.moquette.spi.impl.subscriptions.Topic;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.mqtt.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import static io.moquette.spi.impl.ProtocolProcessor.lowerQosToTheSubscriptionDesired;
class MessagesPublisher {
private static final Logger LOG = LoggerFactory.getLogger(MessagesPublisher.class);
private final IConnectionsManager connectionDescriptors;
private final PersistentQueueMessageSender messageSender;
private final ISubscriptionsDirectory subscriptions;
private SessionsRepository sessionsRepository;
MessagesPublisher(IConnectionsManager connectionDescriptors,
PersistentQueueMessageSender messageSender, ISubscriptionsDirectory subscriptions,
SessionsRepository sessionsRepository) {
this.connectionDescriptors = connectionDescriptors;
this.messageSender = messageSender;
this.subscriptions = subscriptions;
this.sessionsRepository = sessionsRepository;
}
private static MqttPublishMessage notRetainedPublish(String topic, MqttQoS qos, ByteBuf message) {
return notRetainedPublishWithMessageId(topic, qos, message, 0);
}
private static MqttPublishMessage notRetainedPublishWithMessageId(String topic, MqttQoS qos, ByteBuf message,
int messageId) {
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, false, qos, false, 0);
MqttPublishVariableHeader varHeader = new MqttPublishVariableHeader(topic, messageId);
return new MqttPublishMessage(fixedHeader, varHeader, message);
}
void publish2Subscribers(IMessagesStore.StoredMessage pubMsg, Topic topic, int messageID) {
if (LOG.isTraceEnabled()) {
LOG.trace("Sending publish message to subscribers. ClientId={}, topic={}, messageId={}, payload={}, " +
"subscriptionTree={}", pubMsg.getClientID(), topic, messageID,
DebugUtils.payload2Str(pubMsg.getPayload()),
subscriptions.dumpTree());
} else {
LOG.info("Sending publish message to subscribers. ClientId={}, topic={}, messageId={}",
pubMsg.getClientID(), topic, messageID);
}
publish2Subscribers(pubMsg, topic);
}
void publish2Subscribers(IMessagesStore.StoredMessage pubMsg, Topic topic) {
List<Subscription> topicMatchingSubscriptions = subscriptions.matches(topic);
final String topic1 = pubMsg.getTopic();
final MqttQoS publishingQos = pubMsg.getQos();
final ByteBuf origPayload = pubMsg.getPayload();
for (final Subscription sub : topicMatchingSubscriptions) {
MqttQoS qos = lowerQosToTheSubscriptionDesired(sub, publishingQos);
ClientSession targetSession = this.sessionsRepository.sessionForClient(sub.getClientId());
boolean targetIsActive = this.connectionDescriptors.isConnected(sub.getClientId());
// TODO move all this logic into messageSender, which puts into the flightZone only the messages
// that pull out of the queue.
if (targetIsActive) {
LOG.debug("Sending PUBLISH message to active subscriber. CId={}, topicFilter={}, qos={}",
sub.getClientId(), sub.getTopicFilter(), qos);
// we need to retain because duplicate only copy r/w indexes and don't retain() causing
// refCnt = 0
ByteBuf payload = origPayload.retainedDuplicate();
MqttPublishMessage publishMsg;
if (qos != MqttQoS.AT_MOST_ONCE) {
// QoS 1 or 2
int messageId = targetSession.inFlightAckWaiting(pubMsg);
// set the PacketIdentifier only for QoS > 0
publishMsg = notRetainedPublishWithMessageId(topic1, qos, payload, messageId);
} else {
publishMsg = notRetainedPublish(topic1, qos, payload);
}
this.messageSender.sendPublish(targetSession, publishMsg);
} else {
if (!targetSession.isCleanSession()) {
LOG.debug("Storing pending PUBLISH inactive message. CId={}, topicFilter={}, qos={}",
sub.getClientId(), sub.getTopicFilter(), qos);
// store the message in targetSession queue to deliver
targetSession.enqueue(pubMsg);
}
}
}
}
}
@@ -1,80 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl;
import io.moquette.connections.IConnectionsManager;
import io.moquette.server.ConnectionDescriptor;
import io.moquette.server.ConnectionDescriptorStore;
import io.moquette.spi.ClientSession;
import io.netty.handler.codec.mqtt.MqttMessageType;
import io.netty.handler.codec.mqtt.MqttPublishMessage;
import io.netty.handler.codec.mqtt.MqttQoS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Optional;
import static io.moquette.spi.impl.ProtocolProcessor.asStoredMessage;
import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE;
class PersistentQueueMessageSender {
private static final Logger LOG = LoggerFactory.getLogger(PersistentQueueMessageSender.class);
private final IConnectionsManager connectionDescriptorStore;
PersistentQueueMessageSender(IConnectionsManager connectionDescriptorStore) {
this.connectionDescriptorStore = connectionDescriptorStore;
}
void sendPublish(ClientSession clientsession, MqttPublishMessage pubMessage) {
String clientId = clientsession.clientID;
final int messageId = pubMessage.variableHeader().messageId();
final String topicName = pubMessage.variableHeader().topicName();
MqttQoS qos = pubMessage.fixedHeader().qosLevel();
if (LOG.isDebugEnabled()) {
LOG.debug("Sending PUBLISH message. MessageId={}, CId={}, topic={}, qos={}, payload={}", messageId,
clientId, topicName, qos, DebugUtils.payload2Str(pubMessage.payload()));
} else {
LOG.info("Sending PUBLISH message. MessageId={}, CId={}, topic={}", messageId, clientId, topicName);
}
boolean messageDelivered = false;
final Optional<ConnectionDescriptor> optDescriptor = connectionDescriptorStore.lookupDescriptor(clientId);
if (optDescriptor.isPresent()) {
final ConnectionDescriptor descriptor = optDescriptor.get();
try {
descriptor.writeAndFlush(pubMessage);
messageDelivered = true;
} catch (Throwable e) {
LOG.error("Unable to send {} message. CId=<{}>, messageId={}", pubMessage.fixedHeader().messageType(),
clientId, messageId, e);
}
}
if (!messageDelivered) {
if (qos != AT_MOST_ONCE && !clientsession.isCleanSession()) {
LOG.warn("PUBLISH message could not be delivered. It will be stored. MessageId={}, CId={}, topic={}, " +
"qos={}, removeTemporaryQoS2={}", messageId, clientId, topicName, qos, false);
clientsession.enqueue(asStoredMessage(pubMessage));
} else {
LOG.warn("PUBLISH message could not be delivered. It will be discarded. MessageId={}, CId={}, " +
"topic={}, qos={}, removeTemporaryQoS2={}", messageId, clientId, topicName, qos, true);
}
}
}
}
@@ -1,583 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl;
import io.moquette.connections.IConnectionsManager;
import io.moquette.interception.InterceptHandler;
import io.moquette.interception.messages.InterceptAcknowledgedMessage;
import io.moquette.server.ConnectionDescriptor;
import io.moquette.server.ConnectionDescriptorStore;
import io.moquette.server.netty.NettyUtils;
import io.moquette.spi.ClientSession;
import io.moquette.spi.EnqueuedMessage;
import io.moquette.spi.IMessagesStore;
import io.moquette.spi.IMessagesStore.StoredMessage;
import io.moquette.spi.ISessionsStore;
import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory;
import io.moquette.spi.impl.subscriptions.Subscription;
import io.moquette.spi.impl.subscriptions.Topic;
import io.moquette.spi.security.IAuthenticator;
import io.moquette.spi.security.IAuthorizatorPolicy;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.handler.codec.mqtt.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static io.moquette.spi.impl.InternalRepublisher.createPublishForQos;
import static io.moquette.spi.impl.Utils.messageId;
import static io.moquette.spi.impl.Utils.readBytesAndRewind;
import static io.netty.channel.ChannelFutureListener.CLOSE_ON_FAILURE;
import static io.netty.channel.ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE;
import static io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader.from;
import static io.netty.handler.codec.mqtt.MqttQoS.*;
/**
* Class responsible to handle the logic of MQTT protocol it's the director of the protocol
* execution.
*
* Used by the front facing class ProtocolProcessorBootstrapper.
*/
public class ProtocolProcessor {
private enum SubscriptionState {
STORED, VERIFIED
}
private static final Logger LOG = LoggerFactory.getLogger(ProtocolProcessor.class);
private IConnectionsManager connectionDescriptors;
private ConcurrentMap<RunningSubscription, SubscriptionState> subscriptionInCourse;
private ISubscriptionsDirectory subscriptions;
private IAuthorizatorPolicy m_authorizator;
private IMessagesStore m_messagesStore;
private ISessionsStore m_sessionsStore;
private BrokerInterceptor m_interceptor;
private Qos0PublishHandler qos0PublishHandler;
private Qos1PublishHandler qos1PublishHandler;
private Qos2PublishHandler qos2PublishHandler;
private MessagesPublisher messagesPublisher;
private InternalRepublisher internalRepublisher;
SessionsRepository sessionsRepository;
private ConnectHandler connectHandler;
private DisconnectHandler disconnectHandler;
ProtocolProcessor() {
}
public void init(ISubscriptionsDirectory subscriptions, IMessagesStore storageService, ISessionsStore sessionsStore,
IAuthenticator authenticator, boolean allowAnonymous, IAuthorizatorPolicy authorizator,
BrokerInterceptor interceptor, SessionsRepository sessionsRepository,
boolean reauthorizeSubscriptionsOnConnect) {
init(subscriptions, storageService, sessionsStore, authenticator, allowAnonymous, false,
authorizator, interceptor, sessionsRepository, reauthorizeSubscriptionsOnConnect);
}
public void init(ISubscriptionsDirectory subscriptions, IMessagesStore storageService, ISessionsStore sessionsStore,
IAuthenticator authenticator, boolean allowAnonymous, boolean allowZeroByteClientId,
IAuthorizatorPolicy authorizator, BrokerInterceptor interceptor, SessionsRepository sessionsRepository,
boolean reauthorizeSubscriptionsOnConnect) {
init(new ConnectionDescriptorStore(), subscriptions, storageService, sessionsStore,
authenticator, allowAnonymous, allowZeroByteClientId, authorizator, interceptor, sessionsRepository,
reauthorizeSubscriptionsOnConnect);
}
/**
* @param subscriptions
* the subscription store where are stored all the existing clients subscriptions.
* @param storageService
* the persistent store to use for save/load of messages for QoS1 and QoS2 handling.
* @param sessionsStore
* the clients sessions store, used to persist subscriptions.
* @param authenticator
* the authenticator used in connect messages.
* @param allowAnonymous
* true connection to clients without credentials.
* @param allowZeroByteClientId
* true to allow clients connect without a clientid
* @param authorizator
* used to apply ACL policies to publishes and subscriptions.
* @param interceptor
* to notify events to an intercept handler
*/
void init(IConnectionsManager connectionDescriptors, ISubscriptionsDirectory subscriptions,
IMessagesStore storageService, ISessionsStore sessionsStore, IAuthenticator authenticator,
boolean allowAnonymous, boolean allowZeroByteClientId, IAuthorizatorPolicy authorizator,
BrokerInterceptor interceptor, SessionsRepository sessionsRepository,
boolean reauthorizeSubscriptionsOnConnect) {
LOG.info("Initializing MQTT protocol processor..");
this.connectionDescriptors = connectionDescriptors;
this.subscriptionInCourse = new ConcurrentHashMap<>();
this.m_interceptor = interceptor;
this.subscriptions = subscriptions;
m_authorizator = authorizator;
if (LOG.isDebugEnabled()) {
LOG.debug("Initial subscriptions tree={}", subscriptions.dumpTree());
}
m_messagesStore = storageService;
m_sessionsStore = sessionsStore;
this.sessionsRepository = sessionsRepository;
LOG.debug("Initializing messages publisher...");
final PersistentQueueMessageSender messageSender = new PersistentQueueMessageSender(this.connectionDescriptors);
this.messagesPublisher = new MessagesPublisher(connectionDescriptors, messageSender,
subscriptions, this.sessionsRepository);
LOG.debug("Initializing QoS publish handlers...");
this.qos0PublishHandler = new Qos0PublishHandler(m_authorizator, m_messagesStore, m_interceptor,
this.messagesPublisher);
this.qos1PublishHandler = new Qos1PublishHandler(m_authorizator, m_messagesStore, m_interceptor,
this.connectionDescriptors, this.messagesPublisher);
this.qos2PublishHandler = new Qos2PublishHandler(m_authorizator, subscriptions, m_messagesStore, m_interceptor,
this.connectionDescriptors, this.messagesPublisher, this.sessionsRepository);
LOG.debug("Initializing internal republisher...");
this.internalRepublisher = new InternalRepublisher(messageSender);
connectHandler = new ConnectHandler(connectionDescriptors, m_interceptor, sessionsRepository, m_sessionsStore,
authenticator, m_authorizator, subscriptions,
allowAnonymous, allowZeroByteClientId, reauthorizeSubscriptionsOnConnect, internalRepublisher);
disconnectHandler = new DisconnectHandler(connectionDescriptors, sessionsRepository, subscriptions, m_interceptor);
LOG.info("Initialized");
}
public void processConnect(Channel channel, MqttConnectMessage connectMessage) {
this.connectHandler.processConnect(channel, connectMessage);
}
public void processPubAck(Channel channel, MqttPubAckMessage msg) {
String clientID = NettyUtils.clientID(channel);
int messageID = msg.variableHeader().messageId();
String username = NettyUtils.userName(channel);
LOG.trace("retrieving inflight for messageID <{}>", messageID);
ClientSession targetSession = this.sessionsRepository.sessionForClient(clientID);
StoredMessage inflightMsg = targetSession.inFlightAcknowledged(messageID);
String topic = inflightMsg.getTopic();
InterceptAcknowledgedMessage wrapped = new InterceptAcknowledgedMessage(inflightMsg, topic, username,
messageID);
m_interceptor.notifyMessageAcknowledged(wrapped);
}
public static IMessagesStore.StoredMessage asStoredMessage(MqttPublishMessage msg) {
// TODO ugly, too much array copy
ByteBuf payload = msg.payload();
byte[] payloadContent = readBytesAndRewind(payload);
IMessagesStore.StoredMessage stored = new IMessagesStore.StoredMessage(payloadContent,
msg.fixedHeader().qosLevel(), msg.variableHeader().topicName());
stored.setRetained(msg.fixedHeader().isRetain());
return stored;
}
private static IMessagesStore.StoredMessage asStoredMessage(WillMessage will) {
IMessagesStore.StoredMessage pub = new IMessagesStore.StoredMessage(will.getPayload().array(), will.getQos(),
will.getTopic());
pub.setRetained(will.isRetained());
return pub;
}
public void processPublish(Channel channel, MqttPublishMessage msg) {
final MqttQoS qos = msg.fixedHeader().qosLevel();
final String clientId = NettyUtils.clientID(channel);
LOG.info("Processing PUBLISH message. CId={}, topic={}, messageId={}, qos={}", clientId,
msg.variableHeader().topicName(), msg.variableHeader().messageId(), qos);
switch (qos) {
case AT_MOST_ONCE:
this.qos0PublishHandler.receivedPublishQos0(channel, msg);
break;
case AT_LEAST_ONCE:
this.qos1PublishHandler.receivedPublishQos1(channel, msg);
break;
case EXACTLY_ONCE:
this.qos2PublishHandler.receivedPublishQos2(channel, msg);
break;
default:
LOG.error("Unknown QoS-Type:{}", qos);
break;
}
}
/**
* Intended usage is only for embedded versions of the broker, where the hosting application
* want to use the broker to send a publish message. Inspired by {@link #processPublish} but
* with some changes to avoid security check, and the handshake phases for Qos1 and Qos2. It
* also doesn't notifyTopicPublished because using internally the owner should already know
* where it's publishing.
*
* @param msg
* the message to publish.
* @param clientId
* the clientID
*/
public void internalPublish(MqttPublishMessage msg, final String clientId) {
final MqttQoS qos = msg.fixedHeader().qosLevel();
final Topic topic = new Topic(msg.variableHeader().topicName());
LOG.info("Sending PUBLISH message. Topic={}, qos={}", topic, qos);
IMessagesStore.StoredMessage toStoreMsg = asStoredMessage(msg);
if (clientId == null || clientId.isEmpty()) {
toStoreMsg.setClientID("BROKER_SELF");
} else {
toStoreMsg.setClientID(clientId);
}
this.messagesPublisher.publish2Subscribers(toStoreMsg, topic);
if (!msg.fixedHeader().isRetain()) {
return;
}
if (qos == AT_MOST_ONCE || msg.payload().readableBytes() == 0) {
// QoS == 0 && retain => clean old retained
m_messagesStore.cleanRetained(topic);
return;
}
m_messagesStore.storeRetained(topic, toStoreMsg);
}
/**
* Specialized version to publish will testament message.
*/
private void forwardPublishWill(WillMessage will, String clientID) {
LOG.info("Publishing will message. CId={}, topic={}", clientID, will.getTopic());
// it has just to publish the message downstream to the subscribers
// NB it's a will publish, it needs a PacketIdentifier for this conn, default to 1
IMessagesStore.StoredMessage tobeStored = asStoredMessage(will);
tobeStored.setClientID(clientID);
Topic topic = new Topic(tobeStored.getTopic());
this.messagesPublisher.publish2Subscribers(tobeStored, topic);
//Stores retained message to the topic
if (will.isRetained()) {
m_messagesStore.storeRetained(topic, tobeStored);
}
}
static MqttQoS lowerQosToTheSubscriptionDesired(Subscription sub, MqttQoS qos) {
if (qos.value() > sub.getRequestedQos().value()) {
qos = sub.getRequestedQos();
}
return qos;
}
/**
* Second phase of a publish QoS2 protocol, sent by publisher to the broker. Search the stored
* message and publish to all interested subscribers.
*
* @param channel
* the channel of the incoming message.
* @param msg
* the decoded pubrel message.
*/
public void processPubRel(Channel channel, MqttMessage msg) {
this.qos2PublishHandler.processPubRel(channel, msg);
}
public void processPubRec(Channel channel, MqttMessage msg) {
String clientID = NettyUtils.clientID(channel);
int messageID = messageId(msg);
LOG.debug("Processing PUBREC message. CId={}, messageId={}", clientID, messageID);
ClientSession targetSession = this.sessionsRepository.sessionForClient(clientID);
// remove from the inflight and move to the QoS2 second phase queue
StoredMessage ackedMsg = targetSession.inFlightAcknowledged(messageID);
targetSession.moveInFlightToSecondPhaseAckWaiting(messageID, ackedMsg);
// once received a PUBREC reply with a PUBREL(messageID)
LOG.debug("Processing PUBREC message. CId={}, messageId={}", clientID, messageID);
MqttFixedHeader pubRelHeader = new MqttFixedHeader(MqttMessageType.PUBREL, false, AT_LEAST_ONCE, false, 0);
MqttMessage pubRelMessage = new MqttMessage(pubRelHeader, from(messageID));
channel.writeAndFlush(pubRelMessage).addListener(FIRE_EXCEPTION_ON_FAILURE);
}
public void processPubComp(Channel channel, MqttMessage msg) {
String clientID = NettyUtils.clientID(channel);
int messageID = messageId(msg);
LOG.debug("Processing PUBCOMP message. CId={}, messageId={}", clientID, messageID);
// once received the PUBCOMP then remove the message from the temp memory
ClientSession targetSession = this.sessionsRepository.sessionForClient(clientID);
StoredMessage inflightMsg = targetSession.completeReleasedPublish(messageID);
String username = NettyUtils.userName(channel);
String topic = inflightMsg.getTopic();
final InterceptAcknowledgedMessage interceptAckMsg = new InterceptAcknowledgedMessage(inflightMsg, topic,
username, messageID);
m_interceptor.notifyMessageAcknowledged(interceptAckMsg);
}
public void processDisconnect(Channel channel) {
final String clientID = NettyUtils.clientID(channel);
LOG.debug("Processing DISCONNECT message. CId={}", clientID);
channel.flush();
disconnectHandler.processDisconnect(channel, clientID);
}
public void processConnectionLost(String clientID, Channel channel) {
LOG.info("Lost connection with client <{}>", clientID);
ConnectionDescriptor oldConnDescr = new ConnectionDescriptor(clientID, channel, true);
connectionDescriptors.removeConnection(oldConnDescr);
// publish the Will message (if any) for the clientID
final ClientSession clientSession = this.sessionsRepository.sessionForClient(clientID);
WillMessage will = clientSession.willMessage();
if (will != null) {
forwardPublishWill(will, clientID);
clientSession.removeWill();
}
String username = NettyUtils.userName(channel);
m_interceptor.notifyClientConnectionLost(clientID, username);
}
/**
* Remove the clientID from topic subscription, if not previously subscribed, doesn't reply any
* error.
*
* @param channel
* the channel of the incoming message.
* @param msg
* the decoded unsubscribe message.
*/
public void processUnsubscribe(Channel channel, MqttUnsubscribeMessage msg) {
List<String> topics = msg.payload().topics();
String clientID = NettyUtils.clientID(channel);
LOG.debug("Processing UNSUBSCRIBE message. CId={}, topics={}", clientID, topics);
ClientSession clientSession = this.sessionsRepository.sessionForClient(clientID);
for (String t : topics) {
Topic topic = new Topic(t);
boolean validTopic = topic.isValid();
if (!validTopic) {
// close the connection, not valid topicFilter is a protocol violation
channel.close().addListener(CLOSE_ON_FAILURE);
LOG.error("Topic filter is not valid. CId={}, topics={}, badTopicFilter={}", clientID, topics, topic);
return;
}
LOG.trace("Removing subscription. CId={}, topic={}", clientID, topic);
subscriptions.removeSubscription(topic, clientID);
clientSession.unsubscribeFrom(topic);
String username = NettyUtils.userName(channel);
m_interceptor.notifyTopicUnsubscribed(topic.toString(), clientID, username);
}
// ack the client
int messageID = msg.variableHeader().messageId();
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.UNSUBACK, false, AT_MOST_ONCE,
false, 0);
MqttUnsubAckMessage ackMessage = new MqttUnsubAckMessage(fixedHeader, from(messageID));
LOG.debug("Sending UNSUBACK message. CId={}, topics={}, messageId={}", clientID, topics, messageID);
channel.writeAndFlush(ackMessage).addListener(FIRE_EXCEPTION_ON_FAILURE);
LOG.info("Client <{}> unsubscribed from topics <{}>", clientID, topics);
}
public void processSubscribe(Channel channel, MqttSubscribeMessage msg) {
String clientID = NettyUtils.clientID(channel);
int messageID = messageId(msg);
LOG.debug("Processing SUBSCRIBE message. CId={}, messageId={}", clientID, messageID);
RunningSubscription executionKey = new RunningSubscription(clientID, messageID);
SubscriptionState currentStatus = subscriptionInCourse.putIfAbsent(executionKey, SubscriptionState.VERIFIED);
if (currentStatus != null) {
LOG.warn("Client sent another SUBSCRIBE message while this one was being processed CId={}, messageId={}",
clientID, messageID);
return;
}
String username = NettyUtils.userName(channel);
List<MqttTopicSubscription> ackTopics = doVerify(clientID, username, msg);
MqttSubAckMessage ackMessage = doAckMessageFromValidateFilters(ackTopics, messageID);
if (!this.subscriptionInCourse.replace(executionKey, SubscriptionState.VERIFIED, SubscriptionState.STORED)) {
LOG.warn("Client sent another SUBSCRIBE message while the topic filters were being verified CId={}, " +
"messageId={}", clientID, messageID);
return;
}
LOG.debug("Creating and storing subscriptions CId={}, messageId={}, topics={}", clientID, messageID, ackTopics);
List<Subscription> newSubscriptions = doStoreSubscription(ackTopics, clientID);
// save session, persist subscriptions from session
for (Subscription subscription : newSubscriptions) {
subscriptions.add(subscription);
}
LOG.debug("Sending SUBACK response CId={}, messageId={}", clientID, messageID);
channel.writeAndFlush(ackMessage).addListener(FIRE_EXCEPTION_ON_FAILURE);
// fire the persisted messages in session
for (Subscription subscription : newSubscriptions) {
publishRetainedMessagesInSession(subscription, username);
}
boolean success = this.subscriptionInCourse.remove(executionKey, SubscriptionState.STORED);
if (!success) {
LOG.warn("Unable to perform the final subscription state update CId={}, messageId={}", clientID, messageID);
} else {
LOG.info("Client <{}> subscribed to topics", clientID);
}
}
private List<Subscription> doStoreSubscription(List<MqttTopicSubscription> ackTopics, String clientID) {
ClientSession clientSession = this.sessionsRepository.sessionForClient(clientID);
List<Subscription> newSubscriptions = new ArrayList<>();
for (MqttTopicSubscription req : ackTopics) {
// TODO this is SUPER UGLY
if (req.qualityOfService() == FAILURE) {
continue;
}
final Topic topic = new Topic(req.topicName());
Subscription newSubscription = new Subscription(clientID, topic, req.qualityOfService());
clientSession.subscribe(newSubscription);
newSubscriptions.add(newSubscription);
}
return newSubscriptions;
}
/**
* @param clientID
* the clientID
* @param username
* the username
* @param msg
* the subscribe message to verify
* @return the list of verified topics for the given subscribe message.
*/
private List<MqttTopicSubscription> doVerify(String clientID, String username, MqttSubscribeMessage msg) {
ClientSession clientSession = this.sessionsRepository.sessionForClient(clientID);
List<MqttTopicSubscription> ackTopics = new ArrayList<>();
final int messageId = messageId(msg);
for (MqttTopicSubscription req : msg.payload().topicSubscriptions()) {
Topic topic = new Topic(req.topicName());
if (!m_authorizator.canRead(topic, username, clientSession.clientID)) {
// send SUBACK with 0x80, the user hasn't credentials to read the topic
LOG.warn("Client does not have read permissions on the topic CId={}, username={}, messageId={}, " +
"topic={}", clientID, username, messageId, topic);
ackTopics.add(new MqttTopicSubscription(topic.toString(), FAILURE));
} else {
MqttQoS qos;
if (topic.isValid()) {
LOG.debug("Client will be subscribed to the topic CId={}, username={}, messageId={}, topic={}",
clientID, username, messageId, topic);
qos = req.qualityOfService();
} else {
LOG.warn("Topic filter is not valid CId={}, username={}, messageId={}, topic={}", clientID,
username, messageId, topic);
qos = FAILURE;
}
ackTopics.add(new MqttTopicSubscription(topic.toString(), qos));
}
}
return ackTopics;
}
/**
* Create the SUBACK response from a list of topicFilters
*/
private MqttSubAckMessage doAckMessageFromValidateFilters(List<MqttTopicSubscription> topicFilters, int messageId) {
List<Integer> grantedQoSLevels = new ArrayList<>();
for (MqttTopicSubscription req : topicFilters) {
grantedQoSLevels.add(req.qualityOfService().value());
}
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.SUBACK, false, AT_MOST_ONCE, false, 0);
MqttSubAckPayload payload = new MqttSubAckPayload(grantedQoSLevels);
return new MqttSubAckMessage(fixedHeader, from(messageId), payload);
}
private void publishRetainedMessagesInSession(Subscription newSubscription, String username) {
LOG.debug("Retrieving retained messages CId={}, topics={}", newSubscription.getClientId(),
newSubscription.getTopicFilter());
// scans retained messages to be published to the new subscription
// TODO this is ugly, it does a linear scan on potential big dataset
Collection<IMessagesStore.StoredMessage> messages = m_messagesStore
.searchMatching(key -> key.match(newSubscription.getTopicFilter()));
if (!messages.isEmpty()) {
LOG.info("Publishing retained messages CId={}, topics={}, messagesNo={}",
newSubscription.getClientId(), newSubscription.getTopicFilter(), messages.size());
}
ClientSession targetSession = this.sessionsRepository.sessionForClient(newSubscription.getClientId());
// adapt the message QoS to subscriber's accepted accepted
for (IMessagesStore.StoredMessage msg : messages) {
int lowestQoS = Math.min(msg.getQos().value(), newSubscription.getRequestedQos().value());
MqttQoS qosToPublish = MqttQoS.valueOf(lowestQoS);
msg.setQos(qosToPublish);
}
this.internalRepublisher.publishRetained(targetSession, messages);
// notify the Observables
m_interceptor.notifyTopicSubscribed(newSubscription, username);
}
public void notifyChannelWritable(Channel channel) {
String clientID = NettyUtils.clientID(channel);
ClientSession clientSession = this.sessionsRepository.sessionForClient(clientID);
boolean emptyQueue = false;
while (channel.isWritable() && !emptyQueue) {
EnqueuedMessage msg = clientSession.poll();
if (msg == null) {
emptyQueue = true;
} else {
// recreate a publish from stored publish in queue
MqttPublishMessage pubMsg = createPublishForQos(msg.msg.getTopic(), msg.msg.getQos(),
msg.msg.getPayload(), msg.msg.isRetained(),
msg.messageId);
channel.write(pubMsg).addListener(FIRE_EXCEPTION_ON_FAILURE);
}
}
channel.flush();
}
public void addInterceptHandler(InterceptHandler interceptHandler) {
this.m_interceptor.addInterceptHandler(interceptHandler);
}
public void removeInterceptHandler(InterceptHandler interceptHandler) {
this.m_interceptor.removeInterceptHandler(interceptHandler);
}
public IMessagesStore getMessagesStore() {
return m_messagesStore;
}
public ISessionsStore getSessionsStore() {
return m_sessionsStore;
}
public void shutdown() {
if (m_interceptor != null)
m_interceptor.stop();
}
}
@@ -1,265 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl;
import io.moquette.BrokerConstants;
import io.moquette.interception.InterceptHandler;
import io.moquette.server.ConnectionDescriptorStore;
import io.moquette.server.Server;
import io.moquette.server.config.IConfig;
import io.moquette.server.config.IResourceLoader;
import io.moquette.spi.IMessagesStore;
import io.moquette.spi.ISessionsStore;
import io.moquette.spi.IStore;
import io.moquette.spi.ISubscriptionsStore;
import io.moquette.spi.impl.security.*;
import io.moquette.spi.impl.subscriptions.CTrieSubscriptionDirectory;
import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory;
import io.moquette.spi.impl.subscriptions.Subscription;
import io.moquette.spi.security.IAuthenticator;
import io.moquette.spi.security.IAuthorizatorPolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
/**
* It's main responsibility is bootstrap the ProtocolProcessor.
*/
public class ProtocolProcessorBootstrapper {
private static final Logger LOG = LoggerFactory.getLogger(ProtocolProcessorBootstrapper.class);
public static final String INMEMDB_STORE_CLASS = "io.moquette.persistence.MemoryStorageService";
private ISessionsStore m_sessionsStore;
private ISubscriptionsStore subscriptionsStore;
private Runnable storeShutdown;
private final ProtocolProcessor m_processor = new ProtocolProcessor();
private ConnectionDescriptorStore connectionDescriptors;
public ProtocolProcessorBootstrapper() {
}
/**
* Initialize the processing part of the broker.
*
* @param props
* the properties carrier where some props like port end host could be loaded. For
* the full list check of configurable properties check moquette.conf file.
* @param embeddedObservers
* a list of callbacks to be notified of certain events inside the broker. Could be
* empty list of null.
* @param authenticator
* an implementation of the authenticator to be used, if null load that specified in
* config and fallback on the default one (permit all).
* @param authorizator
* an implementation of the authorizator to be used, if null load that specified in
* config and fallback on the default one (permit all).
* @param server
* the server to init.
* @return the processor created for the broker.
*/
public ProtocolProcessor init(IConfig props, List<? extends InterceptHandler> embeddedObservers,
IAuthenticator authenticator, IAuthorizatorPolicy authorizator, Server server) {
IMessagesStore messagesStore;
LOG.info("Initializing messages and sessions stores...");
String storageClassName = props.getProperty(BrokerConstants.STORAGE_CLASS_NAME, INMEMDB_STORE_CLASS);
if (storageClassName == null || storageClassName.isEmpty()) {
LOG.error("storage_class property not defined");
throw new IllegalArgumentException("Can't find a valid persistence layer");
}
final IStore store = instantiateConfiguredStore(storageClassName, props, server.getScheduler());
if (store == null) {
throw new IllegalArgumentException("Can't start the persistence layer");
}
store.initStore();
messagesStore = store.messagesStore();
m_sessionsStore = store.sessionsStore();
SessionsRepository sessionsRepository = new SessionsRepository(this.m_sessionsStore, server.getScheduler());
sessionsRepository.init();
this.subscriptionsStore = m_sessionsStore.subscriptionStore();
storeShutdown = new Runnable() {
@Override
public void run() {
store.close();
}
};
LOG.info("Configuring message interceptors...");
List<InterceptHandler> observers = new ArrayList<>(embeddedObservers);
String interceptorClassName = props.getProperty(BrokerConstants.INTERCEPT_HANDLER_PROPERTY_NAME);
if (interceptorClassName != null && !interceptorClassName.isEmpty()) {
InterceptHandler handler = loadClass(interceptorClassName, InterceptHandler.class, Server.class, server);
if (handler != null) {
observers.add(handler);
}
}
BrokerInterceptor interceptor = new BrokerInterceptor(props, observers);
LOG.info("Initializing subscriptions store...");
ISubscriptionsDirectory subscriptions = new CTrieSubscriptionDirectory();
subscriptions.init(sessionsRepository);
LOG.info("Configuring MQTT authenticator...");
String authenticatorClassName = props.getProperty(BrokerConstants.AUTHENTICATOR_CLASS_NAME, "");
if (authenticator == null && !authenticatorClassName.isEmpty()) {
authenticator = loadClass(authenticatorClassName, IAuthenticator.class, IConfig.class, props);
}
IResourceLoader resourceLoader = props.getResourceLoader();
if (authenticator == null) {
String passwdPath = props.getProperty(BrokerConstants.PASSWORD_FILE_PROPERTY_NAME, "");
if (passwdPath.isEmpty()) {
authenticator = new AcceptAllAuthenticator();
} else {
authenticator = new ResourceAuthenticator(resourceLoader, passwdPath);
}
LOG.info("An {} authenticator instance will be used", authenticator.getClass().getName());
}
LOG.info("Configuring MQTT authorizator...");
String authorizatorClassName = props.getProperty(BrokerConstants.AUTHORIZATOR_CLASS_NAME, "");
if (authorizator == null && !authorizatorClassName.isEmpty()) {
authorizator = loadClass(authorizatorClassName, IAuthorizatorPolicy.class, IConfig.class, props);
}
if (authorizator == null) {
String aclFilePath = props.getProperty(BrokerConstants.ACL_FILE_PROPERTY_NAME, "");
if (aclFilePath != null && !aclFilePath.isEmpty()) {
authorizator = new DenyAllAuthorizatorPolicy();
try {
LOG.info("Parsing ACL file. Path = {}", aclFilePath);
authorizator = ACLFileParser.parse(resourceLoader.loadResource(aclFilePath));
} catch (ParseException pex) {
LOG.error("Unable to parse ACL file. path=" + aclFilePath, pex);
}
} else {
authorizator = new PermitAllAuthorizatorPolicy();
}
LOG.info("An {} authorizator instance will be used", authorizator.getClass().getName());
}
LOG.info("Initializing connection descriptor store...");
connectionDescriptors = new ConnectionDescriptorStore();
LOG.info("Initializing MQTT protocol processor...");
boolean allowAnonymous = props.boolProp(BrokerConstants.ALLOW_ANONYMOUS_PROPERTY_NAME, true);
boolean allowZeroByteClientId = props.boolProp(BrokerConstants.ALLOW_ZERO_BYTE_CLIENT_ID_PROPERTY_NAME, false);
boolean reauthorizeSubscriptionsOnConnect = props.boolProp(BrokerConstants.REAUTHORIZE_SUBSCRIPTIONS_ON_CONNECT, false);
m_processor.init(connectionDescriptors, subscriptions, messagesStore, m_sessionsStore, authenticator,
allowAnonymous, allowZeroByteClientId, authorizator, interceptor,
sessionsRepository, reauthorizeSubscriptionsOnConnect);
return m_processor;
}
private IStore instantiateConfiguredStore(String storageClassName, IConfig props,
ScheduledExecutorService scheduledExecutor) {
LOG.info("Loading storage class {}", storageClassName);
Class<? extends IStore> storageClass;
try {
storageClass = this.getClass().getClassLoader()
.loadClass(storageClassName)
.asSubclass(IStore.class);
} catch (ClassNotFoundException cnfex) {
LOG.error("Cannot find storage class " + storageClassName + " in classpath", cnfex);
return null;
}
final Constructor<? extends IStore> constructor;
try {
constructor = storageClass
.getConstructor(IConfig.class, ScheduledExecutorService.class);
} catch (NoSuchMethodException nsmex) {
LOG.error("Cannot find constructor with required params IConfig, ScheduledExecutorService ", nsmex);
return null;
}
try {
return constructor.newInstance(props, scheduledExecutor);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
LOG.error("Cannot instantiate the " + storageClassName + " instance", ex);
}
return null;
}
@SuppressWarnings("unchecked")
private <T, U> T loadClass(String className, Class<T> intrface, Class<U> constructorArgClass, U props) {
T instance = null;
try {
// check if constructor with constructor arg class parameter
// exists
LOG.info("Invoking constructor with {} argument. ClassName={}, interfaceName={}",
constructorArgClass.getName(), className, intrface.getName());
instance = this.getClass().getClassLoader()
.loadClass(className)
.asSubclass(intrface)
.getConstructor(constructorArgClass)
.newInstance(props);
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException ex) {
LOG.warn("Unable to invoke constructor with {} argument. ClassName={}, interfaceName={}, cause={}, " +
"errorMessage={}", constructorArgClass.getName(), className, intrface.getName(), ex.getCause(),
ex.getMessage());
return null;
} catch (NoSuchMethodException | InvocationTargetException e) {
try {
LOG.info("Invoking default constructor. ClassName={}, interfaceName={}", className, intrface.getName());
// fallback to default constructor
instance = this.getClass().getClassLoader()
.loadClass(className)
.asSubclass(intrface)
.getDeclaredConstructor().newInstance();
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException |
NoSuchMethodException | InvocationTargetException ex) {
LOG.error("Unable to invoke default constructor. ClassName={}, interfaceName={}, cause={}, " +
"errorMessage={}", className, intrface.getName(), ex.getCause(), ex.getMessage());
return null;
}
}
return instance;
}
public ISessionsStore getSessionsStore() {
return m_sessionsStore;
}
public List<Subscription> getSubscriptions() {
return this.subscriptionsStore.listAllSubscriptions();
}
public void shutdown() {
if (storeShutdown != null)
storeShutdown.run();
if (m_processor != null)
m_processor.shutdown();
}
public ConnectionDescriptorStore getConnectionDescriptors() {
return connectionDescriptors;
}
}
@@ -1,68 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl;
import io.moquette.server.netty.NettyUtils;
import io.moquette.spi.IMessagesStore;
import io.moquette.spi.impl.subscriptions.Topic;
import io.moquette.spi.security.IAuthorizatorPolicy;
import io.netty.channel.Channel;
import io.netty.handler.codec.mqtt.MqttPublishMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static io.moquette.spi.impl.ProtocolProcessor.asStoredMessage;
class Qos0PublishHandler extends QosPublishHandler {
private static final Logger LOG = LoggerFactory.getLogger(Qos0PublishHandler.class);
private final IMessagesStore m_messagesStore;
private final BrokerInterceptor m_interceptor;
private final MessagesPublisher publisher;
public Qos0PublishHandler(IAuthorizatorPolicy authorizator, IMessagesStore messagesStore, BrokerInterceptor interceptor,
MessagesPublisher messagesPublisher) {
super(authorizator);
this.m_messagesStore = messagesStore;
this.m_interceptor = interceptor;
this.publisher = messagesPublisher;
}
void receivedPublishQos0(Channel channel, MqttPublishMessage msg) {
// verify if topic can be write
final Topic topic = new Topic(msg.variableHeader().topicName());
String clientID = NettyUtils.clientID(channel);
String username = NettyUtils.userName(channel);
if (!m_authorizator.canWrite(topic, username, clientID)) {
LOG.error("MQTT client is not authorized to publish on topic. CId={}, topic={}", clientID, topic);
return;
}
// route message to subscribers
IMessagesStore.StoredMessage toStoreMsg = asStoredMessage(msg);
toStoreMsg.setClientID(clientID);
this.publisher.publish2Subscribers(toStoreMsg, topic);
if (msg.fixedHeader().isRetain()) {
// QoS == 0 && retain => clean old retained
m_messagesStore.cleanRetained(topic);
}
m_interceptor.notifyTopicPublished(msg, clientID, username);
}
}
@@ -1,118 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl;
import io.moquette.connections.IConnectionsManager;
import io.moquette.server.ConnectionDescriptor;
import io.moquette.server.netty.NettyUtils;
import io.moquette.spi.IMessagesStore;
import io.moquette.spi.impl.subscriptions.Topic;
import io.moquette.spi.security.IAuthorizatorPolicy;
import io.netty.channel.Channel;
import io.netty.handler.codec.mqtt.MqttFixedHeader;
import io.netty.handler.codec.mqtt.MqttMessageType;
import io.netty.handler.codec.mqtt.MqttPubAckMessage;
import io.netty.handler.codec.mqtt.MqttPublishMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Optional;
import static io.moquette.spi.impl.ProtocolProcessor.asStoredMessage;
import static io.netty.channel.ChannelFutureListener.CLOSE_ON_FAILURE;
import static io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader.from;
import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE;
class Qos1PublishHandler extends QosPublishHandler {
private static final Logger LOG = LoggerFactory.getLogger(Qos1PublishHandler.class);
private final IMessagesStore m_messagesStore;
private final BrokerInterceptor m_interceptor;
private final IConnectionsManager connectionDescriptors;
private final MessagesPublisher publisher;
Qos1PublishHandler(IAuthorizatorPolicy authorizator, IMessagesStore messagesStore, BrokerInterceptor interceptor,
IConnectionsManager connectionDescriptors, MessagesPublisher messagesPublisher) {
super(authorizator);
this.m_messagesStore = messagesStore;
this.m_interceptor = interceptor;
this.connectionDescriptors = connectionDescriptors;
this.publisher = messagesPublisher;
}
void receivedPublishQos1(Channel channel, MqttPublishMessage msg) {
// verify if topic can be write
final Topic topic = new Topic(msg.variableHeader().topicName());
topic.getTokens();
if (!topic.isValid()) {
LOG.warn("Invalid topic format, force close the connection");
channel.close().addListener(CLOSE_ON_FAILURE);
return;
}
String clientID = NettyUtils.clientID(channel);
String username = NettyUtils.userName(channel);
if (!m_authorizator.canWrite(topic, username, clientID)) {
LOG.error("MQTT client is not authorized to publish on topic. CId={}, topic={}", clientID, topic);
return;
}
final int messageID = msg.variableHeader().messageId();
// route message to subscribers
IMessagesStore.StoredMessage toStoreMsg = asStoredMessage(msg);
toStoreMsg.setClientID(clientID);
this.publisher.publish2Subscribers(toStoreMsg, topic, messageID);
sendPubAck(clientID, messageID);
if (msg.fixedHeader().isRetain()) {
if (!msg.payload().isReadable()) {
m_messagesStore.cleanRetained(topic);
} else {
// before wasn't stored
m_messagesStore.storeRetained(topic, toStoreMsg);
}
}
m_interceptor.notifyTopicPublished(msg, clientID, username);
}
private void sendPubAck(String clientId, int messageID) {
LOG.trace("sendPubAck invoked");
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBACK, false, AT_MOST_ONCE, false, 0);
MqttPubAckMessage pubAckMessage = new MqttPubAckMessage(fixedHeader, from(messageID));
try {
LOG.trace("connected clientIDs are {}", connectionDescriptors.getConnectedClientIds());
if (!connectionDescriptors.isConnected(clientId)) {
throw new RuntimeException(String.format("Can't find a ConnectionDescriptor for client %s in cache %s",
clientId, connectionDescriptors));
}
LOG.info("Sending {} message CId=<{}>", MqttMessageType.PUBACK, clientId);
final Optional<ConnectionDescriptor> optDescriptor = connectionDescriptors.lookupDescriptor(clientId);
if (optDescriptor.isPresent()) {
final ConnectionDescriptor descriptor = optDescriptor.get();
descriptor.writeAndFlush(pubAckMessage);
}
} catch (Throwable t) {
LOG.error(null, t);
}
}
}
@@ -1,164 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl;
import io.moquette.connections.IConnectionsManager;
import io.moquette.server.ConnectionDescriptor;
import io.moquette.server.netty.NettyUtils;
import io.moquette.spi.ClientSession;
import io.moquette.spi.IMessagesStore;
import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory;
import io.moquette.spi.impl.subscriptions.Topic;
import io.moquette.spi.security.IAuthorizatorPolicy;
import io.netty.channel.Channel;
import io.netty.handler.codec.mqtt.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Optional;
import static io.moquette.spi.impl.DebugUtils.payload2Str;
import static io.moquette.spi.impl.ProtocolProcessor.asStoredMessage;
import static io.moquette.spi.impl.Utils.messageId;
import static io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader.from;
import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE;
class Qos2PublishHandler extends QosPublishHandler {
private static final Logger LOG = LoggerFactory.getLogger(Qos2PublishHandler.class);
private final ISubscriptionsDirectory subscriptions;
private final IMessagesStore m_messagesStore;
private final BrokerInterceptor m_interceptor;
private final IConnectionsManager connectionDescriptors;
private final MessagesPublisher publisher;
private final SessionsRepository sessionsRepository;
Qos2PublishHandler(IAuthorizatorPolicy authorizator, ISubscriptionsDirectory subscriptions,
IMessagesStore messagesStore, BrokerInterceptor interceptor,
IConnectionsManager connectionDescriptors,
MessagesPublisher messagesPublisher, SessionsRepository sessionsRepository) {
super(authorizator);
this.subscriptions = subscriptions;
this.m_messagesStore = messagesStore;
this.m_interceptor = interceptor;
this.connectionDescriptors = connectionDescriptors;
this.publisher = messagesPublisher;
this.sessionsRepository = sessionsRepository;
}
void receivedPublishQos2(Channel channel, MqttPublishMessage msg) {
final Topic topic = new Topic(msg.variableHeader().topicName());
// check if the topic can be wrote
String clientID = NettyUtils.clientID(channel);
String username = NettyUtils.userName(channel);
if (!m_authorizator.canWrite(topic, username, clientID)) {
LOG.error("MQTT client is not authorized to publish on topic. CId={}, topic={}", clientID, topic);
return;
}
final int messageID = msg.variableHeader().messageId();
IMessagesStore.StoredMessage toStoreMsg = asStoredMessage(msg);
toStoreMsg.setClientID(clientID);
LOG.info("Sending publish message to subscribers CId={}, topic={}, messageId={}", clientID, topic, messageID);
if (LOG.isTraceEnabled()) {
LOG.trace("payload={}, subs Tree={}", payload2Str(toStoreMsg.getPayload()), subscriptions.dumpTree());
}
this.sessionsRepository.sessionForClient(clientID).markAsInboundInflight(messageID, toStoreMsg);
sendPubRec(clientID, messageID);
// Next the client will send us a pub rel
// NB publish to subscribers for QoS 2 happen upon PUBREL from publisher
// if (msg.fixedHeader().isRetain()) {
// if (msg.payload().readableBytes() == 0) {
// m_messagesStore.cleanRetained(topic);
// } else {
// m_messagesStore.storeRetained(topic, toStoreMsg);
// }
// }
//TODO this should happen on PUB_REL, else we notify false positive
m_interceptor.notifyTopicPublished(msg, clientID, username);
}
/**
* Second phase of a publish QoS2 protocol, sent by publisher to the broker. Search the stored
* message and publish to all interested subscribers.
*/
void processPubRel(Channel channel, MqttMessage msg) {
String clientID = NettyUtils.clientID(channel);
int messageID = messageId(msg);
LOG.info("Processing PUBREL message. CId={}, messageId={}", clientID, messageID);
ClientSession targetSession = this.sessionsRepository.sessionForClient(clientID);
IMessagesStore.StoredMessage evt = targetSession.inboundInflight(messageID);
if (evt == null) {
LOG.warn("Can't find inbound inflight message for CId={}, messageId={}", clientID, messageID);
throw new IllegalArgumentException("Can't find inbound inflight message");
}
final Topic topic = new Topic(evt.getTopic());
this.publisher.publish2Subscribers(evt, topic, messageID);
if (evt.isRetained()) {
if (evt.getPayload().readableBytes() == 0) {
m_messagesStore.cleanRetained(topic);
} else {
m_messagesStore.storeRetained(topic, evt);
}
}
//TODO here we should notify to the listeners
//m_interceptor.notifyTopicPublished(msg, clientID, username);
sendPubComp(clientID, messageID);
}
private void sendPubRec(String clientID, int messageID) {
LOG.trace("Sending PUBREC message. CId={}, messageId={}", clientID, messageID);
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBREC, false, AT_MOST_ONCE, false, 0);
MqttMessage pubRecMessage = new MqttMessage(fixedHeader, from(messageID));
final Optional<ConnectionDescriptor> connectionDescriptor = connectionDescriptors.lookupDescriptor(clientID);
if (connectionDescriptor.isPresent()) {
final ConnectionDescriptor descriptor = connectionDescriptor.get();
try {
descriptor.writeAndFlush(pubRecMessage);
} catch (Throwable e) {
LOG.error("Unable to send {} message. CId=<{}>, messageId={}", MqttMessageType.PUBREC, clientID,
messageID, e);
}
}
}
private void sendPubComp(String clientID, int messageID) {
LOG.trace("Sending PUBCOMP message. CId={}, messageId={}", clientID, messageID);
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, AT_MOST_ONCE, false, 0);
MqttMessage pubCompMessage = new MqttMessage(fixedHeader, from(messageID));
final Optional<ConnectionDescriptor> connectionDescriptor = connectionDescriptors.lookupDescriptor(clientID);
if (connectionDescriptor.isPresent()) {
final ConnectionDescriptor descriptor = connectionDescriptor.get();
try {
descriptor.writeAndFlush(pubCompMessage);
} catch (Throwable e) {
LOG.error("Unable to send {} message. CId=<{}>, messageId={}", MqttMessageType.PUBCOMP, clientID,
messageID, e);
}
}
}
}
@@ -1,45 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.moquette.server.netty.NettyUtils;
import io.moquette.spi.impl.subscriptions.Topic;
import io.moquette.spi.security.IAuthorizatorPolicy;
import io.netty.channel.Channel;
abstract class QosPublishHandler {
private static final Logger LOG = LoggerFactory.getLogger(QosPublishHandler.class);
protected final IAuthorizatorPolicy m_authorizator;
protected QosPublishHandler(IAuthorizatorPolicy m_authorizator) {
this.m_authorizator = m_authorizator;
}
public boolean checkWriteOnTopic(Topic topic, Channel channel) {
String clientID = NettyUtils.clientID(channel);
String username = NettyUtils.userName(channel);
if (!m_authorizator.canWrite(topic, username, clientID)) {
LOG.error("MQTT client is not authorized to publish on topic. CId={}, topic={}", clientID, topic);
return true;
}
return false;
}
}
@@ -1,48 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl;
class RunningSubscription {
final String clientID;
final long packetId;
RunningSubscription(String clientID, long packetId) {
this.clientID = clientID;
this.packetId = packetId;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
RunningSubscription that = (RunningSubscription) o;
return packetId == that.packetId
&& (clientID != null ? clientID.equals(that.clientID) : that.clientID == null);
}
@Override
public int hashCode() {
int result = clientID != null ? clientID.hashCode() : 0;
result = 31 * result + (int) (packetId ^ (packetId >>> 32));
return result;
}
}
@@ -1,158 +0,0 @@
/*
* Copyright (c) 2012-2018 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.moquette.spi.impl;
import io.moquette.persistence.PersistentSession;
import io.moquette.spi.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class SessionsRepository {
/**
* Task to be scheduled to execute the cleaning of persisted sessions (clean flag=false) older than a
* defined period.
*/
private class SessionCleanerTask implements Runnable {
@Override
public void run() {
wipeExpiredSessions();
}
}
private static final Logger LOG = LoggerFactory.getLogger(SessionsRepository.class);
private final ISessionsStore sessions;
private ISubscriptionsStore subscriptionsStore;
private ScheduledExecutorService scheduler;
private final ConcurrentMap<String, ClientSession> sessionsCache = new ConcurrentHashMap<>();
// maps clientID to Will testament, if specified on CONNECT
// maps clientID to Will testament, if specified on CONNECT
private ConcurrentMap<String, WillMessage> willMessages = new ConcurrentHashMap<>();
public SessionsRepository(ISessionsStore sessionsStore, ScheduledExecutorService scheduler) {
this.sessions = sessionsStore;
this.subscriptionsStore = sessionsStore.subscriptionStore();
this.scheduler = scheduler;
}
@SuppressWarnings("FutureReturnValueIgnored")
public void init() {
SessionCleanerTask cleanerTask = new SessionCleanerTask();
this.scheduler.schedule(cleanerTask, 1, TimeUnit.HOURS);
}
public ClientSession sessionForClient(String clientID) {
LOG.debug("Retrieving session. CId={}", clientID);
if (this.sessionsCache.containsKey(clientID)) {
return this.sessionsCache.get(clientID);
}
if (!this.sessions.contains(clientID)) {
LOG.warn("Session does not exist. CId={}", clientID);
return null;
}
PersistentSession storedSession = this.sessions.loadSessionByKey(clientID);
return newClientSessionAndCacheIt(storedSession.clientID, storedSession.cleanSession);
}
private ClientSession newClientSessionAndCacheIt(String clientID, boolean cleanSession) {
ClientSession session;
if (cleanSession) {
session = new TransientSession(clientID, this.willMessages);
} else {
DurableSession durableSession = new DurableSession(clientID, this.sessions, this.subscriptionsStore, willMessages);
durableSession.reloadAllSubscriptionsFromStore();
session = durableSession;
}
this.sessionsCache.put(clientID, session);
return session;
}
public ClientSession createNewSession(String clientID, boolean cleanSession) {
if (sessions.contains(clientID)) {
LOG.error("Unable to create a new session: the client ID is already in use. CId={}, cleanSession={}",
clientID, cleanSession);
throw new IllegalArgumentException("Can't create a session with the ID of an already existing" + clientID);
}
LOG.debug("Creating new session. CId={}, cleanSession={}", clientID, cleanSession);
if (!cleanSession) {
sessions.createNewDurableSession(clientID);
}
return newClientSessionAndCacheIt(clientID, cleanSession);
}
public Collection<ClientSession> getAllSessions() {
Collection<ClientSession> result = new ArrayList<>();
for (PersistentSession persistentSession : this.sessions.listAllSessions()) {
result.add(sessionForClient(persistentSession.clientID));
}
return result;
}
ClientSession createOrLoadClientSession(String clientId, boolean cleanSession) {
ClientSession clientSession = this.sessionForClient(clientId);
if (clientSession == null) {
clientSession = this.createNewSession(clientId, cleanSession);
} else {
//session was already present
if (!clientSession.isCleanSession() && cleanSession) {
//remove from storage & create new transient session
//TODO existing subscription from durable session has to be copied into the new transient?
sessions.removeDurableSession(clientId);
clientSession = this.createNewSession(clientId, true);
} /*else {
clientSession.cleanSession(cleanSession);
this.updateCleanStatus(clientId, cleanSession);
}*/
}
if (cleanSession) {
LOG.info("Cleaning session. CId={}", clientId);
clientSession.cleanSession();
}
return clientSession;
}
public void disconnect(String clientId) {
LOG.debug("Removing session from repository's cache");
ClientSession clientSession = this.sessionForClient(clientId);
if (clientSession == null) {
return;
}
sessionsCache.remove(clientId);
this.sessions.trackSessionClose(LocalDateTime.now(), clientId);
}
private void wipeExpiredSessions() {
final LocalDateTime pin = LocalDateTime.now().minus(6, ChronoUnit.DAYS);
final Set<String> expiredSessionsIds = this.sessions.sessionOlderThan(pin);
for (String expiredSession : expiredSessionsIds) {
this.sessions.removeDurableSession(expiredSession);
this.subscriptionsStore.wipeSubscriptions(expiredSession);
}
}
}
@@ -1,36 +0,0 @@
package io.moquette.spi.impl;
import io.netty.handler.codec.mqtt.MqttQoS;
import java.nio.ByteBuffer;
public final class WillMessage {
private final String topic;
private final ByteBuffer payload;
private final boolean retained;
private final MqttQoS qos;
public WillMessage(String topic, ByteBuffer payload, boolean retained, MqttQoS qos) {
this.topic = topic;
this.payload = payload;
this.retained = retained;
this.qos = qos;
}
public String getTopic() {
return topic;
}
public ByteBuffer getPayload() {
return payload;
}
public boolean isRetained() {
return retained;
}
public MqttQoS getQos() {
return qos;
}
}
@@ -82,7 +82,18 @@ class CNode {
}
CNode addSubscription(Subscription newSubscription) {
this.subscriptions.add(new Subscription(newSubscription));
// if already contains one with same topic and same client, keep that with higher QoS
if (subscriptions.contains(newSubscription)) {
final Subscription existing = subscriptions.stream()
.filter(s -> s.equals(newSubscription))
.findFirst().get();
if (existing.getRequestedQos().value() < newSubscription.getRequestedQos().value()) {
subscriptions.remove(existing);
subscriptions.add(new Subscription(newSubscription));
}
} else {
this.subscriptions.add(new Subscription(newSubscription));
}
return this;
}
@@ -15,15 +15,12 @@
*/
package io.moquette.spi.impl.subscriptions;
import io.moquette.spi.ClientSession;
import io.moquette.spi.impl.SessionsRepository;
import io.moquette.broker.ISubscriptionsRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import static java.lang.String.format;
public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
private static final Logger LOG = LoggerFactory.getLogger(CTrieSubscriptionDirectory.class);
@@ -32,7 +29,7 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
private static final INode NO_PARENT = null;
INode root;
private volatile SessionsRepository sessionsRepository;
private volatile ISubscriptionsRepository sessionsRepository;
interface IVisitor<T> {
@@ -46,7 +43,7 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
}
@Override
public void init(SessionsRepository sessionsRepository) {
public void init(ISubscriptionsRepository sessionsRepository) {
LOG.info("Initializing CTrie");
final CNode mainNode = new CNode();
mainNode.token = ROOT;
@@ -58,12 +55,11 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
if (LOG.isTraceEnabled()) {
LOG.trace("Reloading all stored subscriptions. SubscriptionTree = {}", dumpTree());
}
for (ClientSession session : this.sessionsRepository.getAllSessions()) {
for (Subscription subscription : session.getSubscriptions()) {
LOG.info("Re-subscribing client to topic CId={}, topicFilter={}", subscription.clientId,
subscription.topicFilter);
add(subscription);
}
for (Subscription subscription : this.sessionsRepository.listAllSubscriptions()) {
LOG.info("Re-subscribing client to topic CId={}, topicFilter={}", subscription.clientId,
subscription.topicFilter);
add(subscription);
}
if (LOG.isTraceEnabled()) {
LOG.trace("Stored subscriptions have been reloaded. SubscriptionTree = {}", dumpTree());
@@ -86,7 +82,7 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
@Override
public List<Subscription> matches(Topic topic) {
return new ArrayList<>(match(topic));
return new ArrayList<>(matchQosSharpening(topic));
}
/**
@@ -118,47 +114,6 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
return new HashSet<>(subsGroupedByClient.values());
}
/**
* Given a topic string return the clients subscriptions that matches it. Topic string can't
* contain character # and + because they are reserved to listeners subscriptions, and not topic
* publishing.
*
* @param topic
* to use fo searching matching subscriptions.
* @return the list of matching subscriptions, or empty if not matching.
*/
@Deprecated
Set<Subscription> match(Topic topic) {
final Set<Subscription> matchingSubs = matchWithoutQosSharpening(topic);
// remove the overlapping subscriptions, selecting ones with greatest qos
Map<String, Subscription> subsForClient = loadSubscriptionsFromStorage(matchingSubs);
return new HashSet<>(subsForClient.values());
}
@Deprecated
private Map<String, Subscription> loadSubscriptionsFromStorage(Set<Subscription> matchingSubs) {
Map<String, Subscription> subsForClient = new HashMap<>();
for (Subscription matchingSub : matchingSubs) {
Subscription existingSub = subsForClient.get(matchingSub.clientId);
final ClientSession subscribedSession = this.sessionsRepository.sessionForClient(matchingSub.clientId);
if (subscribedSession == null) {
//clean session disconnected
continue;
}
Subscription sub = subscribedSession.findSubscriptionByTopicFilter(matchingSub);
if (sub == null) {
final String excpMesg = format("Target session %s is connected but doesn't anymore subscribed to %s",
matchingSub.clientId, matchingSub);
throw new IllegalStateException(excpMesg);
}
// update the selected subscriptions if not present or if has a greater qos
if (existingSub == null || existingSub.qosLessThan(sub)) {
subsForClient.put(sub.clientId, sub);
}
}
return subsForClient;
}
Set<Subscription> recursiveMatch(Topic topic, INode inode) {
CNode cnode = inode.mainNode();
if (Token.MULTI.equals(cnode.token)) {
@@ -15,14 +15,14 @@
*/
package io.moquette.spi.impl.subscriptions;
import io.moquette.spi.impl.SessionsRepository;
import io.moquette.broker.ISubscriptionsRepository;
import java.util.List;
import java.util.Set;
public interface ISubscriptionsDirectory {
void init(SessionsRepository sessionsRepository);
void init(ISubscriptionsRepository sessionsRepository);
Set<Subscription> matchWithoutQosSharpening(Topic topic);