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
@@ -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);