Removed all the old ProtocolProcessor stuff
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+9
-54
@@ -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)) {
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user