From a2266e5bbea3034ffad2c80bcf3cd58030b5976a Mon Sep 17 00:00:00 2001 From: andsel Date: Sun, 12 Aug 2018 15:50:22 +0200 Subject: [PATCH] Rearchitected the broker, switching from ProtocolProcessor and publisher to MQTTConnection, Session and PostOffice --- broker/build.gradle | 2 +- .../java/io/moquette/broker/Authorizator.java | 80 +++ .../moquette/broker/BrokerConfiguration.java | 36 + .../moquette/broker/IRetainedRepository.java | 14 + .../io/moquette/broker/InflightResender.java | 162 +++++ .../io/moquette/broker/MQTTConnection.java | 435 ++++++++++++ .../broker/MQTTConnectionFactory.java | 24 + .../broker/MemoryRetainedRepository.java | 35 + .../io/moquette/broker/NewNettyAcceptor.java | 383 ++++++++++ .../moquette/broker/NewNettyMQTTHandler.java | 115 +++ .../java/io/moquette/broker/PostOffice.java | 274 ++++++++ .../main/java/io/moquette/broker/Server.java | 142 ++++ .../main/java/io/moquette/broker/Session.java | 341 +++++++++ .../broker/SessionCorruptedException.java | 10 + .../io/moquette/broker/SessionRegistry.java | 220 ++++++ .../main/java/io/moquette/server/Server.java | 4 +- .../netty/MoquetteIdleTimeoutHandler.java | 2 +- .../io/moquette/spi/impl/ConnectHandler.java | 6 +- .../java/io/moquette/spi/impl/DebugUtils.java | 15 +- .../moquette/spi/impl/ProtocolProcessor.java | 12 +- .../impl/ProtocolProcessorBootstrapper.java | 10 +- .../moquette/spi/impl/Qos0PublishHandler.java | 4 +- .../moquette/spi/impl/Qos1PublishHandler.java | 5 +- .../moquette/spi/impl/Qos2PublishHandler.java | 6 +- .../moquette/spi/impl/QosPublishHandler.java | 6 +- .../security/AuthorizationsCollector.java | 4 +- ...or.java => DenyAllAuthorizatorPolicy.java} | 4 +- ....java => PermitAllAuthorizatorPolicy.java} | 4 +- .../spi/impl/subscriptions/CNode.java | 6 +- .../CTrieSubscriptionDirectory.java | 70 +- .../ISubscriptionsDirectory.java | 5 + .../spi/impl/subscriptions/TNode.java | 2 +- ...orizator.java => IAuthorizatorPolicy.java} | 2 +- .../moquette/broker/ConnectionTestUtils.java | 68 ++ .../broker/MQTTConnectionConnectTest.java | 307 ++++++++ .../broker/MQTTConnectionPublishTest.java | 81 +++ .../broker/PostOfficeInternalPublishTest.java | 298 ++++++++ .../broker/PostOfficePublishTest.java | 381 ++++++++++ .../broker/PostOfficeSubscribeTest.java | 308 +++++++++ .../broker/PostOfficeUnsubscribeTest.java | 352 ++++++++++ .../moquette/broker/SessionRegistryTest.java | 126 ++++ .../server/ConfigurationClassLoaderTest.java | 4 +- ...nPahoCanPublishOnReadBlockedTopicTest.java | 4 +- .../AbstractProtocolProcessorCommonUtils.java | 32 +- .../moquette/spi/impl/ConnectHandlerTest.java | 315 --------- .../moquette/spi/impl/MockAuthenticator.java | 4 +- .../spi/impl/NettyChannelAssertions.java | 10 +- .../spi/impl/ProtocolProcessorTest.java | 493 ++++++------- .../impl/ProtocolProcessor_CONNECT_Test.java | 4 +- ...rotocolProcessor_InternalPublish_Test.java | 3 - ...rotocolProcessor_VariousUseCases_Test.java | 654 +++++++++--------- broker/src/test/resources/log4j.properties | 8 +- perf/build.gradle | 8 + .../clients/mqttbee/BeeBenchmarker.java | 100 +++ .../clients/paho/PahoBenchmarker.java | 56 ++ .../clients/paho/SubscriberCallback.java | 63 ++ tools_scripts/benchmark/benchmarker.groovy | 12 +- .../integration/allAroundWalk.groovy | 96 +++ .../issue39_loopingConnecter.groovy | 4 +- tools_scripts/integration/pahoConnect.groovy | 19 + 60 files changed, 5225 insertions(+), 1025 deletions(-) create mode 100644 broker/src/main/java/io/moquette/broker/Authorizator.java create mode 100644 broker/src/main/java/io/moquette/broker/BrokerConfiguration.java create mode 100644 broker/src/main/java/io/moquette/broker/IRetainedRepository.java create mode 100644 broker/src/main/java/io/moquette/broker/InflightResender.java create mode 100644 broker/src/main/java/io/moquette/broker/MQTTConnection.java create mode 100644 broker/src/main/java/io/moquette/broker/MQTTConnectionFactory.java create mode 100644 broker/src/main/java/io/moquette/broker/MemoryRetainedRepository.java create mode 100644 broker/src/main/java/io/moquette/broker/NewNettyAcceptor.java create mode 100644 broker/src/main/java/io/moquette/broker/NewNettyMQTTHandler.java create mode 100644 broker/src/main/java/io/moquette/broker/PostOffice.java create mode 100644 broker/src/main/java/io/moquette/broker/Server.java create mode 100644 broker/src/main/java/io/moquette/broker/Session.java create mode 100644 broker/src/main/java/io/moquette/broker/SessionCorruptedException.java create mode 100644 broker/src/main/java/io/moquette/broker/SessionRegistry.java rename broker/src/main/java/io/moquette/spi/impl/security/{DenyAllAuthorizator.java => DenyAllAuthorizatorPolicy.java} (87%) rename broker/src/main/java/io/moquette/spi/impl/security/{PermitAllAuthorizator.java => PermitAllAuthorizatorPolicy.java} (87%) rename broker/src/main/java/io/moquette/spi/security/{IAuthorizator.java => IAuthorizatorPolicy.java} (97%) create mode 100644 broker/src/test/java/io/moquette/broker/ConnectionTestUtils.java create mode 100644 broker/src/test/java/io/moquette/broker/MQTTConnectionConnectTest.java create mode 100644 broker/src/test/java/io/moquette/broker/MQTTConnectionPublishTest.java create mode 100644 broker/src/test/java/io/moquette/broker/PostOfficeInternalPublishTest.java create mode 100644 broker/src/test/java/io/moquette/broker/PostOfficePublishTest.java create mode 100644 broker/src/test/java/io/moquette/broker/PostOfficeSubscribeTest.java create mode 100644 broker/src/test/java/io/moquette/broker/PostOfficeUnsubscribeTest.java create mode 100644 broker/src/test/java/io/moquette/broker/SessionRegistryTest.java delete mode 100644 broker/src/test/java/io/moquette/spi/impl/ConnectHandlerTest.java create mode 100644 perf/src/main/java/io/moquette/performance/clients/mqttbee/BeeBenchmarker.java create mode 100644 perf/src/main/java/io/moquette/performance/clients/paho/PahoBenchmarker.java create mode 100644 perf/src/main/java/io/moquette/performance/clients/paho/SubscriberCallback.java create mode 100644 tools_scripts/integration/allAroundWalk.groovy create mode 100644 tools_scripts/integration/pahoConnect.groovy diff --git a/broker/build.gradle b/broker/build.gradle index af90eb96..cdd0e11e 100644 --- a/broker/build.gradle +++ b/broker/build.gradle @@ -23,9 +23,9 @@ dependencies { compile group: 'com.bugsnag', name: 'bugsnag', version:'[3.0,4.0)' compile group: 'commons-codec', name: 'commons-codec', version:'1.10' - // compile group: 'org.hdrhistogram', name: 'HdrHistogram', version:'2.1.4' compile group: 'org.slf4j', name: 'slf4j-api', version:'1.7.5' + runtime group: 'org.slf4j', name: 'slf4j-log4j12', version:'1.7.5' // only for local tests testCompile group: 'org.slf4j', name: 'slf4j-log4j12', version:'1.7.5' testCompile group: 'org.fusesource.mqtt-client', name: 'mqtt-client', version:'1.12' diff --git a/broker/src/main/java/io/moquette/broker/Authorizator.java b/broker/src/main/java/io/moquette/broker/Authorizator.java new file mode 100644 index 00000000..621edfdc --- /dev/null +++ b/broker/src/main/java/io/moquette/broker/Authorizator.java @@ -0,0 +1,80 @@ +package io.moquette.broker; + +import io.moquette.spi.ClientSession; +import io.moquette.spi.impl.subscriptions.Topic; +import io.moquette.spi.security.IAuthorizatorPolicy; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.netty.handler.codec.mqtt.MqttSubscribeMessage; +import io.netty.handler.codec.mqtt.MqttTopicSubscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +import static io.moquette.spi.impl.Utils.messageId; +import static io.netty.handler.codec.mqtt.MqttQoS.FAILURE; + +final class Authorizator { + + private static final Logger LOG = LoggerFactory.getLogger(Authorizator.class); + + private final IAuthorizatorPolicy policy; + + Authorizator(IAuthorizatorPolicy policy) { + this.policy = policy; + } + + /** + * @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. + */ + List verifyTopicsReadAccess(String clientID, String username, MqttSubscribeMessage msg) { + List ackTopics = new ArrayList<>(); + + final int messageId = messageId(msg); + for (MqttTopicSubscription req : msg.payload().topicSubscriptions()) { + Topic topic = new Topic(req.topicName()); + if (!policy.canRead(topic, username, 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; + } + + /** + * Ask the authorization policy if the topic can be used in a publish. + * + * @param topic + * the topic to write to. + * @param user + * the user + * @param client + * the client + * @return true if the user from client can publish data on topic. + */ + boolean canWrite(Topic topic, String user, String client) { + return policy.canWrite(topic, user, client); + } + +} diff --git a/broker/src/main/java/io/moquette/broker/BrokerConfiguration.java b/broker/src/main/java/io/moquette/broker/BrokerConfiguration.java new file mode 100644 index 00000000..d090fa30 --- /dev/null +++ b/broker/src/main/java/io/moquette/broker/BrokerConfiguration.java @@ -0,0 +1,36 @@ +package io.moquette.broker; + +import io.moquette.BrokerConstants; +import io.moquette.server.config.IConfig; + +class BrokerConfiguration { + + private final boolean allowAnonymous; + private final boolean allowZeroByteClientId; + private final boolean reauthorizeSubscriptionsOnConnect; + + BrokerConfiguration(IConfig props) { + allowAnonymous = props.boolProp(BrokerConstants.ALLOW_ANONYMOUS_PROPERTY_NAME, true); + allowZeroByteClientId = props.boolProp(BrokerConstants.ALLOW_ZERO_BYTE_CLIENT_ID_PROPERTY_NAME, false); + reauthorizeSubscriptionsOnConnect = props.boolProp(BrokerConstants.REAUTHORIZE_SUBSCRIPTIONS_ON_CONNECT, false); + } + + public BrokerConfiguration(boolean allowAnonymous, boolean allowZeroByteClientId, + boolean reauthorizeSubscriptionsOnConnect) { + this.allowAnonymous = allowAnonymous; + this.allowZeroByteClientId = allowZeroByteClientId; + this.reauthorizeSubscriptionsOnConnect = reauthorizeSubscriptionsOnConnect; + } + + public boolean isAllowAnonymous() { + return allowAnonymous; + } + + public boolean isAllowZeroByteClientId() { + return allowZeroByteClientId; + } + + public boolean isReauthorizeSubscriptionsOnConnect() { + return reauthorizeSubscriptionsOnConnect; + } +} diff --git a/broker/src/main/java/io/moquette/broker/IRetainedRepository.java b/broker/src/main/java/io/moquette/broker/IRetainedRepository.java new file mode 100644 index 00000000..499f67a5 --- /dev/null +++ b/broker/src/main/java/io/moquette/broker/IRetainedRepository.java @@ -0,0 +1,14 @@ +package io.moquette.broker; + +import io.moquette.spi.impl.subscriptions.Topic; +import io.netty.handler.codec.mqtt.MqttPublishMessage; + +public interface IRetainedRepository { + void cleanRetained(Topic topic); + + void retain(Topic topic, MqttPublishMessage msg); + + boolean isEmtpy(); + + MqttPublishMessage retainedOnTopic(String topic); +} diff --git a/broker/src/main/java/io/moquette/broker/InflightResender.java b/broker/src/main/java/io/moquette/broker/InflightResender.java new file mode 100644 index 00000000..9c729bea --- /dev/null +++ b/broker/src/main/java/io/moquette/broker/InflightResender.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2012-2016 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.broker; + +import io.moquette.server.netty.NettyUtils; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.concurrent.EventExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * Resend inflight not ack'ed publish packets (QoS1 PUB and QoS2 PUB/PUBREL). It's inspired by IdleStateHandler but it's + * specialized version, just invoking Session's resendInflightNotAcked by the channel after a period. + */ +public class InflightResender extends ChannelDuplexHandler { + + /** + * Placeholder event to resend not-acked publish messages in the in flight window. + * */ + public static class ResendNotAckedPublishes { + } + + private final class WriterIdleTimeoutTask implements Runnable { + + private final ChannelHandlerContext ctx; + + WriterIdleTimeoutTask(ChannelHandlerContext ctx) { + this.ctx = ctx; + } + + @Override + public void run() { + if (!ctx.channel().isOpen()) { + return; + } + long nextDelay = resenderTimeNanos - (System.nanoTime() - lastExecutionTime); + if (nextDelay <= 0) { + // Writer is idle - set a new timeout and notify the callback. + resenderTimeout = ctx.executor().schedule(this, resenderTimeNanos, TimeUnit.NANOSECONDS); + try { + resendNotAcked(ctx/* , event */); + } catch (Throwable t) { + ctx.fireExceptionCaught(t); + } + } else { + // Write occurred before the timeout - set a new timeout with shorter delay. + resenderTimeout = ctx.executor().schedule(this, nextDelay, TimeUnit.NANOSECONDS); + } + } + } + + private static final Logger LOG = LoggerFactory.getLogger(InflightResender.class); + private static final long MIN_TIMEOUT_NANOS = TimeUnit.MILLISECONDS.toNanos(1); + + private final long resenderTimeNanos; + volatile ScheduledFuture resenderTimeout; + volatile long lastExecutionTime; + + private volatile int state; // 0 - none, 1 - initialized, 2 - destroyed + + public InflightResender(long writerIdleTime, TimeUnit unit) { + if (unit == null) { + throw new NullPointerException("unit"); + } + resenderTimeNanos = Math.max(unit.toNanos(writerIdleTime), MIN_TIMEOUT_NANOS); + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + 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. + initialize(ctx); + } else { + // channelActive() event has not been fired yet. this.channelActive() will be invoked + // and initialization will occur there. + } + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + destroy(); + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + // Initialize early if channel is active already. + if (ctx.channel().isActive()) { + initialize(ctx); + } + super.channelRegistered(ctx); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + // This method will be invoked only if this handler was added + // before channelActive() event is fired. If a user adds this handler + // after the channelActive() event, initialize() will be called by beforeAdd(). + initialize(ctx); + super.channelActive(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + destroy(); + super.channelInactive(ctx); + } + + private void initialize(ChannelHandlerContext ctx) { + // 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())); + } + switch (state) { + case 1: + case 2: + return; + } + + state = 1; + + EventExecutor loop = ctx.executor(); + + lastExecutionTime = System.nanoTime(); + resenderTimeout = loop.schedule(new WriterIdleTimeoutTask(ctx), resenderTimeNanos, TimeUnit.NANOSECONDS); + } + + private void destroy() { + state = 2; + + if (resenderTimeout != null) { + resenderTimeout.cancel(false); + resenderTimeout = null; + } + } + + private void resendNotAcked(ChannelHandlerContext ctx/* , IdleStateEvent evt */) throws Exception { + if (LOG.isDebugEnabled()) { + LOG.debug("Flushing idle Netty channel. MqttClientId = {}.", NettyUtils.clientID(ctx.channel())); + } + ctx.fireUserEventTriggered(new ResendNotAckedPublishes()); + } +} diff --git a/broker/src/main/java/io/moquette/broker/MQTTConnection.java b/broker/src/main/java/io/moquette/broker/MQTTConnection.java new file mode 100644 index 00000000..b7e38708 --- /dev/null +++ b/broker/src/main/java/io/moquette/broker/MQTTConnection.java @@ -0,0 +1,435 @@ +package io.moquette.broker; + +import io.moquette.server.netty.AutoFlushHandler; +import io.moquette.server.netty.NettyUtils; +import io.moquette.spi.impl.DebugUtils; +import io.moquette.spi.impl.subscriptions.Topic; +import io.moquette.spi.security.IAuthenticator; +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.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +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.*; +import static io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader.from; +import static io.netty.handler.codec.mqtt.MqttQoS.*; + +final class MQTTConnection { + + private static final Logger LOG = LoggerFactory.getLogger(MQTTConnection.class); + + final Channel channel; + private BrokerConfiguration brokerConfig; + private IAuthenticator authenticator; + private SessionRegistry sessionRegistry; + private final PostOffice postOffice; + private boolean connected; + private final AtomicInteger lastPacketId = new AtomicInteger(0); + + MQTTConnection(Channel channel, BrokerConfiguration brokerConfig, IAuthenticator authenticator, + SessionRegistry sessionRegistry, PostOffice postOffice) { + this.channel = channel; + this.brokerConfig = brokerConfig; + this.authenticator = authenticator; + this.sessionRegistry = sessionRegistry; + this.postOffice = postOffice; + this.connected = false; + } + + void handleMessage(MqttMessage msg) { + MqttMessageType messageType = msg.fixedHeader().messageType(); + LOG.debug("Received MQTT message, type: {}, channel: {}", messageType, channel); + switch (messageType) { + case CONNECT: + processConnect((MqttConnectMessage) msg); + break; + case SUBSCRIBE: + processSubscribe((MqttSubscribeMessage) msg); + break; + case UNSUBSCRIBE: + processUnsubscribe((MqttUnsubscribeMessage) msg); + break; + case PUBLISH: + processPublish((MqttPublishMessage) msg); + break; + case PUBREC: + processPubRec(msg); + break; + case PUBCOMP: + processPubComp(msg); + break; + case PUBREL: + processPubRel(msg); + break; + case DISCONNECT: + processDisconnect(msg); + break; + case PUBACK: + processPubAck(msg); + break; + case PINGREQ: + MqttFixedHeader pingHeader = new MqttFixedHeader(MqttMessageType.PINGRESP,false, AT_MOST_ONCE, + false,0); + MqttMessage pingResp = new MqttMessage(pingHeader); + channel.writeAndFlush(pingResp).addListener(CLOSE_ON_FAILURE); + break; + default: + LOG.error("Unknown MessageType: {}, channel: {}", messageType, channel); + break; + } + } + + private void processPubComp(MqttMessage msg) { + final int messageID = ((MqttMessageIdVariableHeader) msg.variableHeader()).messageId(); + final Session session = sessionRegistry.retrieve(getClientId()); + session.processPubComp(messageID); + } + + private void processPubRec(MqttMessage msg) { + final int messageID = ((MqttMessageIdVariableHeader) msg.variableHeader()).messageId(); + final Session session = sessionRegistry.retrieve(getClientId()); + session.processPubRec(messageID); + } + + static MqttMessage pubrel(int messageID) { + MqttFixedHeader pubRelHeader = new MqttFixedHeader(MqttMessageType.PUBREL, false, AT_LEAST_ONCE, false, 0); + return new MqttMessage(pubRelHeader, from(messageID)); + } + + private void processPubAck(MqttMessage msg) { + final int messageID = ((MqttMessageIdVariableHeader) msg.variableHeader()).messageId(); + Session session = sessionRegistry.retrieve(getClientId()); + session.pubAckReceived(messageID); + } + + void processConnect(MqttConnectMessage msg) { + MqttConnectPayload payload = msg.payload(); + String clientId = payload.clientIdentifier(); + final String username = payload.userName(); + LOG.trace("Processing CONNECT message. CId={} username: {} channel: {}", clientId, username, channel); + + if (isNotProtocolVersion(msg, MqttVersion.MQTT_3_1) && isNotProtocolVersion(msg, MqttVersion.MQTT_3_1_1)) { + LOG.warn("MQTT protocol version is not valid. CId={} channel: {}", clientId, channel); + abortConnection(CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION); + return; + } + final boolean cleanSession = msg.variableHeader().isCleanSession(); + if (clientId == null || clientId.length() == 0) { + if (!brokerConfig.isAllowZeroByteClientId()) { + LOG.warn("Broker doesn't permit MQTT client ID empty. Username={}, channel: {}", username, channel); + abortConnection(CONNECTION_REFUSED_IDENTIFIER_REJECTED); + return; + } + + if (!cleanSession) { + LOG.warn("MQTT client ID cannot be empty for persistent session. Username={}, channel: {}", username, channel); + abortConnection(CONNECTION_REFUSED_IDENTIFIER_REJECTED); + return; + } + + // Generating client id. + clientId = UUID.randomUUID().toString().replace("-", ""); + LOG.debug("Client has connected with server generated id={}, username={}, channel: {}", clientId, username, channel); + } + + if (!login(msg, clientId)) { + abortConnection(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); + channel.close().addListener(CLOSE_ON_FAILURE); + return; + } + + try { + LOG.trace("Binding MQTTConnection (channel: {}) to session", channel); + sessionRegistry.bindToSession(this, msg, clientId); + + setupInflightResender(channel); + + NettyUtils.clientID(channel, clientId); + LOG.trace("CONNACK sent, channel: {}", channel); + } catch (SessionCorruptedException scex) { + LOG.warn("MQTT session for client ID {} cannot be created, channel: {}", clientId, channel); + abortConnection(CONNECTION_REFUSED_SERVER_UNAVAILABLE); + } + } + + private void setupInflightResender(Channel channel) { + channel.pipeline() + .addFirst("inflightResender", new InflightResender(5_000, TimeUnit.MILLISECONDS)); + } + + private boolean isNotProtocolVersion(MqttConnectMessage msg, MqttVersion version) { + return msg.variableHeader().version() != version.protocolLevel(); + } + + private void abortConnection(MqttConnectReturnCode returnCode) { + MqttConnAckMessage badProto = connAck(returnCode, false); + channel.writeAndFlush(badProto).addListener(FIRE_EXCEPTION_ON_FAILURE); + channel.close().addListener(CLOSE_ON_FAILURE); + } + + 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(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 (!brokerConfig.isAllowAnonymous()) { + LOG.error("Client didn't supply any password and MQTT anonymous mode is disabled CId={}", clientId); + return false; + } + final String login = msg.payload().userName(); + if (!authenticator.checkValid(clientId, login, pwd)) { + LOG.error("Authenticator has rejected the MQTT credentials CId={}, username={}", clientId, login); + return false; + } + NettyUtils.userName(channel, login); + } else if (!brokerConfig.isAllowAnonymous()) { + LOG.error("Client didn't supply any credentials and MQTT anonymous mode is disabled. CId={}", clientId); + return false; + } + return true; + } + + void handleConnectionLost() { + String clientID = NettyUtils.clientID(channel); + if (clientID != null && !clientID.isEmpty()) { + LOG.info("Notifying connection lost event. CId = {}, channel: {}", clientID, channel); + Session session = sessionRegistry.retrieve(clientID); + if (session.hasWill()) { + postOffice.fireWill(session.getWill()); + } + if (session.isClean()) { + sessionRegistry.remove(clientID); + } else { + sessionRegistry.disconnect(clientID); + } + connected = false; + } + channel.close().addListener(CLOSE_ON_FAILURE); + } + + void sendConnAck(boolean isSessionAlreadyPresent) { + connected = true; + final MqttConnAckMessage ackMessage = connAck(CONNECTION_ACCEPTED, isSessionAlreadyPresent); + channel.writeAndFlush(ackMessage); + } + + void dropConnection() { + channel.close(); + } + + void processDisconnect(MqttMessage msg) { + final String clientID = NettyUtils.clientID(channel); + LOG.trace("Start DISCONNECT CId={}, channel: {}", clientID, channel); + if (!connected) { + LOG.info("DISCONNECT received on already closed connection, CId={}, channel: {}", clientID, channel); + return; + } + sessionRegistry.disconnect(clientID); + connected = false; + channel.close(); + LOG.trace("Processed DISCONNECT CId={}, channel: {}", clientID, channel); + } + + void processSubscribe(MqttSubscribeMessage msg) { + final String clientID = NettyUtils.clientID(channel); + if (!connected) { + LOG.warn("SUBSCRIBE received on already closed connection, CId={}, channel: {}", clientID, channel); + dropConnection(); + return; + } + postOffice.subscribeClientToTopics(msg, clientID, NettyUtils.userName(channel), this); + } + + void sendSubAckMessage(int messageID, MqttSubAckMessage ackMessage) { + final String clientId = NettyUtils.clientID(channel); + LOG.trace("Sending SUBACK response CId={}, messageId: {}", clientId, messageID); + channel.writeAndFlush(ackMessage).addListener(FIRE_EXCEPTION_ON_FAILURE); + } + + private void processUnsubscribe(MqttUnsubscribeMessage msg) { + List topics = msg.payload().topics(); + String clientID = NettyUtils.clientID(channel); + + LOG.trace("Processing UNSUBSCRIBE message. CId={}, topics: {}", clientID, topics); + postOffice.unsubscribe(topics, this, msg.variableHeader().messageId()); + } + + void sendUnsubAckMessage(List topics, String clientID, int messageID) { + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.UNSUBACK, false, AT_MOST_ONCE, + false, 0); + MqttUnsubAckMessage ackMessage = new MqttUnsubAckMessage(fixedHeader, from(messageID)); + + LOG.trace("Sending UNSUBACK message. CId={}, messageId: {}, topics: {}", clientID, messageID, topics); + channel.writeAndFlush(ackMessage).addListener(FIRE_EXCEPTION_ON_FAILURE); + LOG.trace("Client <{}> unsubscribed from topics <{}>", clientID, topics); + } + + void processPublish(MqttPublishMessage msg) { + final MqttQoS qos = msg.fixedHeader().qosLevel(); + final String username = NettyUtils.userName(channel); + final String topicName = msg.variableHeader().topicName(); + final String clientId = getClientId(); + LOG.trace("Processing PUBLISH message. CId={}, topic: {}, messageId: {}, qos: {}", clientId, topicName, + msg.variableHeader().packetId(), qos); + ByteBuf payload = msg.payload(); + final boolean retain = msg.fixedHeader().isRetain(); + final Topic topic = new Topic(topicName); + if (!topic.isValid()) { + LOG.debug("Drop connection because of invalid topic format"); + dropConnection(); + } + switch (qos) { + case AT_MOST_ONCE: + postOffice.receivedPublishQos0(topic, username, clientId, payload, retain); + break; + case AT_LEAST_ONCE: { + final int messageID = msg.variableHeader().packetId(); + postOffice.receivedPublishQos1(this, topic, username, payload, messageID, retain, msg); + break; + } + case EXACTLY_ONCE: { + final int messageID = msg.variableHeader().packetId(); + final Session session = sessionRegistry.retrieve(clientId); + session.receivedPublishQos2(messageID, msg); + postOffice.receivedPublishQos2(this, msg); +// msg.release(); + break; + } + default: + LOG.error("Unknown QoS-Type:{}", qos); + break; + } + } + + void sendPublishReceived(int messageID) { + LOG.trace("sendPubRec invoked on channel: {}", channel); + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBREC, false, AT_MOST_ONCE, + false, 0); + MqttPubAckMessage pubRecMessage = new MqttPubAckMessage(fixedHeader, from(messageID)); + sendIfWritableElseDrop(pubRecMessage); + } + + private void processPubRel(MqttMessage msg) { + final Session session = sessionRegistry.retrieve(getClientId()); + final int messageID = ((MqttMessageIdVariableHeader) msg.variableHeader()).messageId(); + session.receivedPubRelQos2(messageID); + sendPubCompMessage(messageID); + } + + void sendPublish(MqttPublishMessage publishMsg) { + final int packetId = publishMsg.variableHeader().packetId(); + final String topicName = publishMsg.variableHeader().topicName(); + final String clientId = getClientId(); + MqttQoS qos = publishMsg.fixedHeader().qosLevel(); + if (LOG.isTraceEnabled()) { + LOG.trace("Sending PUBLISH({}) message. MessageId={}, CId={}, topic={}, payload={}", qos, packetId, + clientId, topicName, DebugUtils.payload2Str(publishMsg.payload())); + } else { + LOG.debug("Sending PUBLISH({}) message. MessageId={}, CId={}, topic={}", qos, packetId, clientId, + topicName); + } + sendIfWritableElseDrop(publishMsg); + } + + void sendIfWritableElseDrop(MqttMessage msg) { + if (channel.isWritable()) { + channel.write(msg); + } + } + + public void writabilityChanged() { + if (channel.isWritable()) { + LOG.debug("Channel {} is again writable", channel); + final Session session = sessionRegistry.retrieve(getClientId()); + session.writabilityChanged(); + } + } + + void sendPubAck(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)); + sendIfWritableElseDrop(pubAckMessage); + } + + private void sendPubCompMessage(int messageID) { + LOG.trace("Sending PUBCOMP message on channel: {}, messageId: {}", channel, messageID); + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, AT_MOST_ONCE, false, 0); + MqttMessage pubCompMessage = new MqttMessage(fixedHeader, from(messageID)); + sendIfWritableElseDrop(pubCompMessage); + } + + String getClientId() { + return NettyUtils.clientID(channel); + } + + public void sendPublishRetainedQos0(Topic topic, MqttQoS qos, ByteBuf payload) { + MqttPublishMessage publishMsg = retainedPublish(topic.toString(), qos, payload); + sendPublish(publishMsg); + } + + public void sendPublishRetainedWithPacketId(Topic topic, MqttQoS qos, ByteBuf payload) { + final int packetId = nextPacketId(); + MqttPublishMessage publishMsg = retainedPublishWithMessageId(topic.toString(), qos, payload, packetId); + sendPublish(publishMsg); + } + + private static MqttPublishMessage retainedPublish(String topic, MqttQoS qos, ByteBuf message) { + return retainedPublishWithMessageId(topic, qos, message, 0); + } + + private static MqttPublishMessage retainedPublishWithMessageId(String topic, MqttQoS qos, ByteBuf message, + int messageId) { + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, false, qos, true, 0); + MqttPublishVariableHeader varHeader = new MqttPublishVariableHeader(topic, messageId); + return new MqttPublishMessage(fixedHeader, varHeader, message); + } + + // TODO move this method in Session + void sendPublishNotRetainedQos0(Topic topic, MqttQoS qos, ByteBuf payload) { + MqttPublishMessage publishMsg = notRetainedPublish(topic.toString(), qos, payload); + sendPublish(publishMsg); + } + + static MqttPublishMessage notRetainedPublish(String topic, MqttQoS qos, ByteBuf message) { + return notRetainedPublishWithMessageId(topic, qos, message, 0); + } + + 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); + } + + public void resendNotAckedPublishes() { + final Session session = sessionRegistry.retrieve(getClientId()); + session.resendInflightNotAcked(); + } + + int nextPacketId() { + return lastPacketId.incrementAndGet(); + } + + @Override + public String toString() { + return "MQTTConnection{channel=" + channel + ", connected=" + connected + '}'; + } +} diff --git a/broker/src/main/java/io/moquette/broker/MQTTConnectionFactory.java b/broker/src/main/java/io/moquette/broker/MQTTConnectionFactory.java new file mode 100644 index 00000000..53576584 --- /dev/null +++ b/broker/src/main/java/io/moquette/broker/MQTTConnectionFactory.java @@ -0,0 +1,24 @@ +package io.moquette.broker; + +import io.moquette.spi.security.IAuthenticator; +import io.netty.channel.Channel; + +class MQTTConnectionFactory { + + private final BrokerConfiguration brokerConfig; + private final IAuthenticator authenticator; + private final SessionRegistry sessionRegistry; + private final PostOffice postOffice; + + MQTTConnectionFactory(BrokerConfiguration brokerConfig, IAuthenticator authenticator, + SessionRegistry sessionRegistry, PostOffice postOffice) { + this.brokerConfig = brokerConfig; + this.authenticator = authenticator; + this.sessionRegistry = sessionRegistry; + this.postOffice = postOffice; + } + + public MQTTConnection create(Channel channel) { + return new MQTTConnection(channel, brokerConfig, authenticator, sessionRegistry, postOffice); + } +} diff --git a/broker/src/main/java/io/moquette/broker/MemoryRetainedRepository.java b/broker/src/main/java/io/moquette/broker/MemoryRetainedRepository.java new file mode 100644 index 00000000..bec21ebc --- /dev/null +++ b/broker/src/main/java/io/moquette/broker/MemoryRetainedRepository.java @@ -0,0 +1,35 @@ +package io.moquette.broker; + +import io.moquette.spi.impl.subscriptions.Topic; +import io.netty.handler.codec.mqtt.MqttPublishMessage; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/* +* In memory retained messages store +* */ +final class MemoryRetainedRepository implements IRetainedRepository { + + private final ConcurrentMap storage = new ConcurrentHashMap<>(); + + @Override + public void cleanRetained(Topic topic) { + storage.remove(topic.toString()); + } + + @Override + public void retain(Topic topic, MqttPublishMessage msg) { + storage.put(topic.toString(), msg); + } + + @Override + public boolean isEmtpy() { + return storage.isEmpty(); + } + + @Override + public MqttPublishMessage retainedOnTopic(String topic) { + return storage.get(topic); + } +} diff --git a/broker/src/main/java/io/moquette/broker/NewNettyAcceptor.java b/broker/src/main/java/io/moquette/broker/NewNettyAcceptor.java new file mode 100644 index 00000000..9c996559 --- /dev/null +++ b/broker/src/main/java/io/moquette/broker/NewNettyAcceptor.java @@ -0,0 +1,383 @@ +/* + * 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.broker; + +import io.moquette.BrokerConstants; +import io.moquette.server.config.IConfig; +import io.moquette.server.netty.BugSnagErrorsHandler; +import io.moquette.server.netty.MoquetteIdleTimeoutHandler; +import io.moquette.server.netty.metrics.*; +import io.moquette.spi.security.ISslContextCreator; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.*; +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.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.mqtt.MqttDecoder; +import io.netty.handler.codec.mqtt.MqttEncoder; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.concurrent.Future; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import static io.moquette.BrokerConstants.*; +import static io.netty.channel.ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE; + +public class NewNettyAcceptor { + + private static final String MQTT_SUBPROTOCOL_CSV_LIST = "mqtt, mqttv3.1, mqttv3.1.1"; + + static class WebSocketFrameToByteBufDecoder extends MessageToMessageDecoder { + + @Override + protected void decode(ChannelHandlerContext chc, BinaryWebSocketFrame frame, List 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 { + + @Override + protected void encode(ChannelHandlerContext chc, ByteBuf bb, List 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(NewNettyAcceptor.class); + + EventLoopGroup m_bossGroup; + EventLoopGroup m_workerGroup; + BytesMetricsCollector m_bytesMetricsCollector = new BytesMetricsCollector(); + MessageMetricsCollector m_metricsCollector = new MessageMetricsCollector(); + private Optional metrics; + private Optional errorsCather; + + private int nettySoBacklog; + private boolean nettySoReuseaddr; + private boolean nettyTcpNodelay; + private boolean nettySoKeepalive; + private int nettyChannelTimeoutSeconds; + private int maxBytesInMessage; + + private Class channelClass; + + public void initialize(NewNettyMQTTHandler mqttHandler, 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 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() { + + @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(NewNettyMQTTHandler 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") + 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); +// } +} diff --git a/broker/src/main/java/io/moquette/broker/NewNettyMQTTHandler.java b/broker/src/main/java/io/moquette/broker/NewNettyMQTTHandler.java new file mode 100644 index 00000000..e5bf7257 --- /dev/null +++ b/broker/src/main/java/io/moquette/broker/NewNettyMQTTHandler.java @@ -0,0 +1,115 @@ +/* + * 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.broker; + +import io.moquette.server.netty.NettyUtils; +import io.netty.channel.*; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.util.AttributeKey; +import io.netty.util.ReferenceCountUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import static io.netty.channel.ChannelFutureListener.CLOSE_ON_FAILURE; + +@Sharable +public class NewNettyMQTTHandler extends ChannelInboundHandlerAdapter { + + private static final Logger LOG = LoggerFactory.getLogger(NewNettyMQTTHandler.class); + + private static final String ATTR_CONNECTION = "connection"; + private static final AttributeKey ATTR_KEY_CONNECTION = AttributeKey.valueOf(ATTR_CONNECTION); + + private MQTTConnectionFactory connectionFactory; + + NewNettyMQTTHandler(MQTTConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + private static void mqttConnection(Channel channel, MQTTConnection connection) { + channel.attr(ATTR_KEY_CONNECTION).set(connection); + } + + private static MQTTConnection mqttConnection(Channel channel) { + return (MQTTConnection) channel.attr(ATTR_KEY_CONNECTION).get(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object message) throws Exception { + MqttMessage msg = (MqttMessage) message; + if (msg.fixedHeader() == null) { + throw new IOException("Unknown packet"); + } + final MQTTConnection mqttConnection = mqttConnection(ctx.channel()); + try { + mqttConnection.handleMessage(msg); + } catch (Throwable ex) { + //ctx.fireExceptionCaught(ex); + LOG.error("Error processing protocol message: {}", msg.fixedHeader().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 channelActive(ChannelHandlerContext ctx) { + MQTTConnection connection = connectionFactory.create(ctx.channel()); + mqttConnection(ctx.channel(), connection); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + final MQTTConnection mqttConnection = mqttConnection(ctx.channel()); + mqttConnection.handleConnectionLost(); + } + + @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()); +// } + final MQTTConnection mqttConnection = mqttConnection(ctx.channel()); + mqttConnection.writabilityChanged(); + ctx.fireChannelWritabilityChanged(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof InflightResender.ResendNotAckedPublishes) { + final MQTTConnection mqttConnection = mqttConnection(ctx.channel()); + mqttConnection.resendNotAckedPublishes(); + } + ctx.fireUserEventTriggered(evt); + } + +} diff --git a/broker/src/main/java/io/moquette/broker/PostOffice.java b/broker/src/main/java/io/moquette/broker/PostOffice.java new file mode 100644 index 00000000..5d511e33 --- /dev/null +++ b/broker/src/main/java/io/moquette/broker/PostOffice.java @@ -0,0 +1,274 @@ +package io.moquette.broker; + +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.IAuthorizatorPolicy; +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.mqtt.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +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.*; + +class PostOffice { + + private static final Logger LOG = LoggerFactory.getLogger(PostOffice.class); + + private final Authorizator authorizator; + private final ISubscriptionsDirectory subscriptions; + private final IRetainedRepository retainedRepository; + private SessionRegistry sessionRegistry; + + PostOffice(ISubscriptionsDirectory subscriptions, IAuthorizatorPolicy authorizatorPolicy, + IRetainedRepository retainedRepository, SessionRegistry sessionRegistry) { + this.authorizator = new Authorizator(authorizatorPolicy); + this.subscriptions = subscriptions; + this.retainedRepository = retainedRepository; + this.sessionRegistry = sessionRegistry; + } + + public void init(SessionRegistry sessionRegistry) { + this.sessionRegistry = sessionRegistry; + } + + public void fireWill(Session.Will will) { + // MQTT 3.1.2.8-17 + publish2Subscribers(will.payload, new Topic(will.topic), will.qos); + } + + public void subscribeClientToTopics(MqttSubscribeMessage msg, String clientID, String username, + MQTTConnection mqttConnection) { + // verify which topics of the subscribe ongoing has read access permission + int messageID = messageId(msg); + List ackTopics = authorizator.verifyTopicsReadAccess(clientID, username, msg); + MqttSubAckMessage ackMessage = doAckMessageFromValidateFilters(ackTopics, messageID); + + // store topics subscriptions in session + List newSubscriptions = ackTopics.stream() + .filter(req -> req.qualityOfService() != FAILURE) + .map(req -> { + final Topic topic = new Topic(req.topicName()); + return new Subscription(clientID, topic, req.qualityOfService()); + }).collect(Collectors.toList()); + + for (Subscription subscription : newSubscriptions) { + subscriptions.add(subscription); + } + + // add the subscriptions to Session + Session session = sessionRegistry.retrieve(clientID); + session.addSubscriptions(newSubscriptions); + + // send ack message + mqttConnection.sendSubAckMessage(messageID, ackMessage); + + publishRetainedMessagesForSubscriptions(clientID, newSubscriptions); + + // TODO notify the Observables +// for (Subscription subscription : newSubscriptions) { +// m_interceptor.notifyTopicSubscribed(newSubscription, username); +// } + } + + private void publishRetainedMessagesForSubscriptions(String clientID, List newSubscriptions) { + Session targetSession = this.sessionRegistry.retrieve(clientID); + for (Subscription subscription : newSubscriptions) { + final String topicFilter = subscription.getTopicFilter().toString(); + final MqttPublishMessage retainedMsg = retainedRepository.retainedOnTopic(topicFilter); + + if (retainedMsg == null) { + // not found + continue; + } + + final MqttQoS retainedQos = retainedMsg.fixedHeader().qosLevel(); + MqttQoS qos = lowerQosToTheSubscriptionDesired(subscription, retainedQos); + + final ByteBuf origPayload = retainedMsg.payload(); + ByteBuf payload = origPayload.retainedDuplicate(); + targetSession.sendRetainedPublishOnSessionAtQos(subscription.getTopicFilter(), qos, payload); + } + } + + /** + * Create the SUBACK response from a list of topicFilters + */ + private MqttSubAckMessage doAckMessageFromValidateFilters(List topicFilters, int messageId) { + List 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); + } + + public void unsubscribe(List topics, MQTTConnection mqttConnection, int messageId) { + final String clientID = mqttConnection.getClientId(); + 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 + mqttConnection.dropConnection(); + LOG.warn("Topic filter is not valid. CId={}, topics: {}, offending topic filter: {}", clientID, + topics, topic); + return; + } + + LOG.trace("Removing subscription. CId={}, topic={}", clientID, topic); + subscriptions.removeSubscription(topic, clientID); + + // TODO remove the subscriptions to Session +// clientSession.unsubscribeFrom(topic); + + //TODO notify interceptors +// String username = NettyUtils.userName(channel); +// m_interceptor.notifyTopicUnsubscribed(topic.toString(), clientID, username); + } + + // ack the client + mqttConnection.sendUnsubAckMessage(topics, clientID, messageId); + } + + void receivedPublishQos0(Topic topic, String username, String clientID, ByteBuf payload, boolean retain) { + 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, AT_MOST_ONCE); + + if (retain) { + // QoS == 0 && retain => clean old retained + retainedRepository.cleanRetained(topic); + } +// TODO +// m_interceptor.notifyTopicPublished(msg, clientID, username); + } + + void receivedPublishQos1(MQTTConnection connection, Topic topic, String username, ByteBuf payload, int messageID, + boolean retain, MqttPublishMessage msg) { + // verify if topic can be write + topic.getTokens(); + if (!topic.isValid()) { + LOG.warn("Invalid topic format, force close the connection"); + connection.dropConnection(); + return; + } + 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, AT_LEAST_ONCE); + + connection.sendPubAck(messageID); + + if (retain) { + if (!payload.isReadable()) { + retainedRepository.cleanRetained(topic); + } else { + // before wasn't stored + retainedRepository.retain(topic, msg); + } + } +//TODO +// m_interceptor.notifyTopicPublished(msg, clientID, username); + } + + private void publish2Subscribers(ByteBuf origPayload, Topic topic, MqttQoS publishingQos) { + Set topicMatchingSubscriptions = subscriptions.matchQosSharpening(topic); + + for (final Subscription sub : topicMatchingSubscriptions) { + MqttQoS qos = lowerQosToTheSubscriptionDesired(sub, publishingQos); + Session targetSession = this.sessionRegistry.retrieve(sub.getClientId()); + + boolean isSessionPresent = targetSession != null; + if (isSessionPresent) { + 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(); + targetSession.sendPublishOnSessionAtQos(topic, qos, payload); + } else { + // If we are, the subscriber disconnected after the subscriptions tree selected that session as a + // destination. + LOG.debug("PUBLISH to not yet present session. CId: {}, topicFilter: {}, qos: {}", sub.getClientId(), + sub.getTopicFilter(), qos); + } + } + } + + /** + * First phase of a publish QoS2 protocol, sent by publisher to the broker. Publish to all interested + * subscribers. + */ + void receivedPublishQos2(MQTTConnection connection, MqttPublishMessage mqttPublishMessage) { + LOG.trace("Processing PUBREL message on connection: {}", connection); + final Topic topic = new Topic(mqttPublishMessage.variableHeader().topicName()); + final ByteBuf payload = mqttPublishMessage.payload(); + publish2Subscribers(payload, topic, EXACTLY_ONCE); + + final boolean retained = mqttPublishMessage.fixedHeader().isRetain(); + if (retained) { + if (!payload.isReadable()) { + retainedRepository.cleanRetained(topic); + } else { + // before wasn't stored + retainedRepository.retain(topic, mqttPublishMessage); + } + } + + //TODO here we should notify to the listeners + //m_interceptor.notifyTopicPublished(msg, clientID, username); + } + + static MqttQoS lowerQosToTheSubscriptionDesired(Subscription sub, MqttQoS qos) { + if (qos.value() > sub.getRequestedQos().value()) { + qos = sub.getRequestedQos(); + } + return qos; + } + + /** + * Intended usage is only for embedded versions of the broker, where the hosting application + * want to use the broker to send a publish message. Like normal external publish message 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()); + final ByteBuf payload = msg.payload(); + LOG.info("Sending internal PUBLISH message Topic={}, qos={}", topic, qos); + + publish2Subscribers(payload, topic, qos); + + if (!msg.fixedHeader().isRetain()) { + return; + } + if (qos == AT_MOST_ONCE || msg.payload().readableBytes() == 0) { + // QoS == 0 && retain => clean old retained + retainedRepository.cleanRetained(topic); + return; + } + retainedRepository.retain(topic, msg); + } +} diff --git a/broker/src/main/java/io/moquette/broker/Server.java b/broker/src/main/java/io/moquette/broker/Server.java new file mode 100644 index 00000000..e36bcc85 --- /dev/null +++ b/broker/src/main/java/io/moquette/broker/Server.java @@ -0,0 +1,142 @@ +package io.moquette.broker; + +import io.moquette.BrokerConstants; +import io.moquette.interception.InterceptHandler; +import io.moquette.persistence.MemoryStorageService; +import io.moquette.server.config.FileResourceLoader; +import io.moquette.server.config.IConfig; +import io.moquette.server.config.IResourceLoader; +import io.moquette.server.config.ResourceLoaderConfig; +import io.moquette.spi.ISessionsStore; +import io.moquette.spi.impl.SessionsRepository; +import io.moquette.spi.impl.security.AcceptAllAuthenticator; +import io.moquette.spi.impl.security.PermitAllAuthorizatorPolicy; +import io.moquette.spi.impl.subscriptions.CTrieSubscriptionDirectory; +import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory; +import io.moquette.spi.security.IAuthenticator; +import io.moquette.spi.security.IAuthorizatorPolicy; +import io.moquette.spi.security.ISslContextCreator; +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.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import static io.moquette.logging.LoggingUtils.getInterceptorIds; + +public class Server { + + private static final Logger LOG = LoggerFactory.getLogger(io.moquette.server.Server.class); + + private ScheduledExecutorService scheduler; + private NewNettyAcceptor acceptor; + private volatile boolean initialized; + + 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); + } + + public void startServer(IConfig config) throws IOException { + LOG.debug("Starting Moquette server using IConfig instance"); + startServer(config, null); + } + + public void startServer(IConfig config, List 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 handlers, ISslContextCreator sslCtxCreator, + IAuthenticator authenticator, IAuthorizatorPolicy authorizatorPolicy) 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); +// } + // DBG + if (authenticator == null) { + authenticator = new AcceptAllAuthenticator(); + } + if (authorizatorPolicy == null) { + authorizatorPolicy = new PermitAllAuthorizatorPolicy(); + } + + MemoryStorageService memStorage = new MemoryStorageService(null, null); + ISessionsStore sessionStore = memStorage.sessionsStore(); + SessionsRepository sessionsRepository = new SessionsRepository(sessionStore, null); + // DBG + + ISubscriptionsDirectory subscriptions = new CTrieSubscriptionDirectory(); + subscriptions.init(sessionsRepository); + SessionRegistry sessions = new SessionRegistry(subscriptions); + final PostOffice dispatcher = new PostOffice(subscriptions, authorizatorPolicy, new MemoryRetainedRepository(), sessions); + final BrokerConfiguration brokerConfig = new BrokerConfiguration(config); + MQTTConnectionFactory connectionFactory = new MQTTConnectionFactory(brokerConfig, authenticator, sessions, + dispatcher); + + final NewNettyMQTTHandler mqttHandler = new NewNettyMQTTHandler(connectionFactory); + acceptor = new NewNettyAcceptor(); + acceptor.initialize(mqttHandler, config, sslCtxCreator); + + final long startTime = System.currentTimeMillis() - start; + LOG.info("Moquette server has been started successfully in {} ms", startTime); + initialized = true; + } + + public void stopServer() { + 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, + // and SessionsRepository does not stop its tasks. Thus shutdownNow(). + scheduler.shutdownNow(); + + LOG.info("Moquette server has been stopped."); + } + + private static File defaultConfigFile() { + String configPath = System.getProperty("moquette.path", null); + return new File(configPath, IConfig.DEFAULT_CONFIG); + } +} diff --git a/broker/src/main/java/io/moquette/broker/Session.java b/broker/src/main/java/io/moquette/broker/Session.java new file mode 100644 index 00000000..6dc37c46 --- /dev/null +++ b/broker/src/main/java/io/moquette/broker/Session.java @@ -0,0 +1,341 @@ +package io.moquette.broker; + +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.*; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +class Session { + + private static final Logger LOG = LoggerFactory.getLogger(Session.class); + private static final int FLIGHT_BEFORE_RESEND_MS = 5_000; + private static final int INFLIGHT_WINDOW_SIZE = 10; + + class InFlightPacket implements Delayed { + + final int packetId; + private long startTime; + + InFlightPacket(int packetId, long delayInMilliseconds) { + this.packetId = packetId; + this.startTime = System.currentTimeMillis() + delayInMilliseconds; + } + + @Override + public long getDelay(TimeUnit unit) { + long diff = startTime - System.currentTimeMillis(); + return unit.convert(diff, TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(Delayed o) { + if ((this.startTime - ((InFlightPacket) o).startTime) == 0) { + return 0; + } + if ((this.startTime - ((InFlightPacket) o).startTime) > 0) { + return 1; + } else { + return -1; + } + } + } + + enum SessionStatus { + CONNECTED, CONNECTING, DISCONNECTING, DISCONNECTED + } + + static final class Will { + + final String topic; + final ByteBuf payload; + final MqttQoS qos; + final boolean retained; + + Will(String topic, ByteBuf payload, MqttQoS qos, boolean retained) { + this.topic = topic; + this.payload = payload; + this.qos = qos; + this.retained = retained; + } + } + + private final String clientId; + private boolean clean; + private Will will; + private Queue sessionQueue; + private final AtomicReference status = new AtomicReference<>(SessionStatus.DISCONNECTED); + private MQTTConnection mqttConnection; + private List subscriptions = new ArrayList<>(); + private final Map inflightWindow = new HashMap<>(); + private final DelayQueue inflightTimeouts = new DelayQueue<>(); + private final Map qos2Receiving = new HashMap<>(); + private final AtomicInteger inflightSlots = new AtomicInteger(INFLIGHT_WINDOW_SIZE); // this should be configurable + + Session(String clientId, boolean clean, Will will, Queue sessionQueue) { + this.clientId = clientId; + this.clean = clean; + this.will = will; + this.sessionQueue = sessionQueue; + } + + Session(boolean clean, String clientId, Queue sessionQueue) { + this.clientId = clientId; + this.clean = clean; + this.sessionQueue = sessionQueue; + } + + void update(boolean clean, Will will) { + this.clean = clean; + this.will = will; + } + + void markConnected() { + assignState(SessionStatus.DISCONNECTED, SessionStatus.CONNECTED); + } + + void bind(MQTTConnection mqttConnection) { + this.mqttConnection = mqttConnection; + } + + public boolean disconnected() { + return status.get() == SessionStatus.DISCONNECTED; + } + + public boolean connected() { + return status.get() == SessionStatus.CONNECTED; + } + + public String getClientID() { + return clientId; + } + + public List getSubscriptions() { + return new ArrayList<>(subscriptions); + } + + public void addSubscriptions(List newSubscriptions) { + subscriptions.addAll(newSubscriptions); + } + + public boolean hasWill() { + return will != null; + } + + public Will getWill() { + return will; + } + + boolean assignState(SessionStatus expected, SessionStatus newState) { + return status.compareAndSet(expected, newState); + } + + public void closeImmediately() { + mqttConnection.dropConnection(); + } + + public void disconnect() { + final boolean res = assignState(SessionStatus.CONNECTED, SessionStatus.DISCONNECTING); + if (!res) { + // someone already moved away from CONNECTED + // TODO what to do? + return; + } + + mqttConnection = null; + will = null; + + assignState(SessionStatus.DISCONNECTING, SessionStatus.DISCONNECTED); + } + + boolean isClean() { + return clean; + } + + public void processPubRec(int packetId) { + inflightWindow.remove(packetId); + inflightSlots.incrementAndGet(); + if (canSkipQueue()) { + inflightSlots.decrementAndGet(); + int pubRelPacketId = mqttConnection.nextPacketId(); + inflightWindow.put(pubRelPacketId, new SessionRegistry.PubRelMarker()); + inflightTimeouts.add(new InFlightPacket(pubRelPacketId, FLIGHT_BEFORE_RESEND_MS)); + MqttMessage pubRel = MQTTConnection.pubrel(pubRelPacketId); + mqttConnection.sendIfWritableElseDrop(pubRel); + + drainQueueToConnection(); + } else { + sessionQueue.add(new SessionRegistry.PubRelMarker()); + } + } + + public void processPubComp(int messageID) { + inflightWindow.remove(messageID); + inflightSlots.incrementAndGet(); + + drainQueueToConnection(); + + // TODO notify the interceptor +// final InterceptAcknowledgedMessage interceptAckMsg = new InterceptAcknowledgedMessage(inflightMsg, topic, +// username, messageID); +// m_interceptor.notifyMessageAcknowledged(interceptAckMsg); + } + + public void sendPublishOnSessionAtQos(Topic topic, MqttQoS qos, ByteBuf payload) { + switch (qos) { + case AT_MOST_ONCE: + if (connected()) { + mqttConnection.sendPublishNotRetainedQos0(topic, qos, payload); + } + break; + case AT_LEAST_ONCE: + sendPublishQos1(topic, qos, payload); + break; + case EXACTLY_ONCE: + sendPublishQos2(topic, qos, payload); + break; + case FAILURE: + LOG.error("Not admissible"); + } + } + + private void sendPublishQos1(Topic topic, MqttQoS qos, ByteBuf payload) { + if (!connected() && isClean()) { + //pushing messages to disconnected not clean session + return; + } + + if (canSkipQueue()) { + inflightSlots.decrementAndGet(); + int packetId = mqttConnection.nextPacketId(); + inflightWindow.put(packetId, new SessionRegistry.PublishedMessage(topic, qos, payload)); + inflightTimeouts.add(new InFlightPacket(packetId, FLIGHT_BEFORE_RESEND_MS)); + MqttPublishMessage publishMsg = MQTTConnection.notRetainedPublishWithMessageId(topic.toString(), qos, payload, packetId); + mqttConnection.sendPublish(publishMsg); + + // TODO drainQueueToConnection();? + } else { + final SessionRegistry.PublishedMessage msg = new SessionRegistry.PublishedMessage(topic, qos, payload); + sessionQueue.add(msg); + } + } + + private void sendPublishQos2(Topic topic, MqttQoS qos, ByteBuf payload) { + if (canSkipQueue()) { + inflightSlots.decrementAndGet(); + int packetId = mqttConnection.nextPacketId(); + inflightWindow.put(packetId, new SessionRegistry.PublishedMessage(topic, qos, payload)); + inflightTimeouts.add(new InFlightPacket(packetId, FLIGHT_BEFORE_RESEND_MS)); + MqttPublishMessage publishMsg = MQTTConnection.notRetainedPublishWithMessageId(topic.toString(), qos, payload, packetId); + mqttConnection.sendPublish(publishMsg); + + drainQueueToConnection(); + } else { + final SessionRegistry.PublishedMessage msg = new SessionRegistry.PublishedMessage(topic, qos, payload); + sessionQueue.add(msg); + } + } + + private boolean canSkipQueue() { + return sessionQueue.isEmpty() && + inflightSlots.get() > 0 && + connected() && + mqttConnection.channel.isWritable(); + } + + void pubAckReceived(int ackPacketId) { + // TODO remain to invoke in somehow m_interceptor.notifyMessageAcknowledged + inflightWindow.remove(ackPacketId); + inflightSlots.incrementAndGet(); + drainQueueToConnection(); + } + + public void resendInflightNotAcked() { + Collection expired = new ArrayList<>(INFLIGHT_WINDOW_SIZE); + inflightTimeouts.drainTo(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); + mqttConnection.sendPublish(publishMsg); + } + } + } + + 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); + return new MqttPublishMessage(fixedHeader, varHeader, payload); + } + + private void drainQueueToConnection() { + // consume the queue + while (!canSkipQueue()) { + final SessionRegistry.EnqueuedMessage msg = sessionQueue.remove(); + inflightSlots.decrementAndGet(); + int sendPacketId = mqttConnection.nextPacketId(); + inflightWindow.put(sendPacketId, msg); + if (msg instanceof SessionRegistry.PubRelMarker) { + MqttMessage pubRel = MQTTConnection.pubrel(sendPacketId); + mqttConnection.sendIfWritableElseDrop(pubRel); + } else { + final SessionRegistry.PublishedMessage msgPub = (SessionRegistry.PublishedMessage) msg; + MqttPublishMessage publishMsg = MQTTConnection.notRetainedPublishWithMessageId(msgPub.topic.toString(), + msgPub.publishingQos, + msgPub.payload, sendPacketId); + mqttConnection.sendPublish(publishMsg); + } + } + } + + public void writabilityChanged() { + drainQueueToConnection(); + } + + public void sendQueuedMessagesWhileOffline() { + LOG.trace("Republishing all saved messages for session {} on CId={}", this, this.clientId); + drainQueueToConnection(); + } + + void sendRetainedPublishOnSessionAtQos(Topic topic, MqttQoS qos, ByteBuf payload) { + if (qos != MqttQoS.AT_MOST_ONCE) { + // QoS 1 or 2 + mqttConnection.sendPublishRetainedWithPacketId(topic, qos, payload); + } else { + mqttConnection.sendPublishRetainedQos0(topic, qos, payload); + } + } + + public void receivedPublishQos2(int messageID, MqttPublishMessage msg) { + qos2Receiving.put(messageID, msg); + msg.retain(); + mqttConnection.sendPublishReceived(messageID); + } + + public void receivedPubRelQos2(int messageID) { + qos2Receiving.remove(messageID); + } + + @Override + public String toString() { + return "Session{" + + "clientId='" + clientId + '\'' + + ", clean=" + clean + + ", status=" + status + + ", inflightSlots=" + inflightSlots + + '}'; + } +} diff --git a/broker/src/main/java/io/moquette/broker/SessionCorruptedException.java b/broker/src/main/java/io/moquette/broker/SessionCorruptedException.java new file mode 100644 index 00000000..cbbd877c --- /dev/null +++ b/broker/src/main/java/io/moquette/broker/SessionCorruptedException.java @@ -0,0 +1,10 @@ +package io.moquette.broker; + +public class SessionCorruptedException extends RuntimeException { + + private static final long serialVersionUID = 5848069213104389412L; + + SessionCorruptedException(String msg) { + super(msg); + } +} diff --git a/broker/src/main/java/io/moquette/broker/SessionRegistry.java b/broker/src/main/java/io/moquette/broker/SessionRegistry.java new file mode 100644 index 00000000..0611878e --- /dev/null +++ b/broker/src/main/java/io/moquette/broker/SessionRegistry.java @@ -0,0 +1,220 @@ +package io.moquette.broker; + +import io.moquette.broker.Session.SessionStatus; +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.buffer.Unpooled; +import io.netty.handler.codec.mqtt.MqttConnectMessage; +import io.netty.handler.codec.mqtt.MqttQoS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; + +class SessionRegistry { + + static abstract class EnqueuedMessage { + } + + static class PublishedMessage extends EnqueuedMessage { + + final Topic topic; + final MqttQoS publishingQos; + final ByteBuf payload; + + PublishedMessage(Topic topic, MqttQoS publishingQos, ByteBuf payload) { + this.topic = topic; + this.publishingQos = publishingQos; + this.payload = payload; + } + } + + static final class PubRelMarker extends EnqueuedMessage { + } + + private enum PostConnectAction { + NONE, SEND_STORED_MESSAGES + } + + private static final Logger LOG = LoggerFactory.getLogger(SessionRegistry.class); + + private final ConcurrentMap pool = new ConcurrentHashMap<>(); + private final ISubscriptionsDirectory subscriptionsDirectory; + private final ConcurrentMap> queues = new ConcurrentHashMap<>(); + + SessionRegistry(ISubscriptionsDirectory subscriptionsDirectory) { + this.subscriptionsDirectory = subscriptionsDirectory; + } + + void bindToSession(MQTTConnection mqttConnection, MqttConnectMessage msg, String clientId) { + boolean isSessionAlreadyStored = false; + PostConnectAction postConnectAction = PostConnectAction.NONE; + if (!pool.containsKey(clientId)) { + // case 1 + final Session newSession = createNewSession(mqttConnection, msg, clientId); + + // publish the session + final Session previous = pool.putIfAbsent(clientId, newSession); + final boolean success = previous == null; + + if (success) { + LOG.trace("case 1, not existing session with CId {}", clientId); + } else { + postConnectAction = bindToExistingSession(mqttConnection, msg, clientId, newSession); + isSessionAlreadyStored = true; + } + } else { + final Session newSession = createNewSession(mqttConnection, msg, clientId); + postConnectAction = bindToExistingSession(mqttConnection, msg, clientId, newSession); + isSessionAlreadyStored = true; + } + final boolean msgCleanSessionFlag = msg.variableHeader().isCleanSession(); + boolean isSessionAlreadyPresent = (!msgCleanSessionFlag && isSessionAlreadyStored); + mqttConnection.sendConnAck(isSessionAlreadyPresent); + + if (postConnectAction == PostConnectAction.SEND_STORED_MESSAGES) { + final Session session = pool.get(clientId); + session.sendQueuedMessagesWhileOffline(); + } + } + + private PostConnectAction bindToExistingSession(MQTTConnection mqttConnection, MqttConnectMessage msg, + String clientId, Session newSession) { + PostConnectAction postConnectAction = PostConnectAction.NONE; + final boolean newIsClean = msg.variableHeader().isCleanSession(); + final Session oldSession = pool.get(clientId); + if (newIsClean && oldSession.disconnected()) { + // case 2 + dropQueuesForClient(clientId); + unsubscribe(oldSession); + + // publish new session + boolean result = oldSession.assignState(SessionStatus.DISCONNECTED, SessionStatus.CONNECTING); + if (!result) { + throw new SessionCorruptedException("old session was already changed state"); + } + copySessionConfig(msg, oldSession); + oldSession.bind(mqttConnection); + + result = oldSession.assignState(SessionStatus.CONNECTING, SessionStatus.CONNECTED); + if (!result) { + throw new SessionCorruptedException("old session moved in connected state by other thread"); + } + final boolean published = pool.replace(clientId, oldSession, oldSession); + if (!published) { + throw new SessionCorruptedException("old session was already removed"); + } + LOG.trace("case 2, oldSession with same CId {} disconnected", clientId); + } else if (!newIsClean && oldSession.disconnected()) { + // case 3 + reactivateSubscriptions(oldSession); + + // mark as connected + final boolean connecting = oldSession.assignState(SessionStatus.DISCONNECTED, SessionStatus.CONNECTING); + if (!connecting) { + throw new SessionCorruptedException("old session moved in connected state by other thread"); + } + oldSession.bind(mqttConnection); + + final boolean connected = oldSession.assignState(SessionStatus.CONNECTING, SessionStatus.CONNECTED); + if (!connected) { + throw new SessionCorruptedException("old session moved in other state state by other thread"); + } + + // publish new session + final boolean published = pool.replace(clientId, oldSession, oldSession); + if (!published) { + throw new SessionCorruptedException("old session was already removed"); + } + postConnectAction = PostConnectAction.SEND_STORED_MESSAGES; + LOG.trace("case 3, oldSession with same CId {} disconnected", clientId); + } else if (oldSession.connected()) { + // case 4 + LOG.trace("case 4, oldSession with same CId {} still connected, force to close", clientId); + oldSession.closeImmediately(); + //remove(clientId); + // publish new session + final boolean published = pool.replace(clientId, oldSession, newSession); + if (!published) { + throw new SessionCorruptedException("old session was already removed"); + } + } + // case not covered new session is clean true/false and old session not in CONNECTED/DISCONNECTED + return postConnectAction; + } + + private void reactivateSubscriptions(Session session) { + for (Subscription existingSub : session.getSubscriptions()) { + // TODO +// subscriptionsDirectory.reactivate(existingSub.getTopicFilter(), session.getClientID()); + } + } + + private void unsubscribe(Session session) { + for (Subscription existingSub : session.getSubscriptions()) { + subscriptionsDirectory.removeSubscription(existingSub.getTopicFilter(), session.getClientID()); + } + } + + private Session createNewSession(MQTTConnection mqttConnection, MqttConnectMessage msg, String clientId) { + final Queue sessionQueue = queues.computeIfAbsent(clientId, (String cli) -> new ConcurrentLinkedQueue<>()); + final boolean clean = msg.variableHeader().isCleanSession(); + final Session newSession; + if (msg.variableHeader().isWillFlag()) { + final Session.Will will = createWill(msg); + newSession = new Session(clientId, clean, will, sessionQueue); + } else { + newSession = new Session(clean, clientId, sessionQueue); + } + + newSession.markConnected(); + newSession.bind(mqttConnection); + + return newSession; + } + + private void copySessionConfig(MqttConnectMessage msg, Session session) { + final boolean clean = msg.variableHeader().isCleanSession(); + final Session.Will will; + if (msg.variableHeader().isWillFlag()) { + will = createWill(msg); + } else { + will = null; + } + session.update(clean, will); + } + + private Session.Will createWill(MqttConnectMessage msg) { + final ByteBuf willPayload = Unpooled.copiedBuffer(msg.payload().willMessageInBytes()); + final String willTopic = msg.payload().willTopic(); + final boolean retained = msg.variableHeader().isWillRetain(); + final MqttQoS qos = MqttQoS.valueOf(msg.variableHeader().willQos()); + return new Session.Will(willTopic, willPayload, qos, retained); + } + + Session retrieve(String clientID) { + return pool.get(clientID); + } + + public void remove(String clientID) { + pool.remove(clientID); + } + + public void disconnect(String clientID) { + final Session session = retrieve(clientID); + if (session == null) { + LOG.debug("Some other thread already removed the session CId={}", clientID); + return; + } + session.disconnect(); + } + + private void dropQueuesForClient(String clientId) { + queues.remove(clientId); + } +} diff --git a/broker/src/main/java/io/moquette/server/Server.java b/broker/src/main/java/io/moquette/server/Server.java index e053dc2f..36fcefd8 100644 --- a/broker/src/main/java/io/moquette/server/Server.java +++ b/broker/src/main/java/io/moquette/server/Server.java @@ -25,7 +25,7 @@ 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.IAuthorizator; +import io.moquette.spi.security.IAuthorizatorPolicy; import io.moquette.spi.security.ISslContextCreator; import io.netty.handler.codec.mqtt.MqttPublishMessage; import org.slf4j.Logger; @@ -141,7 +141,7 @@ public class Server { } public void startServer(IConfig config, List handlers, ISslContextCreator sslCtxCreator, - IAuthenticator authenticator, IAuthorizator authorizator) throws IOException { + IAuthenticator authenticator, IAuthorizatorPolicy authorizator) throws IOException { final long start = System.currentTimeMillis(); if (handlers == null) { handlers = Collections.emptyList(); diff --git a/broker/src/main/java/io/moquette/server/netty/MoquetteIdleTimeoutHandler.java b/broker/src/main/java/io/moquette/server/netty/MoquetteIdleTimeoutHandler.java index 1b5a945f..a3a4eef9 100644 --- a/broker/src/main/java/io/moquette/server/netty/MoquetteIdleTimeoutHandler.java +++ b/broker/src/main/java/io/moquette/server/netty/MoquetteIdleTimeoutHandler.java @@ -27,7 +27,7 @@ import io.netty.handler.timeout.IdleStateEvent; import static io.netty.channel.ChannelFutureListener.CLOSE_ON_FAILURE; @Sharable -class MoquetteIdleTimeoutHandler extends ChannelDuplexHandler { +public class MoquetteIdleTimeoutHandler extends ChannelDuplexHandler { private static final Logger LOG = LoggerFactory.getLogger(MoquetteIdleTimeoutHandler.class); diff --git a/broker/src/main/java/io/moquette/spi/impl/ConnectHandler.java b/broker/src/main/java/io/moquette/spi/impl/ConnectHandler.java index 17f92c65..43fed374 100644 --- a/broker/src/main/java/io/moquette/spi/impl/ConnectHandler.java +++ b/broker/src/main/java/io/moquette/spi/impl/ConnectHandler.java @@ -10,7 +10,7 @@ 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.IAuthorizator; +import io.moquette.spi.security.IAuthorizatorPolicy; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; @@ -40,7 +40,7 @@ public class ConnectHandler { private SessionsRepository sessionsRepository; private ISessionsStore m_sessionsStore; private IAuthenticator m_authenticator; - private IAuthorizator m_authorizator; + private IAuthorizatorPolicy m_authorizator; private ISubscriptionsDirectory subscriptions; private boolean allowAnonymous; private boolean allowZeroByteClientId; @@ -48,7 +48,7 @@ public class ConnectHandler { private InternalRepublisher internalRepublisher; ConnectHandler(IConnectionsManager connectedClients, BrokerInterceptor interceptor, SessionsRepository sessionsRepository, - ISessionsStore sessionsStore, IAuthenticator authenticator, IAuthorizator authorizator, + ISessionsStore sessionsStore, IAuthenticator authenticator, IAuthorizatorPolicy authorizator, ISubscriptionsDirectory subscriptions, boolean allowAnonymous, boolean allowZeroByteClientId, boolean reauthorizeSubscriptionsOnConnect, InternalRepublisher internalRepublisher) { this.connectionDescriptors = connectedClients; diff --git a/broker/src/main/java/io/moquette/spi/impl/DebugUtils.java b/broker/src/main/java/io/moquette/spi/impl/DebugUtils.java index 5d14a6c3..d4417790 100644 --- a/broker/src/main/java/io/moquette/spi/impl/DebugUtils.java +++ b/broker/src/main/java/io/moquette/spi/impl/DebugUtils.java @@ -20,10 +20,19 @@ import io.netty.buffer.ByteBuf; import java.nio.charset.StandardCharsets; -final class DebugUtils { +public final class DebugUtils { - static String payload2Str(ByteBuf content) { - return new String(content.copy().array(), StandardCharsets.UTF_8); + public static String payload2Str(ByteBuf content) { + final ByteBuf copy = content.copy(); + final byte[] bytesContent; + if (copy.isDirect()) { + final int size = copy.readableBytes(); + bytesContent = new byte[size]; + copy.readBytes(bytesContent); + } else { + bytesContent = copy.array(); + } + return new String(bytesContent, StandardCharsets.UTF_8); } private DebugUtils() { diff --git a/broker/src/main/java/io/moquette/spi/impl/ProtocolProcessor.java b/broker/src/main/java/io/moquette/spi/impl/ProtocolProcessor.java index d91ca4c8..0ba2c64b 100644 --- a/broker/src/main/java/io/moquette/spi/impl/ProtocolProcessor.java +++ b/broker/src/main/java/io/moquette/spi/impl/ProtocolProcessor.java @@ -31,7 +31,7 @@ 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.IAuthorizator; +import io.moquette.spi.security.IAuthorizatorPolicy; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.handler.codec.mqtt.*; @@ -41,11 +41,9 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import static io.moquette.server.ConnectionDescriptor.ConnectionState.*; import static io.moquette.spi.impl.InternalRepublisher.createPublishForQos; import static io.moquette.spi.impl.Utils.messageId; import static io.moquette.spi.impl.Utils.readBytesAndRewind; @@ -72,7 +70,7 @@ public class ProtocolProcessor { private ConcurrentMap subscriptionInCourse; private ISubscriptionsDirectory subscriptions; - private IAuthorizator m_authorizator; + private IAuthorizatorPolicy m_authorizator; private IMessagesStore m_messagesStore; @@ -94,7 +92,7 @@ public class ProtocolProcessor { } public void init(ISubscriptionsDirectory subscriptions, IMessagesStore storageService, ISessionsStore sessionsStore, - IAuthenticator authenticator, boolean allowAnonymous, IAuthorizator authorizator, + IAuthenticator authenticator, boolean allowAnonymous, IAuthorizatorPolicy authorizator, BrokerInterceptor interceptor, SessionsRepository sessionsRepository, boolean reauthorizeSubscriptionsOnConnect) { init(subscriptions, storageService, sessionsStore, authenticator, allowAnonymous, false, @@ -103,7 +101,7 @@ public class ProtocolProcessor { public void init(ISubscriptionsDirectory subscriptions, IMessagesStore storageService, ISessionsStore sessionsStore, IAuthenticator authenticator, boolean allowAnonymous, boolean allowZeroByteClientId, - IAuthorizator authorizator, BrokerInterceptor interceptor, SessionsRepository sessionsRepository, + IAuthorizatorPolicy authorizator, BrokerInterceptor interceptor, SessionsRepository sessionsRepository, boolean reauthorizeSubscriptionsOnConnect) { init(new ConnectionDescriptorStore(), subscriptions, storageService, sessionsStore, authenticator, allowAnonymous, allowZeroByteClientId, authorizator, interceptor, sessionsRepository, @@ -130,7 +128,7 @@ public class ProtocolProcessor { */ void init(IConnectionsManager connectionDescriptors, ISubscriptionsDirectory subscriptions, IMessagesStore storageService, ISessionsStore sessionsStore, IAuthenticator authenticator, - boolean allowAnonymous, boolean allowZeroByteClientId, IAuthorizator authorizator, + boolean allowAnonymous, boolean allowZeroByteClientId, IAuthorizatorPolicy authorizator, BrokerInterceptor interceptor, SessionsRepository sessionsRepository, boolean reauthorizeSubscriptionsOnConnect) { LOG.info("Initializing MQTT protocol processor.."); diff --git a/broker/src/main/java/io/moquette/spi/impl/ProtocolProcessorBootstrapper.java b/broker/src/main/java/io/moquette/spi/impl/ProtocolProcessorBootstrapper.java index b8146d70..123ba81f 100644 --- a/broker/src/main/java/io/moquette/spi/impl/ProtocolProcessorBootstrapper.java +++ b/broker/src/main/java/io/moquette/spi/impl/ProtocolProcessorBootstrapper.java @@ -31,7 +31,7 @@ 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.IAuthorizator; +import io.moquette.spi.security.IAuthorizatorPolicy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -82,7 +82,7 @@ public class ProtocolProcessorBootstrapper { * @return the processor created for the broker. */ public ProtocolProcessor init(IConfig props, List embeddedObservers, - IAuthenticator authenticator, IAuthorizator authorizator, Server server) { + 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); @@ -145,13 +145,13 @@ public class ProtocolProcessorBootstrapper { LOG.info("Configuring MQTT authorizator..."); String authorizatorClassName = props.getProperty(BrokerConstants.AUTHORIZATOR_CLASS_NAME, ""); if (authorizator == null && !authorizatorClassName.isEmpty()) { - authorizator = loadClass(authorizatorClassName, IAuthorizator.class, IConfig.class, props); + 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 DenyAllAuthorizator(); + authorizator = new DenyAllAuthorizatorPolicy(); try { LOG.info("Parsing ACL file. Path = {}", aclFilePath); authorizator = ACLFileParser.parse(resourceLoader.loadResource(aclFilePath)); @@ -159,7 +159,7 @@ public class ProtocolProcessorBootstrapper { LOG.error("Unable to parse ACL file. path=" + aclFilePath, pex); } } else { - authorizator = new PermitAllAuthorizator(); + authorizator = new PermitAllAuthorizatorPolicy(); } LOG.info("An {} authorizator instance will be used", authorizator.getClass().getName()); } diff --git a/broker/src/main/java/io/moquette/spi/impl/Qos0PublishHandler.java b/broker/src/main/java/io/moquette/spi/impl/Qos0PublishHandler.java index a7100ef7..d2372b1a 100644 --- a/broker/src/main/java/io/moquette/spi/impl/Qos0PublishHandler.java +++ b/broker/src/main/java/io/moquette/spi/impl/Qos0PublishHandler.java @@ -19,7 +19,7 @@ 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.IAuthorizator; +import io.moquette.spi.security.IAuthorizatorPolicy; import io.netty.channel.Channel; import io.netty.handler.codec.mqtt.MqttPublishMessage; import org.slf4j.Logger; @@ -34,7 +34,7 @@ class Qos0PublishHandler extends QosPublishHandler { private final BrokerInterceptor m_interceptor; private final MessagesPublisher publisher; - public Qos0PublishHandler(IAuthorizator authorizator, IMessagesStore messagesStore, BrokerInterceptor interceptor, + public Qos0PublishHandler(IAuthorizatorPolicy authorizator, IMessagesStore messagesStore, BrokerInterceptor interceptor, MessagesPublisher messagesPublisher) { super(authorizator); this.m_messagesStore = messagesStore; diff --git a/broker/src/main/java/io/moquette/spi/impl/Qos1PublishHandler.java b/broker/src/main/java/io/moquette/spi/impl/Qos1PublishHandler.java index 88919356..e51f4581 100644 --- a/broker/src/main/java/io/moquette/spi/impl/Qos1PublishHandler.java +++ b/broker/src/main/java/io/moquette/spi/impl/Qos1PublishHandler.java @@ -18,11 +18,10 @@ package io.moquette.spi.impl; import io.moquette.connections.IConnectionsManager; import io.moquette.server.ConnectionDescriptor; -import io.moquette.server.ConnectionDescriptorStore; import io.moquette.server.netty.NettyUtils; import io.moquette.spi.IMessagesStore; import io.moquette.spi.impl.subscriptions.Topic; -import io.moquette.spi.security.IAuthorizator; +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; @@ -47,7 +46,7 @@ class Qos1PublishHandler extends QosPublishHandler { private final IConnectionsManager connectionDescriptors; private final MessagesPublisher publisher; - Qos1PublishHandler(IAuthorizator authorizator, IMessagesStore messagesStore, BrokerInterceptor interceptor, + Qos1PublishHandler(IAuthorizatorPolicy authorizator, IMessagesStore messagesStore, BrokerInterceptor interceptor, IConnectionsManager connectionDescriptors, MessagesPublisher messagesPublisher) { super(authorizator); this.m_messagesStore = messagesStore; diff --git a/broker/src/main/java/io/moquette/spi/impl/Qos2PublishHandler.java b/broker/src/main/java/io/moquette/spi/impl/Qos2PublishHandler.java index c38a0861..f74dd701 100644 --- a/broker/src/main/java/io/moquette/spi/impl/Qos2PublishHandler.java +++ b/broker/src/main/java/io/moquette/spi/impl/Qos2PublishHandler.java @@ -18,14 +18,12 @@ package io.moquette.spi.impl; import io.moquette.connections.IConnectionsManager; 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.IMessagesStore; -import io.moquette.spi.ISessionsStore; import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory; import io.moquette.spi.impl.subscriptions.Topic; -import io.moquette.spi.security.IAuthorizator; +import io.moquette.spi.security.IAuthorizatorPolicy; import io.netty.channel.Channel; import io.netty.handler.codec.mqtt.*; import org.slf4j.Logger; @@ -50,7 +48,7 @@ class Qos2PublishHandler extends QosPublishHandler { private final MessagesPublisher publisher; private final SessionsRepository sessionsRepository; - Qos2PublishHandler(IAuthorizator authorizator, ISubscriptionsDirectory subscriptions, + Qos2PublishHandler(IAuthorizatorPolicy authorizator, ISubscriptionsDirectory subscriptions, IMessagesStore messagesStore, BrokerInterceptor interceptor, IConnectionsManager connectionDescriptors, MessagesPublisher messagesPublisher, SessionsRepository sessionsRepository) { diff --git a/broker/src/main/java/io/moquette/spi/impl/QosPublishHandler.java b/broker/src/main/java/io/moquette/spi/impl/QosPublishHandler.java index 32ce5252..f16725e0 100644 --- a/broker/src/main/java/io/moquette/spi/impl/QosPublishHandler.java +++ b/broker/src/main/java/io/moquette/spi/impl/QosPublishHandler.java @@ -20,16 +20,16 @@ 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.IAuthorizator; +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 IAuthorizator m_authorizator; + protected final IAuthorizatorPolicy m_authorizator; - protected QosPublishHandler(IAuthorizator m_authorizator) { + protected QosPublishHandler(IAuthorizatorPolicy m_authorizator) { this.m_authorizator = m_authorizator; } diff --git a/broker/src/main/java/io/moquette/spi/impl/security/AuthorizationsCollector.java b/broker/src/main/java/io/moquette/spi/impl/security/AuthorizationsCollector.java index 8ada712c..6c53031f 100644 --- a/broker/src/main/java/io/moquette/spi/impl/security/AuthorizationsCollector.java +++ b/broker/src/main/java/io/moquette/spi/impl/security/AuthorizationsCollector.java @@ -17,7 +17,7 @@ package io.moquette.spi.impl.security; import io.moquette.spi.impl.subscriptions.Topic; -import io.moquette.spi.security.IAuthorizator; +import io.moquette.spi.security.IAuthorizatorPolicy; import java.text.ParseException; import java.util.*; @@ -27,7 +27,7 @@ import java.util.*; * * Not thread safe. */ -class AuthorizationsCollector implements IAuthorizator { +class AuthorizationsCollector implements IAuthorizatorPolicy { private List m_globalAuthorizations = new ArrayList<>(); private List m_patternAuthorizations = new ArrayList<>(); diff --git a/broker/src/main/java/io/moquette/spi/impl/security/DenyAllAuthorizator.java b/broker/src/main/java/io/moquette/spi/impl/security/DenyAllAuthorizatorPolicy.java similarity index 87% rename from broker/src/main/java/io/moquette/spi/impl/security/DenyAllAuthorizator.java rename to broker/src/main/java/io/moquette/spi/impl/security/DenyAllAuthorizatorPolicy.java index f370627c..4256e664 100644 --- a/broker/src/main/java/io/moquette/spi/impl/security/DenyAllAuthorizator.java +++ b/broker/src/main/java/io/moquette/spi/impl/security/DenyAllAuthorizatorPolicy.java @@ -17,9 +17,9 @@ package io.moquette.spi.impl.security; import io.moquette.spi.impl.subscriptions.Topic; -import io.moquette.spi.security.IAuthorizator; +import io.moquette.spi.security.IAuthorizatorPolicy; -public class DenyAllAuthorizator implements IAuthorizator { +public class DenyAllAuthorizatorPolicy implements IAuthorizatorPolicy { @Override public boolean canWrite(Topic topic, String user, String client) { diff --git a/broker/src/main/java/io/moquette/spi/impl/security/PermitAllAuthorizator.java b/broker/src/main/java/io/moquette/spi/impl/security/PermitAllAuthorizatorPolicy.java similarity index 87% rename from broker/src/main/java/io/moquette/spi/impl/security/PermitAllAuthorizator.java rename to broker/src/main/java/io/moquette/spi/impl/security/PermitAllAuthorizatorPolicy.java index 2545a0cb..76badac6 100644 --- a/broker/src/main/java/io/moquette/spi/impl/security/PermitAllAuthorizator.java +++ b/broker/src/main/java/io/moquette/spi/impl/security/PermitAllAuthorizatorPolicy.java @@ -17,9 +17,9 @@ package io.moquette.spi.impl.security; import io.moquette.spi.impl.subscriptions.Topic; -import io.moquette.spi.security.IAuthorizator; +import io.moquette.spi.security.IAuthorizatorPolicy; -public class PermitAllAuthorizator implements IAuthorizator { +public class PermitAllAuthorizatorPolicy implements IAuthorizatorPolicy { @Override public boolean canWrite(Topic topic, String user, String client) { diff --git a/broker/src/main/java/io/moquette/spi/impl/subscriptions/CNode.java b/broker/src/main/java/io/moquette/spi/impl/subscriptions/CNode.java index d243c16c..fbfb49c0 100644 --- a/broker/src/main/java/io/moquette/spi/impl/subscriptions/CNode.java +++ b/broker/src/main/java/io/moquette/spi/impl/subscriptions/CNode.java @@ -15,6 +15,8 @@ */ package io.moquette.spi.impl.subscriptions; +import io.netty.handler.codec.mqtt.MqttQoS; + import java.util.*; class CNode { @@ -79,8 +81,8 @@ class CNode { this.children.remove(node); } - CNode addSubscription(String clientId, Topic topic) { - this.subscriptions.add(new Subscription(clientId, topic)); + CNode addSubscription(Subscription newSubscription) { + this.subscriptions.add(new Subscription(newSubscription)); return this; } diff --git a/broker/src/main/java/io/moquette/spi/impl/subscriptions/CTrieSubscriptionDirectory.java b/broker/src/main/java/io/moquette/spi/impl/subscriptions/CTrieSubscriptionDirectory.java index 88994e35..34d9c4d6 100644 --- a/broker/src/main/java/io/moquette/spi/impl/subscriptions/CTrieSubscriptionDirectory.java +++ b/broker/src/main/java/io/moquette/spi/impl/subscriptions/CTrieSubscriptionDirectory.java @@ -98,9 +98,45 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory { * to use fo searching matching subscriptions. * @return the list of matching subscriptions, or empty if not matching. */ - Set match(Topic topic) { - final Set matchingSubs = recursiveMatch(topic, this.root); + @Override + public Set matchWithoutQosSharpening(Topic topic) { + return recursiveMatch(topic, this.root); + } + + @Override + public Set matchQosSharpening(Topic topic) { + final Set subscriptions = matchWithoutQosSharpening(topic); + + Map subsGroupedByClient = new HashMap<>(); + for (Subscription sub : subscriptions) { + Subscription existingSub = subsGroupedByClient.get(sub.clientId); + // update the selected subscriptions if not present or if has a greater qos + if (existingSub == null || existingSub.qosLessThan(sub)) { + subsGroupedByClient.put(sub.clientId, sub); + } + } + 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 match(Topic topic) { + final Set matchingSubs = matchWithoutQosSharpening(topic); // remove the overlapping subscriptions, selecting ones with greatest qos + Map subsForClient = loadSubscriptionsFromStorage(matchingSubs); + return new HashSet<>(subsForClient.values()); + } + + @Deprecated + private Map loadSubscriptionsFromStorage(Set matchingSubs) { Map subsForClient = new HashMap<>(); for (Subscription matchingSub : matchingSubs) { Subscription existingSub = subsForClient.get(matchingSub.clientId); @@ -120,7 +156,7 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory { subsForClient.put(sub.clientId, sub); } } - return new HashSet<>(subsForClient.values()); + return subsForClient; } Set recursiveMatch(Topic topic, INode inode) { @@ -171,28 +207,28 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory { public void add(Subscription newSubscription) { Action res; do { - res = insert(newSubscription.clientId, newSubscription.topicFilter, this.root, newSubscription.topicFilter); + res = insert(newSubscription.topicFilter, this.root, newSubscription); } while (res == Action.REPEAT); } - private Action insert(String clientId, Topic topic, final INode inode, Topic fullpath) { + private Action insert(Topic topic, final INode inode, Subscription newSubscription) { Token token = topic.headToken(); if (!topic.isEmpty() && inode.mainNode().anyChildrenMatch(token)) { Topic remainingTopic = topic.exceptHeadToken(); INode nextInode = inode.mainNode().childOf(token); - return insert(clientId, remainingTopic, nextInode, fullpath); + return insert(remainingTopic, nextInode, newSubscription); } else { if (topic.isEmpty()) { - return insertSubscription(clientId, fullpath, inode); + return insertSubscription(inode, newSubscription); } else { - return createNodeAndInsertSubscription(clientId, topic, inode, fullpath); + return createNodeAndInsertSubscription(topic, inode, newSubscription); } } } - private Action insertSubscription(String clientId, Topic topic, INode inode) { + private Action insertSubscription(INode inode, Subscription newSubscription) { CNode cnode = inode.mainNode(); - CNode updatedCnode = cnode.copy().addSubscription(clientId, topic); + CNode updatedCnode = cnode.copy().addSubscription(newSubscription); if (inode.compareAndSet(cnode, updatedCnode)) { return Action.OK; } else { @@ -200,8 +236,8 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory { } } - private Action createNodeAndInsertSubscription(String clientId, Topic topic, INode inode, Topic fullpath) { - INode newInode = createPathRec(clientId, topic, fullpath); + private Action createNodeAndInsertSubscription(Topic topic, INode inode, Subscription newSubscription) { + INode newInode = createPathRec(topic, newSubscription); CNode cnode = inode.mainNode(); CNode updatedCnode = cnode.copy(); updatedCnode.add(newInode); @@ -209,24 +245,24 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory { return inode.compareAndSet(cnode, updatedCnode) ? Action.OK : Action.REPEAT; } - private INode createLeafNodes(String clientId, Topic fullpath, Token token) { + private INode createLeafNodes(Token token, Subscription newSubscription) { CNode newLeafCnode = new CNode(); newLeafCnode.token = token; - newLeafCnode.addSubscription(clientId, fullpath); + newLeafCnode.addSubscription(newSubscription); return new INode(newLeafCnode); } - private INode createPathRec(String clientId, Topic topic, Topic fullpath) { + private INode createPathRec(Topic topic, Subscription newSubscription) { Topic remainingTopic = topic.exceptHeadToken(); if (!remainingTopic.isEmpty()) { - INode inode = createPathRec(clientId, remainingTopic, fullpath); + INode inode = createPathRec(remainingTopic, newSubscription); CNode cnode = new CNode(); cnode.token = topic.headToken(); cnode.add(inode); return new INode(cnode); } else { - return createLeafNodes(clientId, fullpath, topic.headToken()); + return createLeafNodes(topic.headToken(), newSubscription); } } diff --git a/broker/src/main/java/io/moquette/spi/impl/subscriptions/ISubscriptionsDirectory.java b/broker/src/main/java/io/moquette/spi/impl/subscriptions/ISubscriptionsDirectory.java index abf80ac9..b5a89cd7 100644 --- a/broker/src/main/java/io/moquette/spi/impl/subscriptions/ISubscriptionsDirectory.java +++ b/broker/src/main/java/io/moquette/spi/impl/subscriptions/ISubscriptionsDirectory.java @@ -18,11 +18,16 @@ package io.moquette.spi.impl.subscriptions; import io.moquette.spi.impl.SessionsRepository; import java.util.List; +import java.util.Set; public interface ISubscriptionsDirectory { void init(SessionsRepository sessionsRepository); + Set matchWithoutQosSharpening(Topic topic); + + Set matchQosSharpening(Topic topic); + void add(Subscription newSubscription); void removeSubscription(Topic topic, String clientID); diff --git a/broker/src/main/java/io/moquette/spi/impl/subscriptions/TNode.java b/broker/src/main/java/io/moquette/spi/impl/subscriptions/TNode.java index 283c8051..e3e43e93 100644 --- a/broker/src/main/java/io/moquette/spi/impl/subscriptions/TNode.java +++ b/broker/src/main/java/io/moquette/spi/impl/subscriptions/TNode.java @@ -33,7 +33,7 @@ class TNode extends CNode { } @Override - CNode addSubscription(String clientId, Topic topic) { + CNode addSubscription(Subscription newSubscription) { throw new IllegalStateException("Can't be invoked on TNode"); } diff --git a/broker/src/main/java/io/moquette/spi/security/IAuthorizator.java b/broker/src/main/java/io/moquette/spi/security/IAuthorizatorPolicy.java similarity index 97% rename from broker/src/main/java/io/moquette/spi/security/IAuthorizator.java rename to broker/src/main/java/io/moquette/spi/security/IAuthorizatorPolicy.java index 8cff8d6b..a051f54c 100644 --- a/broker/src/main/java/io/moquette/spi/security/IAuthorizator.java +++ b/broker/src/main/java/io/moquette/spi/security/IAuthorizatorPolicy.java @@ -24,7 +24,7 @@ import io.moquette.spi.impl.subscriptions.Topic; * Create an authorizator that matches topic names with same grammar of subscriptions. The # is * always a terminator and its the multilevel matcher. The + sign is the single level matcher. */ -public interface IAuthorizator { +public interface IAuthorizatorPolicy { /** * Ask the implementation of the authorizator if the topic can be used in a publish. diff --git a/broker/src/test/java/io/moquette/broker/ConnectionTestUtils.java b/broker/src/test/java/io/moquette/broker/ConnectionTestUtils.java new file mode 100644 index 00000000..b28c1e15 --- /dev/null +++ b/broker/src/test/java/io/moquette/broker/ConnectionTestUtils.java @@ -0,0 +1,68 @@ +package io.moquette.broker; + +import io.moquette.spi.impl.DebugUtils; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.mqtt.*; + +import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_ACCEPTED; +import static org.junit.Assert.*; + +class ConnectionTestUtils { + + static void assertConnectAccepted(EmbeddedChannel channel) { + MqttConnAckMessage connAck = channel.readOutbound(); + final MqttConnectReturnCode connAckReturnCode = connAck.variableHeader().connectReturnCode(); + assertEquals("Connect must be accepted", CONNECTION_ACCEPTED, connAckReturnCode); + } + + static void verifyReceivePublish(EmbeddedChannel embeddedChannel, String expectedTopic, String expectedContent) { + MqttPublishMessage receivedPublish = embeddedChannel.flushOutbound().readOutbound(); + assertPublishIsCorrect(expectedTopic, expectedContent, receivedPublish); + } + + private static void assertPublishIsCorrect(String expectedTopic, String expectedContent, MqttPublishMessage receivedPublish) { + assertNotNull("Expecting a PUBLISH message", receivedPublish); + final String decodedPayload = DebugUtils.payload2Str(receivedPublish.payload()); + assertEquals(expectedContent, decodedPayload); + assertEquals(expectedTopic, receivedPublish.variableHeader().topicName()); + } + + static void verifyReceiveRetainedPublish(EmbeddedChannel embeddedChannel, String expectedTopic, String expectedContent) { + MqttPublishMessage receivedPublish = embeddedChannel.readOutbound(); + assertPublishIsCorrect(expectedTopic, expectedContent, receivedPublish); + assertTrue("MUST be retained publish", receivedPublish.fixedHeader().isRetain()); + } + + static void verifyReceiveRetainedPublish(EmbeddedChannel embeddedChannel, String expectedTopic, + String expectedContent, MqttQoS expectedQos) { + MqttPublishMessage receivedPublish = embeddedChannel.flushOutbound().readOutbound(); + assertEquals(receivedPublish.fixedHeader().qosLevel(), expectedQos); + assertPublishIsCorrect(expectedTopic, expectedContent, receivedPublish); + assertTrue("MUST be retained publish", receivedPublish.fixedHeader().isRetain()); + } + + static void verifyPublishIsReceived(EmbeddedChannel embCh, MqttQoS expectedQos, String expectedPayload) { + final MqttPublishMessage publishReceived = embCh.flushOutbound().readOutbound(); + final String payloadMessage = DebugUtils.payload2Str(publishReceived.payload()); + assertEquals("Sent and received payload must be identical", expectedPayload, payloadMessage); + assertEquals("Expected QoS don't match", expectedQos, publishReceived.fixedHeader().qosLevel()); + } + + static void verifyNoPublishIsReceived(EmbeddedChannel channel) { + final Object messageReceived = channel.readOutbound(); + assertNull("Received an out message from processor while not expected", messageReceived); + } + + static MqttConnectMessage buildConnect(String clientId) { + return MqttMessageBuilders.connect() + .clientId(clientId) + .build(); + } + + static MqttConnectMessage buildConnectNotClean(String clientId) { + return MqttMessageBuilders.connect() + .clientId(clientId) + .cleanSession(false) + .build(); + } +} diff --git a/broker/src/test/java/io/moquette/broker/MQTTConnectionConnectTest.java b/broker/src/test/java/io/moquette/broker/MQTTConnectionConnectTest.java new file mode 100644 index 00000000..308dfb8f --- /dev/null +++ b/broker/src/test/java/io/moquette/broker/MQTTConnectionConnectTest.java @@ -0,0 +1,307 @@ +package io.moquette.broker; + +import io.moquette.persistence.MemoryStorageService; +import io.moquette.spi.ISessionsStore; +import io.moquette.spi.impl.MockAuthenticator; +import io.moquette.spi.impl.SessionsRepository; +import io.moquette.spi.impl.security.PermitAllAuthorizatorPolicy; +import io.moquette.spi.impl.subscriptions.CTrieSubscriptionDirectory; +import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory; +import io.moquette.spi.security.IAuthenticator; +import io.netty.channel.Channel; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.mqtt.MqttConnectMessage; +import io.netty.handler.codec.mqtt.MqttMessageBuilders; +import io.netty.handler.codec.mqtt.MqttVersion; +import org.junit.Before; +import org.junit.Test; + +import static io.moquette.spi.impl.NettyChannelAssertions.assertEqualsConnAck; +import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.*; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonMap; +import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class MQTTConnectionConnectTest { + + private static final String FAKE_CLIENT_ID = "FAKE_123"; + private static final String TEST_USER = "fakeuser"; + private static final String TEST_PWD = "fakepwd"; + private static final String EVIL_TEST_USER = "eviluser"; + private static final String EVIL_TEST_PWD = "unsecret"; + + private MQTTConnection sut; + private EmbeddedChannel channel; + private SessionRegistry sessionRegistry; + private MqttMessageBuilders.ConnectBuilder connMsg; + private static final BrokerConfiguration CONFIG = new BrokerConfiguration(true, true, false); + private IAuthenticator mockAuthenticator; + private PostOffice postOffice; + + @Before + public void setUp() { + connMsg = MqttMessageBuilders.connect().protocolVersion(MqttVersion.MQTT_3_1).cleanSession(true); + + MemoryStorageService memStorage = new MemoryStorageService(null, null); + ISessionsStore sessionStore = memStorage.sessionsStore(); + mockAuthenticator = new MockAuthenticator(singleton(FAKE_CLIENT_ID), singletonMap(TEST_USER, TEST_PWD)); + + ISubscriptionsDirectory subscriptions = new CTrieSubscriptionDirectory(); + SessionsRepository sessionsRepository = new SessionsRepository(sessionStore, null); + subscriptions.init(sessionsRepository); + + sessionRegistry = new SessionRegistry(subscriptions); + postOffice = new PostOffice(subscriptions, new PermitAllAuthorizatorPolicy(), new MemoryRetainedRepository(), sessionRegistry); + + sut = createMQTTConnection(CONFIG); + channel = (EmbeddedChannel) sut.channel; + } + + private MQTTConnection createMQTTConnection(BrokerConfiguration config) { + EmbeddedChannel channel = new EmbeddedChannel(); + return createMQTTConnection(config, channel, postOffice); + } + + private MQTTConnection createMQTTConnectionWithPostOffice(BrokerConfiguration config, PostOffice postOffice) { + EmbeddedChannel channel = new EmbeddedChannel(); + return createMQTTConnection(config, channel, postOffice); + } + + private MQTTConnection createMQTTConnection(BrokerConfiguration config, Channel channel, PostOffice postOffice) { + return new MQTTConnection(channel, config, mockAuthenticator, sessionRegistry, postOffice); + } + + @Test + public void testZeroByteClientIdWithCleanSession() { + // Connect message with clean session set to true and client id is null. + MqttConnectMessage msg = MqttMessageBuilders.connect() + .protocolVersion(MqttVersion.MQTT_3_1_1) + .clientId(null) + .cleanSession(true) + .build(); + + sut.processConnect(msg); + assertEqualsConnAck("Connection must be accepted", CONNECTION_ACCEPTED, channel.readOutbound()); + assertNotNull("unique clientid must be generated", sut.getClientId()); + assertTrue("clean session flag must be true", sessionRegistry.retrieve(sut.getClientId()).isClean()); + assertTrue("Connection must be open", channel.isOpen()); + } + + @Test + public void invalidAuthentication() { + MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID) + .username(TEST_USER + "_fake") + .password(TEST_PWD) + .build(); + + // Exercise + sut.processConnect(msg); + + // Verify + assertEqualsConnAck(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD, channel.readOutbound()); + assertFalse("Connection should be closed by the broker.", channel.isOpen()); + } + + @Test + public void testConnect_badClientID() { + connMsg.clientId("extremely_long_clientID_greater_than_23").build(); + + // Exercise + sut.processConnect(connMsg.clientId("extremely_long_clientID_greater_than_23").build()); + + // Verify + assertEqualsConnAck(CONNECTION_ACCEPTED, channel.readOutbound()); + } + + @Test + public void testWillIsAccepted() { + MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID).willFlag(true) + .willTopic("topic").willMessage("Topic message").build(); + + // Exercise + // m_handler.setMessaging(mockedMessaging); + sut.processConnect(msg); + + // Verify + assertEqualsConnAck(CONNECTION_ACCEPTED, channel.readOutbound()); + assertTrue("Connection is accepted and therefore should remain open", channel.isOpen()); + } + + @Test + public void testWillIsFired() { + final PostOffice postOfficeMock = mock(PostOffice.class); + sut = createMQTTConnectionWithPostOffice(CONFIG, postOfficeMock); + channel = (EmbeddedChannel) sut.channel; + + MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID).willFlag(true) + .willTopic("topic").willMessage("Topic message").build(); + sut.processConnect(msg); + + // Exercise + sut.handleConnectionLost(); + + // Verify + verify(postOfficeMock).fireWill(any(Session.Will.class)); + assertFalse("Connection is accepted and therefore should remain open", channel.isOpen()); + } + + @Test + public void acceptAnonymousClient() { + MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID).build(); + + // Exercise + sut.processConnect(msg); + + // Verify + assertEqualsConnAck(CONNECTION_ACCEPTED, channel.readOutbound()); + assertTrue("Connection is accepted and therefore must remain open", channel.isOpen()); + } + + @Test + public void validAuthentication() { + MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID) + .username(TEST_USER).password(TEST_PWD).build(); + + // Exercise + sut.processConnect(msg); + + // Verify + assertEqualsConnAck(CONNECTION_ACCEPTED, channel.readOutbound()); + assertTrue("Connection is accepted and therefore must remain open", channel.isOpen()); + } + + @Test + public void noPasswdAuthentication() { + MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID) + .username(TEST_USER) + .build(); + + // Exercise + sut.processConnect(msg); + + // Verify + assertEqualsConnAck(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD, channel.readOutbound()); + assertFalse("Connection must be closed by the broker", channel.isOpen()); + } + + @Test + public void prohibitAnonymousClient() { + MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID).build(); + BrokerConfiguration config = new BrokerConfiguration(false, true, false); + + sut = createMQTTConnection(config); + channel = (EmbeddedChannel) sut.channel; + + // Exercise + sut.processConnect(msg); + + // Verify + assertEqualsConnAck(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD, channel.readOutbound()); + assertFalse("Connection must be closed by the broker", channel.isOpen()); + } + + @Test + public void prohibitAnonymousClient_providingUsername() { + MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID) + .username(TEST_USER + "_fake") + .build(); + BrokerConfiguration config = new BrokerConfiguration(false, true, false); + + createMQTTConnection(config); + + // Exercise + sut.processConnect(msg); + + // Verify + assertEqualsConnAck(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD, channel.readOutbound()); + assertFalse("Connection should be closed by the broker.", channel.isOpen()); + } + + @Test + public void testZeroByteClientIdNotAllowed() { + BrokerConfiguration config = new BrokerConfiguration(false, false, false); + + sut = createMQTTConnection(config); + channel = (EmbeddedChannel) sut.channel; + + // Connect message with clean session set to true and client id is null. + MqttConnectMessage msg = connMsg.clientId(null) + .protocolVersion(MqttVersion.MQTT_3_1_1) + .cleanSession(true) + .build(); + + sut.processConnect(msg); + assertEqualsConnAck("Zero byte client identifiers are not allowed", + CONNECTION_REFUSED_IDENTIFIER_REJECTED, channel.readOutbound()); + assertFalse("Connection must closed", channel.isOpen()); + } + + @Test + public void testZeroByteClientIdWithoutCleanSession() { + // Allow zero byte client ids + // Connect message without clean session set to true but client id is still null + MqttConnectMessage msg = MqttMessageBuilders.connect().clientId(null).protocolVersion(MqttVersion.MQTT_3_1_1) + .build(); + + sut.processConnect(msg); + assertEqualsConnAck("Identifier must be rejected due to having clean session set to false", + CONNECTION_REFUSED_IDENTIFIER_REJECTED, channel.readOutbound()); + assertFalse("Connection must be closed by the broker", channel.isOpen()); + } + + @Test + public void testBindWithSameClientIDBadCredentialsDoesntDropExistingClient() { + // Connect a client1 + MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID) + .username(TEST_USER) + .password(TEST_PWD) + .build(); + sut.processConnect(msg); + assertEqualsConnAck(CONNECTION_ACCEPTED, channel.readOutbound()); + + // create another connect same clientID but with bad credentials + MqttConnectMessage evilClientConnMsg = MqttMessageBuilders.connect() + .protocolVersion(MqttVersion.MQTT_3_1) + .clientId(FAKE_CLIENT_ID) + .username(EVIL_TEST_USER) + .password(EVIL_TEST_PWD) + .build(); + + EmbeddedChannel evilChannel = new EmbeddedChannel(); + + // Exercise + BrokerConfiguration config = new BrokerConfiguration(true, true, false); + final MQTTConnection evilConnection = createMQTTConnection(config, evilChannel, postOffice); + evilConnection.processConnect(evilClientConnMsg); + + // Verify + // the evil client gets a not auth notification + assertEqualsConnAck(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD, evilChannel.readOutbound()); + // the good client remains connected + assertTrue("Original connected client must remain connected", channel.isOpen()); + assertFalse("Channel trying to connect with bad credentials must be closed", evilChannel.isOpen()); + } + + @Test + public void testForceClientDisconnection_issue116() { + MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID) + .username(TEST_USER) + .password(TEST_PWD) + .build(); + sut.processConnect(msg); + assertEqualsConnAck(CONNECTION_ACCEPTED, channel.readOutbound()); + + // now create another connection and check the new one closes the older + MQTTConnection anotherConnection = createMQTTConnection(CONFIG); + anotherConnection.processConnect(msg); + EmbeddedChannel anotherChannel = (EmbeddedChannel) anotherConnection.channel; + assertEqualsConnAck(CONNECTION_ACCEPTED, anotherChannel.readOutbound()); + + // Verify + assertFalse("First 'FAKE_CLIENT_ID' channel MUST be closed by the broker", channel.isOpen()); + assertTrue("Second 'FAKE_CLIENT_ID' channel MUST be still open", anotherChannel.isOpen()); + } +} diff --git a/broker/src/test/java/io/moquette/broker/MQTTConnectionPublishTest.java b/broker/src/test/java/io/moquette/broker/MQTTConnectionPublishTest.java new file mode 100644 index 00000000..15ebee4e --- /dev/null +++ b/broker/src/test/java/io/moquette/broker/MQTTConnectionPublishTest.java @@ -0,0 +1,81 @@ +package io.moquette.broker; + +import io.moquette.persistence.MemoryStorageService; +import io.moquette.spi.ISessionsStore; +import io.moquette.spi.impl.MockAuthenticator; +import io.moquette.spi.impl.SessionsRepository; +import io.moquette.spi.impl.security.PermitAllAuthorizatorPolicy; +import io.moquette.spi.impl.subscriptions.CTrieSubscriptionDirectory; +import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory; +import io.moquette.spi.security.IAuthenticator; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.mqtt.MqttMessageBuilders; +import io.netty.handler.codec.mqtt.MqttPublishMessage; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.netty.handler.codec.mqtt.MqttVersion; +import org.junit.Before; +import org.junit.Test; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonMap; +import static org.junit.Assert.assertFalse; + +public class MQTTConnectionPublishTest { + + private static final String FAKE_CLIENT_ID = "FAKE_123"; + private static final String TEST_USER = "fakeuser"; + private static final String TEST_PWD = "fakepwd"; + + private MQTTConnection sut; + private EmbeddedChannel channel; + private SessionRegistry sessionRegistry; + private MqttMessageBuilders.ConnectBuilder connMsg; + + @Before + public void setUp() { + connMsg = MqttMessageBuilders.connect().protocolVersion(MqttVersion.MQTT_3_1).cleanSession(true); + + BrokerConfiguration config = new BrokerConfiguration(true, true, false); + + createMQTTConnection(config); + } + + private void createMQTTConnection(BrokerConfiguration config) { + channel = new EmbeddedChannel(); + sut = createMQTTConnection(config, channel); + } + + private MQTTConnection createMQTTConnection(BrokerConfiguration config, Channel channel) { + MemoryStorageService memStorage = new MemoryStorageService(null, null); + ISessionsStore sessionStore = memStorage.sessionsStore(); + IAuthenticator mockAuthenticator = new MockAuthenticator(singleton(FAKE_CLIENT_ID), singletonMap(TEST_USER, TEST_PWD)); + + ISubscriptionsDirectory subscriptions = new CTrieSubscriptionDirectory(); + SessionsRepository sessionsRepository = new SessionsRepository(sessionStore, null); + subscriptions.init(sessionsRepository); + + sessionRegistry = new SessionRegistry(subscriptions); + final PostOffice postOffice = new PostOffice(subscriptions, new PermitAllAuthorizatorPolicy(), + new MemoryRetainedRepository(), sessionRegistry); + return new MQTTConnection(channel, config, mockAuthenticator, sessionRegistry, postOffice); + } + + @Test + public void dropConnectionOnPublishWithInvalidTopicFormat() { + // Connect message with clean session set to true and client id is null. + MqttPublishMessage publish = MqttMessageBuilders.publish() + .topicName("") + .retained(false) + .qos(MqttQoS.AT_MOST_ONCE) + .payload(Unpooled.copiedBuffer("Hello MQTT world!".getBytes(UTF_8))).build(); + + sut.processPublish(publish); + + // Verify + assertFalse("Connection should be closed by the broker", channel.isOpen()); + } + +} diff --git a/broker/src/test/java/io/moquette/broker/PostOfficeInternalPublishTest.java b/broker/src/test/java/io/moquette/broker/PostOfficeInternalPublishTest.java new file mode 100644 index 00000000..f24e0bcd --- /dev/null +++ b/broker/src/test/java/io/moquette/broker/PostOfficeInternalPublishTest.java @@ -0,0 +1,298 @@ +package io.moquette.broker; + +import io.moquette.persistence.MemoryStorageService; +import io.moquette.spi.ISessionsStore; +import io.moquette.spi.impl.MockAuthenticator; +import io.moquette.spi.impl.SessionsRepository; +import io.moquette.spi.impl.security.PermitAllAuthorizatorPolicy; +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.impl.subscriptions.Topic; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.mqtt.*; +import org.junit.Before; +import org.junit.Test; + +import java.util.Set; + +import static io.moquette.broker.PostOfficeUnsubscribeTest.CONFIG; +import static io.netty.handler.codec.mqtt.MqttQoS.*; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonMap; +import static org.junit.Assert.*; + +public class PostOfficeInternalPublishTest { + + private static final String FAKE_CLIENT_ID = "FAKE_123"; + private static final String TEST_USER = "fakeuser"; + private static final String TEST_PWD = "fakepwd"; + private static final String PAYLOAD = "Hello MQTT World"; + + private MQTTConnection connection; + private EmbeddedChannel channel; + private PostOffice sut; + private ISubscriptionsDirectory subscriptions; + private MqttConnectMessage connectMessage; + private SessionRegistry sessionRegistry; + private MockAuthenticator mockAuthenticator; + private static final BrokerConfiguration ALLOW_ANONYMOUS_AND_ZERO_BYTES_CLID = + new BrokerConfiguration(true, true, false); + private MemoryRetainedRepository retainedRepository; + + @Before + public void setUp() { + sessionRegistry = initPostOfficeAndSubsystems(); + + mockAuthenticator = new MockAuthenticator(singleton(FAKE_CLIENT_ID), singletonMap(TEST_USER, TEST_PWD)); + connection = createMQTTConnection(ALLOW_ANONYMOUS_AND_ZERO_BYTES_CLID); + + connectMessage = ConnectionTestUtils.buildConnect(FAKE_CLIENT_ID); + + connection.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted(channel); + } + + private MQTTConnection createMQTTConnection(BrokerConfiguration config) { + channel = new EmbeddedChannel(); + return createMQTTConnection(config, channel); + } + + private MQTTConnection createMQTTConnection(BrokerConfiguration config, Channel channel) { + return new MQTTConnection(channel, config, mockAuthenticator, sessionRegistry, sut); + } + + private SessionRegistry initPostOfficeAndSubsystems() { + MemoryStorageService memStorage = new MemoryStorageService(null, null); + ISessionsStore sessionStore = memStorage.sessionsStore(); + + subscriptions = new CTrieSubscriptionDirectory(); + SessionsRepository sessionsRepository = new SessionsRepository(sessionStore, null); + subscriptions.init(sessionsRepository); + retainedRepository = new MemoryRetainedRepository(); + + SessionRegistry sessionRegistry = new SessionRegistry(subscriptions); + sut = new PostOffice(subscriptions, new PermitAllAuthorizatorPolicy(), retainedRepository, sessionRegistry); + return sessionRegistry; + } + + private void internalPublishNotRetainedTo(String topic) { + internalPublishTo(topic, AT_MOST_ONCE, false); + } + + private void internalPublishRetainedTo(String topic) { + internalPublishTo(topic, AT_MOST_ONCE, true); + } + + private void internalPublishTo(String topic, MqttQoS qos, boolean retained) { + MqttPublishMessage publish = MqttMessageBuilders.publish() + .topicName(topic) + .retained(retained) + .qos(qos) + .payload(Unpooled.copiedBuffer(PAYLOAD.getBytes(UTF_8))).build(); + sut.internalPublish(publish, "INTRPUBL"); + } + + @Test + public void testClientSubscribeAfterNotRetainedQoS0IsSent() { +// connection.processConnect(connectMessage); +// ConnectionTestUtils.assertConnectAccepted(channel); + + // Exercise + final String topic = "/topic"; + internalPublishNotRetainedTo(topic); + + subscribe(AT_MOST_ONCE, topic, connection); + + // Verify + verifyNoPublishIsReceived(channel); + } + + @Test + public void testClientSubscribeAfterRetainedQoS0IsSent() { + // Exercise + final String topic = "/topic"; + internalPublishRetainedTo(topic); + + subscribe(AT_MOST_ONCE, topic, connection); + + // Verify + verifyNoPublishIsReceived(channel); + } + + @Test + public void testClientSubscribeBeforeNotRetainedQoS0IsSent() { + subscribe(AT_MOST_ONCE, "/topic", connection); + + // Exercise + internalPublishNotRetainedTo("/topic"); + + // Verify + ConnectionTestUtils.verifyPublishIsReceived(channel, AT_MOST_ONCE, PAYLOAD); + } + + @Test + public void testClientSubscribeBeforeRetainedQoS0IsSent() { + subscribe(AT_MOST_ONCE, "/topic", connection); + + // Exercise + internalPublishRetainedTo("/topic"); + + // Verify + ConnectionTestUtils.verifyPublishIsReceived(channel, AT_MOST_ONCE, PAYLOAD); + } + + @Test + public void testClientSubscribeBeforeNotRetainedQoS1IsSent() { + subscribe(AT_LEAST_ONCE, "/topic", connection); + + // Exercise + internalPublishTo("/topic", AT_LEAST_ONCE, false); + + // Verify + ConnectionTestUtils.verifyPublishIsReceived(channel, AT_LEAST_ONCE, PAYLOAD); + } + + @Test + public void testClientSubscribeAfterNotRetainedQoS1IsSent() { + // Exercise + internalPublishTo("/topic", AT_LEAST_ONCE, false); + subscribe(AT_LEAST_ONCE, "/topic", connection); + + // Verify + verifyNoPublishIsReceived(channel); + } + + @Test + public void testClientSubscribeBeforeRetainedQoS1IsSent() { + subscribe(AT_LEAST_ONCE, "/topic", connection); + + // Exercise + internalPublishTo("/topic", AT_LEAST_ONCE, true); + + // Verify + ConnectionTestUtils.verifyPublishIsReceived(channel, AT_LEAST_ONCE, PAYLOAD); + } + + @Test + public void testClientSubscribeAfterRetainedQoS1IsSent() { + // Exercise + internalPublishTo("/topic", AT_LEAST_ONCE, true); + subscribe(AT_LEAST_ONCE, "/topic", connection); + + // Verify + ConnectionTestUtils.verifyPublishIsReceived(channel, AT_LEAST_ONCE, PAYLOAD); + } + + @Test + public void testClientSubscribeBeforeNotRetainedQoS2IsSent() { + subscribe(EXACTLY_ONCE, "/topic", connection); + + // Exercise + internalPublishTo("/topic", EXACTLY_ONCE, false); + + // Verify + ConnectionTestUtils.verifyPublishIsReceived(channel, EXACTLY_ONCE, PAYLOAD); + } + + @Test + public void testClientSubscribeAfterNotRetainedQoS2IsSent() { + // Exercise + internalPublishTo("/topic", EXACTLY_ONCE, false); + subscribe(EXACTLY_ONCE, "/topic", connection); + + // Verify + verifyNoPublishIsReceived(channel); + } + + @Test + public void testClientSubscribeBeforeRetainedQoS2IsSent() { + subscribe(EXACTLY_ONCE, "/topic", connection); + + // Exercise + internalPublishTo("/topic", EXACTLY_ONCE, true); + + // Verify + ConnectionTestUtils.verifyPublishIsReceived(channel, EXACTLY_ONCE, PAYLOAD); + } + + @Test + public void testClientSubscribeAfterRetainedQoS2IsSent() { + // Exercise + internalPublishTo("/topic", EXACTLY_ONCE, true); + subscribe(EXACTLY_ONCE, "/topic", connection); + + // Verify + ConnectionTestUtils.verifyPublishIsReceived(channel, EXACTLY_ONCE, PAYLOAD); + } + + @Test + public void testClientSubscribeAfterDisconnected() { + subscribe(AT_MOST_ONCE, "foo", connection); + connection.processDisconnect(null); + + internalPublishTo("foo", AT_MOST_ONCE, false); + + verifyNoPublishIsReceived(channel); + } + + @Test + public void testClientSubscribeWithoutCleanSession() { + subscribe(AT_MOST_ONCE, "foo", connection); + connection.processDisconnect(null); + assertEquals(1, subscriptions.size()); + + MQTTConnection anotherConn = createMQTTConnection(CONFIG); + + MqttConnectMessage connectMessage = MqttMessageBuilders.connect() + .clientId(FAKE_CLIENT_ID) + .cleanSession(false) + .build(); + anotherConn.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted((EmbeddedChannel) anotherConn.channel); + + assertEquals(1, subscriptions.size()); + internalPublishTo("foo", MqttQoS.AT_MOST_ONCE, false); + ConnectionTestUtils.verifyPublishIsReceived((EmbeddedChannel) anotherConn.channel, AT_MOST_ONCE, PAYLOAD); + } + + private void subscribe(MqttQoS topic, String newsTopic, MQTTConnection connection) { + MqttSubscribeMessage subscribe = MqttMessageBuilders.subscribe() + .addSubscription(topic, newsTopic) + .messageId(1) + .build(); + sut.subscribeClientToTopics(subscribe, connection.getClientId(), null, this.connection); + + MqttSubAckMessage subAck = ((EmbeddedChannel) this.connection.channel).readOutbound(); + assertEquals(topic.value(), (int) subAck.payload().grantedQoSLevels().get(0)); + } + + protected void subscribe(MQTTConnection connection, String topic, MqttQoS desiredQos) { + EmbeddedChannel channel = (EmbeddedChannel) connection.channel; + MqttSubscribeMessage subscribe = MqttMessageBuilders.subscribe() + .addSubscription(desiredQos, topic) + .messageId(1) + .build(); + sut.subscribeClientToTopics(subscribe, connection.getClientId(), null, connection); + + MqttSubAckMessage subAck = channel.readOutbound(); + assertEquals(desiredQos.value(), (int) subAck.payload().grantedQoSLevels().get(0)); + + final String clientId = connection.getClientId(); + Subscription expectedSubscription = new Subscription(clientId, new Topic(topic), desiredQos); + + final Set matchedSubscriptions = subscriptions.matchWithoutQosSharpening(new Topic(topic)); + assertEquals(1, matchedSubscriptions.size()); + final Subscription onlyMatchedSubscription = matchedSubscriptions.iterator().next(); + assertEquals(expectedSubscription, onlyMatchedSubscription); + } + + + private void verifyNoPublishIsReceived(EmbeddedChannel channel) { + final Object messageReceived = channel.readOutbound(); + assertNull("Received an out message from processor while not expected", messageReceived); + } +} diff --git a/broker/src/test/java/io/moquette/broker/PostOfficePublishTest.java b/broker/src/test/java/io/moquette/broker/PostOfficePublishTest.java new file mode 100644 index 00000000..2e6b3f61 --- /dev/null +++ b/broker/src/test/java/io/moquette/broker/PostOfficePublishTest.java @@ -0,0 +1,381 @@ +package io.moquette.broker; + +import io.moquette.persistence.MemoryStorageService; +import io.moquette.spi.ISessionsStore; +import io.moquette.spi.impl.MockAuthenticator; +import io.moquette.spi.impl.SessionsRepository; +import io.moquette.spi.impl.security.PermitAllAuthorizatorPolicy; +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.impl.subscriptions.Topic; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.mqtt.*; +import org.junit.Before; +import org.junit.Test; + +import java.nio.charset.Charset; +import java.util.*; + +import static io.moquette.broker.PostOfficeUnsubscribeTest.CONFIG; +import static io.netty.handler.codec.mqtt.MqttQoS.AT_LEAST_ONCE; +import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE; +import static io.netty.handler.codec.mqtt.MqttQoS.EXACTLY_ONCE; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonMap; +import static org.junit.Assert.*; + +public class PostOfficePublishTest { + + private static final String FAKE_CLIENT_ID = "FAKE_123"; + private static final String FAKE_CLIENT_ID2 = "FAKE_456"; + static final String SUBSCRIBER_ID = "Subscriber"; + static final String PUBLISHER_ID = "Publisher"; + private static final String TEST_USER = "fakeuser"; + private static final String TEST_PWD = "fakepwd"; + private static final String NEWS_TOPIC = "/news"; + private static final String BAD_FORMATTED_TOPIC = "#MQTTClient"; + + private MQTTConnection connection; + private EmbeddedChannel channel; + private PostOffice sut; + private ISubscriptionsDirectory subscriptions; + public static final String FAKE_USER_NAME = "UnAuthUser"; + private MqttConnectMessage connectMessage; + private SessionRegistry sessionRegistry; + private MockAuthenticator mockAuthenticator; + static final BrokerConfiguration ALLOW_ANONYMOUS_AND_ZERO_BYTES_CLID = + new BrokerConfiguration(true, true, false); + private MemoryRetainedRepository retainedRepository; + + @Before + public void setUp() { + sessionRegistry = initPostOfficeAndSubsystems(); + + mockAuthenticator = new MockAuthenticator(singleton(FAKE_CLIENT_ID), singletonMap(TEST_USER, TEST_PWD)); + connection = createMQTTConnection(ALLOW_ANONYMOUS_AND_ZERO_BYTES_CLID); + + connectMessage = ConnectionTestUtils.buildConnect(FAKE_CLIENT_ID); + } + + private MQTTConnection createMQTTConnection(BrokerConfiguration config) { + channel = new EmbeddedChannel(); + return createMQTTConnection(config, channel); + } + + private MQTTConnection createMQTTConnection(BrokerConfiguration config, Channel channel) { + return new MQTTConnection(channel, config, mockAuthenticator, sessionRegistry, sut); + } + + private SessionRegistry initPostOfficeAndSubsystems() { + MemoryStorageService memStorage = new MemoryStorageService(null, null); + ISessionsStore sessionStore = memStorage.sessionsStore(); + + subscriptions = new CTrieSubscriptionDirectory(); + SessionsRepository sessionsRepository = new SessionsRepository(sessionStore, null); + subscriptions.init(sessionsRepository); + retainedRepository = new MemoryRetainedRepository(); + + SessionRegistry sessionRegistry = new SessionRegistry(subscriptions); + sut = new PostOffice(subscriptions, new PermitAllAuthorizatorPolicy(), retainedRepository, sessionRegistry); + return sessionRegistry; + } + + @Test + public void testPublishQoS0ToItself() { + connection.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted(channel); + + // subscribe + subscribe(AT_MOST_ONCE, NEWS_TOPIC, connection); + + // Exercise + final ByteBuf payload = Unpooled.copiedBuffer("Hello world!", Charset.defaultCharset()); + sut.receivedPublishQos0(new Topic(NEWS_TOPIC), TEST_USER, FAKE_CLIENT_ID, payload, false); + + // Verify + ConnectionTestUtils.verifyReceivePublish(channel, NEWS_TOPIC, "Hello world!"); + } + + @Test + public void testForceClientDisconnection_issue116() { + final MQTTConnection clientXA = connectAs("subscriber"); + subscribe(clientXA, NEWS_TOPIC, AT_MOST_ONCE); + + final MQTTConnection clientXB = connectAs("publisher"); + final ByteBuf anyPayload = Unpooled.copiedBuffer("Hello", Charset.defaultCharset()); + sut.receivedPublishQos2(clientXB, MqttMessageBuilders.publish() + .payload(anyPayload) + .qos(MqttQoS.EXACTLY_ONCE) + .retained(false) + .topicName(NEWS_TOPIC).build()); + + final MQTTConnection clientYA = connectAs("subscriber"); + subscribe(clientYA, NEWS_TOPIC, AT_MOST_ONCE); + + final MQTTConnection clientYB = connectAs("publisher"); + final ByteBuf anyPayload2 = Unpooled.copiedBuffer("Hello 2", Charset.defaultCharset()); + sut.receivedPublishQos2(clientYB, MqttMessageBuilders.publish() + .payload(anyPayload2) + .qos(MqttQoS.EXACTLY_ONCE) + .retained(true) + .topicName(NEWS_TOPIC).build()); + + // Verify + assertFalse("First 'subscriber' channel MUST be closed by the broker", clientXA.channel.isOpen()); + ConnectionTestUtils.verifyPublishIsReceived((EmbeddedChannel) clientYA.channel, AT_MOST_ONCE, "Hello 2"); + } + + private MQTTConnection connectAs(String clientId) { + EmbeddedChannel channel = new EmbeddedChannel(); + MQTTConnection connection = createMQTTConnection(CONFIG, channel); + connection.processConnect(ConnectionTestUtils.buildConnect(clientId)); + ConnectionTestUtils.assertConnectAccepted(channel); + return connection; + } + + private void subscribe(MqttQoS topic, String newsTopic, MQTTConnection connection) { + MqttSubscribeMessage subscribe = MqttMessageBuilders.subscribe() + .addSubscription(topic, newsTopic) + .messageId(1) + .build(); + sut.subscribeClientToTopics(subscribe, connection.getClientId(), null, this.connection); + + MqttSubAckMessage subAck = ((EmbeddedChannel) this.connection.channel).readOutbound(); + assertEquals(topic.value(), (int) subAck.payload().grantedQoSLevels().get(0)); + } + + protected void subscribe(MQTTConnection connection, String topic, MqttQoS desiredQos) { + EmbeddedChannel channel = (EmbeddedChannel) connection.channel; + MqttSubscribeMessage subscribe = MqttMessageBuilders.subscribe() + .addSubscription(desiredQos, topic) + .messageId(1) + .build(); + sut.subscribeClientToTopics(subscribe, connection.getClientId(), null, connection); + + MqttSubAckMessage subAck = channel.readOutbound(); + assertEquals(desiredQos.value(), (int) subAck.payload().grantedQoSLevels().get(0)); + + final String clientId = connection.getClientId(); + Subscription expectedSubscription = new Subscription(clientId, new Topic(topic), desiredQos); + + final Set matchedSubscriptions = subscriptions.matchWithoutQosSharpening(new Topic(topic)); + assertEquals(1, matchedSubscriptions.size()); + final Subscription onlyMatchedSubscription = matchedSubscriptions.iterator().next(); + assertEquals(expectedSubscription, onlyMatchedSubscription); + } + + @Test + public void testPublishToMultipleSubscribers() { + final Set clientIds = new HashSet<>(Arrays.asList(FAKE_CLIENT_ID, FAKE_CLIENT_ID2)); + mockAuthenticator = new MockAuthenticator(clientIds, singletonMap(TEST_USER, TEST_PWD)); + EmbeddedChannel channel1 = new EmbeddedChannel(); + MQTTConnection connection1 = createMQTTConnection(ALLOW_ANONYMOUS_AND_ZERO_BYTES_CLID, channel1); + connection1.processConnect(ConnectionTestUtils.buildConnect(FAKE_CLIENT_ID)); + ConnectionTestUtils.assertConnectAccepted(channel1); + + EmbeddedChannel channel2 = new EmbeddedChannel(); + MQTTConnection connection2 = createMQTTConnection(ALLOW_ANONYMOUS_AND_ZERO_BYTES_CLID, channel2); + connection2.processConnect(ConnectionTestUtils.buildConnect(FAKE_CLIENT_ID2)); + ConnectionTestUtils.assertConnectAccepted(channel2); + + // subscribe + final MqttQoS qos = AT_MOST_ONCE; + final String newsTopic = NEWS_TOPIC; + subscribe(qos, newsTopic, connection1); + subscribe(qos, newsTopic, connection2); + + // Exercise + final ByteBuf payload = Unpooled.copiedBuffer("Hello world!", Charset.defaultCharset()); + sut.receivedPublishQos0(new Topic(NEWS_TOPIC), TEST_USER, FAKE_CLIENT_ID, payload, false); + + // Verify + ConnectionTestUtils.verifyReceivePublish(channel1, NEWS_TOPIC, "Hello world!"); + ConnectionTestUtils.verifyReceivePublish(channel2, NEWS_TOPIC, "Hello world!"); + } + + @Test + public void testPublishWithEmptyPayloadClearRetainedStore() { + connection.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted(channel); + + this.retainedRepository.retain(new Topic(NEWS_TOPIC), MqttMessageBuilders.publish() + .payload(ByteBufUtil.writeAscii(UnpooledByteBufAllocator.DEFAULT, "Hello world!")) + .qos(AT_LEAST_ONCE) + .build()); + + // Exercise + final ByteBuf anyPayload = Unpooled.copiedBuffer("Any payload", Charset.defaultCharset()); + sut.receivedPublishQos0(new Topic(NEWS_TOPIC), TEST_USER, FAKE_CLIENT_ID, anyPayload, true); + + // Verify + assertTrue("QoS0 MUST clean retained message for topic", retainedRepository.isEmtpy()); + } + + @Test + public void testPublishWithQoS1() { + connection.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted(channel); + subscribe(connection, NEWS_TOPIC, AT_LEAST_ONCE); + + // Exercise + final ByteBuf anyPayload = Unpooled.copiedBuffer("Any payload", Charset.defaultCharset()); + sut.receivedPublishQos1(connection, new Topic(NEWS_TOPIC), TEST_USER, anyPayload, 1,true, + MqttMessageBuilders.publish() + .payload(Unpooled.copiedBuffer("Any payload", Charset.defaultCharset())) + .qos(MqttQoS.AT_LEAST_ONCE) + .retained(true) + .topicName(NEWS_TOPIC).build()); + + // Verify + ConnectionTestUtils.verifyPublishIsReceived(channel, AT_LEAST_ONCE, "Any payload"); + } + + @Test + public void testPublishWithQoS2() { + connection.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted(channel); + subscribe(connection, NEWS_TOPIC, EXACTLY_ONCE); + + // Exercise + final ByteBuf anyPayload = Unpooled.copiedBuffer("Any payload", Charset.defaultCharset()); + sut.receivedPublishQos2(connection, MqttMessageBuilders.publish() + .payload(anyPayload) + .qos(MqttQoS.EXACTLY_ONCE) + .retained(true) + .topicName(NEWS_TOPIC).build()); + + // Verify + ConnectionTestUtils.verifyPublishIsReceived(channel, EXACTLY_ONCE, "Any payload"); + } + + // aka testPublishWithQoS1_notCleanSession + @Test + public void forwardQoS1PublishesWhenNotCleanSessionReconnects() { + connection.processConnect(ConnectionTestUtils.buildConnectNotClean(FAKE_CLIENT_ID)); + ConnectionTestUtils.assertConnectAccepted(channel); + subscribe(connection, NEWS_TOPIC, AT_LEAST_ONCE); + connection.processDisconnect(null); + + // publish a QoS 1 message from another client publish a message on the topic + EmbeddedChannel pubChannel = new EmbeddedChannel(); + MQTTConnection pubConn = createMQTTConnection(ALLOW_ANONYMOUS_AND_ZERO_BYTES_CLID, pubChannel); + pubConn.processConnect(ConnectionTestUtils.buildConnect(PUBLISHER_ID)); + ConnectionTestUtils.assertConnectAccepted(pubChannel); + + final ByteBuf anyPayload = Unpooled.copiedBuffer("Any payload", Charset.defaultCharset()); + sut.receivedPublishQos1(pubConn, new Topic(NEWS_TOPIC), TEST_USER, anyPayload, 1,true, + MqttMessageBuilders.publish() + .payload(anyPayload.retainedDuplicate()) + .qos(MqttQoS.AT_LEAST_ONCE) + .topicName(NEWS_TOPIC).build()); + + // simulate a reconnection from the other client + connection = createMQTTConnection(ALLOW_ANONYMOUS_AND_ZERO_BYTES_CLID); + connectMessage = ConnectionTestUtils.buildConnectNotClean(FAKE_CLIENT_ID); + connection.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted(channel); + + // Verify + ConnectionTestUtils.verifyPublishIsReceived(channel, AT_LEAST_ONCE, "Any payload"); + } + + @Test + public void checkReceivePublishedMessage_after_a_reconnect_with_notCleanSession() { + // first connect - subscribe -disconnect + connection.processConnect(ConnectionTestUtils.buildConnectNotClean(FAKE_CLIENT_ID)); + ConnectionTestUtils.assertConnectAccepted(channel); + subscribe(connection, NEWS_TOPIC, AT_LEAST_ONCE); + connection.processDisconnect(null); + + // connect - subscribe from another connection but with same ClientID + EmbeddedChannel secondChannel = new EmbeddedChannel(); + MQTTConnection secondConn = createMQTTConnection(ALLOW_ANONYMOUS_AND_ZERO_BYTES_CLID, secondChannel); + secondConn.processConnect(ConnectionTestUtils.buildConnect(FAKE_CLIENT_ID)); + ConnectionTestUtils.assertConnectAccepted(secondChannel); + subscribe(secondConn, NEWS_TOPIC, AT_LEAST_ONCE); + + // publish a QoS 1 message another client publish a message on the topic + EmbeddedChannel pubChannel = new EmbeddedChannel(); + MQTTConnection pubConn = createMQTTConnection(ALLOW_ANONYMOUS_AND_ZERO_BYTES_CLID, pubChannel); + pubConn.processConnect(ConnectionTestUtils.buildConnect(PUBLISHER_ID)); + ConnectionTestUtils.assertConnectAccepted(pubChannel); + + final ByteBuf anyPayload = Unpooled.copiedBuffer("Any payload", Charset.defaultCharset()); + sut.receivedPublishQos1(pubConn, new Topic(NEWS_TOPIC), TEST_USER, anyPayload, 1,true, + MqttMessageBuilders.publish() + .payload(anyPayload.retainedDuplicate()) + .qos(MqttQoS.AT_LEAST_ONCE) + .topicName(NEWS_TOPIC).build()); + + // Verify that after a reconnection the client receive the message + ConnectionTestUtils.verifyPublishIsReceived(secondChannel, AT_LEAST_ONCE, "Any payload"); + } + + @Test + public void noPublishToInactiveSession() { + // create an inactive session for Subscriber + connection.processConnect(ConnectionTestUtils.buildConnectNotClean(SUBSCRIBER_ID)); + ConnectionTestUtils.assertConnectAccepted(channel); + subscribe(connection, NEWS_TOPIC, AT_LEAST_ONCE); + connection.processDisconnect(null); + + // Exercise + EmbeddedChannel pubChannel = new EmbeddedChannel(); + MQTTConnection pubConn = createMQTTConnection(ALLOW_ANONYMOUS_AND_ZERO_BYTES_CLID, pubChannel); + pubConn.processConnect(ConnectionTestUtils.buildConnect(PUBLISHER_ID)); + ConnectionTestUtils.assertConnectAccepted(pubChannel); + + final ByteBuf anyPayload = Unpooled.copiedBuffer("Any payload", Charset.defaultCharset()); + sut.receivedPublishQos1(pubConn, new Topic(NEWS_TOPIC), TEST_USER, anyPayload, 1,true, + MqttMessageBuilders.publish() + .payload(anyPayload) + .qos(MqttQoS.AT_LEAST_ONCE) + .retained(true) + .topicName(NEWS_TOPIC).build()); + + verifyNoPublishIsReceived(channel); + } + + private void verifyNoPublishIsReceived(EmbeddedChannel channel) { + final Object messageReceived = channel.readOutbound(); + assertNull("Received an out message from processor while not expected", messageReceived); + } + + @Test + public void cleanRetainedMessageStoreWhenPublishWithRetainedQos0IsReceived() { + connection.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted(channel); + + // publish a QoS1 retained message + final ByteBuf anyPayload = Unpooled.copiedBuffer("Any payload", Charset.defaultCharset()); + final MqttPublishMessage publishMsg = MqttMessageBuilders.publish() + .payload(Unpooled.copiedBuffer("Any payload", Charset.defaultCharset())) + .qos(MqttQoS.AT_LEAST_ONCE) + .retained(true) + .topicName(NEWS_TOPIC) + .build(); + sut.receivedPublishQos1(connection, new Topic(NEWS_TOPIC), TEST_USER, anyPayload, 1,true, + publishMsg); + + assertMessageIsRetained(NEWS_TOPIC); + + // publish a QoS0 retained message + // Exercise + final ByteBuf qos0Payload = Unpooled.copiedBuffer("QoS0 payload", Charset.defaultCharset()); + sut.receivedPublishQos0(new Topic(NEWS_TOPIC), TEST_USER, connection.getClientId(), qos0Payload, true); + + // Verify + assertTrue("Retained message for topic /news must be cleared", retainedRepository.isEmtpy()); + } + + private void assertMessageIsRetained(String expectedTopicName) { + MqttPublishMessage msg = retainedRepository.retainedOnTopic(expectedTopicName); + assertEquals(expectedTopicName, msg.variableHeader().topicName()); + } +} diff --git a/broker/src/test/java/io/moquette/broker/PostOfficeSubscribeTest.java b/broker/src/test/java/io/moquette/broker/PostOfficeSubscribeTest.java new file mode 100644 index 00000000..dd59534a --- /dev/null +++ b/broker/src/test/java/io/moquette/broker/PostOfficeSubscribeTest.java @@ -0,0 +1,308 @@ +package io.moquette.broker; + +import io.moquette.persistence.MemoryStorageService; +import io.moquette.server.netty.NettyUtils; +import io.moquette.spi.ISessionsStore; +import io.moquette.spi.impl.MockAuthenticator; +import io.moquette.spi.impl.SessionsRepository; +import io.moquette.spi.impl.security.PermitAllAuthorizatorPolicy; +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.impl.subscriptions.Topic; +import io.moquette.spi.security.IAuthenticator; +import io.moquette.spi.security.IAuthorizatorPolicy; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.mqtt.*; +import org.junit.Before; +import org.junit.Test; + +import java.nio.charset.Charset; +import java.util.List; +import java.util.Set; + +import static io.moquette.broker.PostOfficePublishTest.ALLOW_ANONYMOUS_AND_ZERO_BYTES_CLID; +import static io.moquette.broker.PostOfficePublishTest.SUBSCRIBER_ID; +import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_ACCEPTED; +import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE; +import static io.netty.handler.codec.mqtt.MqttQoS.EXACTLY_ONCE; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonMap; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class PostOfficeSubscribeTest { + + private static final String FAKE_CLIENT_ID = "FAKE_123"; + private static final String TEST_USER = "fakeuser"; + private static final String TEST_PWD = "fakepwd"; + private static final String NEWS_TOPIC = "/news"; + private static final String BAD_FORMATTED_TOPIC = "#MQTTClient"; + + private MQTTConnection connection; + private EmbeddedChannel channel; + private PostOffice sut; + private ISubscriptionsDirectory subscriptions; + public static final String FAKE_USER_NAME = "UnAuthUser"; + private MqttConnectMessage connectMessage; + private IAuthenticator mockAuthenticator; + private SessionRegistry sessionRegistry; + public static final BrokerConfiguration CONFIG = new BrokerConfiguration(true, true, false); + + @Before + public void setUp() { + connectMessage = MqttMessageBuilders.connect() + .clientId(FAKE_CLIENT_ID) + .build(); + + prepareSUT(); + createMQTTConnection(CONFIG); + } + + private void createMQTTConnection(BrokerConfiguration config) { + channel = new EmbeddedChannel(); + connection = createMQTTConnection(config, channel); + } + + private void prepareSUT() { + MemoryStorageService memStorage = new MemoryStorageService(null, null); + ISessionsStore sessionStore = memStorage.sessionsStore(); + mockAuthenticator = new MockAuthenticator(singleton(FAKE_CLIENT_ID), singletonMap(TEST_USER, TEST_PWD)); + + subscriptions = new CTrieSubscriptionDirectory(); + SessionsRepository sessionsRepository = new SessionsRepository(sessionStore, null); + subscriptions.init(sessionsRepository); + + sessionRegistry = new SessionRegistry(subscriptions); + sut = new PostOffice(subscriptions, new PermitAllAuthorizatorPolicy(), new MemoryRetainedRepository(), + sessionRegistry); + } + + private MQTTConnection createMQTTConnection(BrokerConfiguration config, Channel channel) { + return new MQTTConnection(channel, config, mockAuthenticator, sessionRegistry, sut); + } + + protected void connect() { + MqttConnectMessage connectMessage = MqttMessageBuilders.connect() + .clientId(FAKE_CLIENT_ID) + .build(); + connection.processConnect(connectMessage); + MqttConnAckMessage connAck = channel.readOutbound(); + assertEquals("Connect must be accepted", CONNECTION_ACCEPTED, connAck.variableHeader().connectReturnCode()); + } + + @Test + public void testSubscribe() { + connection.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted(channel); + + // Exercise & verify + subscribe(channel, NEWS_TOPIC, AT_MOST_ONCE); + } + + protected void subscribe(EmbeddedChannel channel, String topic, MqttQoS desiredQos) { + MqttSubscribeMessage subscribe = MqttMessageBuilders.subscribe() + .addSubscription(desiredQos, topic) + .messageId(1) + .build(); + sut.subscribeClientToTopics(subscribe, FAKE_CLIENT_ID, null, connection); + + MqttSubAckMessage subAck = channel.readOutbound(); + assertEquals(desiredQos.value(), (int) subAck.payload().grantedQoSLevels().get(0)); + + final String clientId = NettyUtils.clientID(channel); + Subscription expectedSubscription = new Subscription(clientId, new Topic(topic), desiredQos); + + final Set matchedSubscriptions = subscriptions.matchWithoutQosSharpening(new Topic(topic)); + assertEquals(1, matchedSubscriptions.size()); + final Subscription onlyMatchedSubscription = matchedSubscriptions.iterator().next(); + assertEquals(expectedSubscription, onlyMatchedSubscription); + } + + protected void subscribe(MQTTConnection connection, String topic, MqttQoS desiredQos) { + EmbeddedChannel channel = (EmbeddedChannel) connection.channel; + MqttSubscribeMessage subscribe = MqttMessageBuilders.subscribe() + .addSubscription(desiredQos, topic) + .messageId(1) + .build(); + sut.subscribeClientToTopics(subscribe, connection.getClientId(), null, connection); + + MqttSubAckMessage subAck = channel.readOutbound(); + assertEquals(desiredQos.value(), (int) subAck.payload().grantedQoSLevels().get(0)); + + final String clientId = connection.getClientId(); + Subscription expectedSubscription = new Subscription(clientId, new Topic(topic), desiredQos); + + final Set matchedSubscriptions = subscriptions.matchWithoutQosSharpening(new Topic(topic)); + assertEquals(1, matchedSubscriptions.size()); + final Subscription onlyMatchedSubscription = matchedSubscriptions.iterator().next(); + assertEquals(expectedSubscription, onlyMatchedSubscription); + } + + @Test + public void testSubscribedToNotAuthorizedTopic() { + NettyUtils.userName(channel, FAKE_USER_NAME); + + IAuthorizatorPolicy prohibitReadOnNewsTopic = mock(IAuthorizatorPolicy.class); + when(prohibitReadOnNewsTopic.canRead(eq(new Topic(NEWS_TOPIC)), eq(FAKE_USER_NAME), eq(FAKE_CLIENT_ID))) + .thenReturn(false); + + sut = new PostOffice(subscriptions, prohibitReadOnNewsTopic, new MemoryRetainedRepository(), sessionRegistry); + + connection.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted(channel); + + //Exercise + MqttSubscribeMessage subscribe = MqttMessageBuilders.subscribe() + .addSubscription(AT_MOST_ONCE, NEWS_TOPIC) + .messageId(1) + .build(); + sut.subscribeClientToTopics(subscribe, FAKE_CLIENT_ID, FAKE_USER_NAME, connection); + + // Verify + MqttSubAckMessage subAckMsg = channel.readOutbound(); + verifyFailureQos(subAckMsg); + } + + + private void verifyFailureQos(MqttSubAckMessage subAckMsg) { + List grantedQoSes = subAckMsg.payload().grantedQoSLevels(); + assertEquals(1, grantedQoSes.size()); + assertTrue(grantedQoSes.contains(MqttQoS.FAILURE.value())); + } + + @Test + public void testDoubleSubscribe() { + connection.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted(channel); + assertEquals("After CONNECT subscription MUST be empty", 0, subscriptions.size()); + subscribe(channel, NEWS_TOPIC, AT_MOST_ONCE); + assertEquals("After /news subscribe, subscription MUST contain it",1, subscriptions.size()); + + //Exercise & verify + subscribe(channel, NEWS_TOPIC, AT_MOST_ONCE); + } + + @Test + public void testSubscribeWithBadFormattedTopic() { + connection.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted(channel); + assertEquals("After CONNECT subscription MUST be empty", 0, subscriptions.size()); + + //Exercise + MqttSubscribeMessage subscribe = MqttMessageBuilders.subscribe() + .addSubscription(AT_MOST_ONCE, BAD_FORMATTED_TOPIC) + .messageId(1) + .build(); + this.sut.subscribeClientToTopics(subscribe, FAKE_CLIENT_ID, FAKE_USER_NAME, connection); + MqttSubAckMessage subAckMsg = channel.readOutbound(); + + assertEquals("Bad topic CAN'T add any subscription",0, subscriptions.size()); + verifyFailureQos(subAckMsg); + } + + @Test + public void testCleanSession_maintainClientSubscriptions() { + connection.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted(channel); + assertEquals("After CONNECT subscription MUST be empty", 0, subscriptions.size()); + + subscribe(channel, NEWS_TOPIC, AT_MOST_ONCE); + + assertEquals("Subscribe MUST contain one subscription", 1, subscriptions.size()); + + connection.processDisconnect(null); + assertEquals("Disconnection MUSTN'T clear subscriptions", 1, subscriptions.size()); + + EmbeddedChannel anotherChannel = new EmbeddedChannel(); + MQTTConnection anotherConn = createMQTTConnection(ALLOW_ANONYMOUS_AND_ZERO_BYTES_CLID, anotherChannel); + anotherConn.processConnect(ConnectionTestUtils.buildConnect(FAKE_CLIENT_ID)); + ConnectionTestUtils.assertConnectAccepted(anotherChannel); + assertEquals("After a reconnect, subscription MUST be still present", 1, subscriptions.size()); + + final ByteBuf payload = Unpooled.copiedBuffer("Hello world!", Charset.defaultCharset()); + sut.receivedPublishQos0(new Topic(NEWS_TOPIC), TEST_USER, TEST_PWD, payload, false); + + ConnectionTestUtils.verifyPublishIsReceived(anotherChannel, AT_MOST_ONCE, "Hello world!"); + } + + /** + * Check that after a client has connected with clean session false, subscribed to some topic + * and exited, if it reconnects with clean session true, the broker correctly cleanup every + * previous subscription + */ + @Test + public void testCleanSession_correctlyClientSubscriptions() { + connection.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted(channel); + assertEquals("After CONNECT subscription MUST be empty", 0, subscriptions.size()); + + //subscribe(channel, NEWS_TOPIC, AT_MOST_ONCE); + final MqttSubscribeMessage subscribeMsg = MqttMessageBuilders + .subscribe() + .addSubscription(AT_MOST_ONCE, NEWS_TOPIC) + .messageId(1) + .build(); + connection.processSubscribe(subscribeMsg); + assertEquals("Subscribe MUST contain one subscription", 1, subscriptions.size()); + + connection.processDisconnect(null); + assertEquals("Disconnection MUSTN'T clear subscriptions", 1, subscriptions.size()); + + connectMessage = MqttMessageBuilders.connect() + .clientId(FAKE_CLIENT_ID) + .cleanSession(true) + .build(); + channel = new EmbeddedChannel(); + connection = createMQTTConnection(CONFIG, channel); + connection.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted(channel); + assertEquals("After CONNECT with clean, subscription MUST be empty", 0, subscriptions.size()); + + // publish on /news + final ByteBuf payload = Unpooled.copiedBuffer("Hello world!", Charset.defaultCharset()); + sut.receivedPublishQos0(new Topic(NEWS_TOPIC), TEST_USER, TEST_PWD, payload, false); + + // verify no publish is fired + ConnectionTestUtils.verifyNoPublishIsReceived(channel); + } + + @Test + public void testReceiveRetainedPublishRespectingSubscriptionQoSAndNotPublisher() { + // publisher publish a retained message on topic /news + connection.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted(channel); + final ByteBuf payload = Unpooled.copiedBuffer("Hello world!", Charset.defaultCharset()); + final MqttPublishMessage retainedPubQoS1Msg = MqttMessageBuilders.publish() + .payload(payload.retainedDuplicate()) + .qos(MqttQoS.AT_LEAST_ONCE) + .topicName(NEWS_TOPIC).build(); + sut.receivedPublishQos1(connection, new Topic(NEWS_TOPIC), TEST_USER, payload, 1, true, + retainedPubQoS1Msg); + + // subscriber connects subscribe to topic /news and receive the last retained message + EmbeddedChannel subChannel = new EmbeddedChannel(); + MQTTConnection subConn = createMQTTConnection(ALLOW_ANONYMOUS_AND_ZERO_BYTES_CLID, subChannel); + subConn.processConnect(ConnectionTestUtils.buildConnect(SUBSCRIBER_ID)); + ConnectionTestUtils.assertConnectAccepted(subChannel); + subscribe(subConn, NEWS_TOPIC, MqttQoS.AT_MOST_ONCE); + + // Verify publish is received + ConnectionTestUtils.verifyReceiveRetainedPublish(subChannel, NEWS_TOPIC, "Hello world!", MqttQoS.AT_MOST_ONCE); + } + + @Test + public void testLowerTheQosToTheRequestedBySubscription() { + Subscription subQos1 = new Subscription("Sub A", new Topic("a/b"), MqttQoS.AT_LEAST_ONCE); + assertEquals(MqttQoS.AT_LEAST_ONCE, PostOffice.lowerQosToTheSubscriptionDesired(subQos1, EXACTLY_ONCE)); + + Subscription subQos2 = new Subscription("Sub B", new Topic("a/+"), EXACTLY_ONCE); + assertEquals(EXACTLY_ONCE, PostOffice.lowerQosToTheSubscriptionDesired(subQos2, EXACTLY_ONCE)); + } +} diff --git a/broker/src/test/java/io/moquette/broker/PostOfficeUnsubscribeTest.java b/broker/src/test/java/io/moquette/broker/PostOfficeUnsubscribeTest.java new file mode 100644 index 00000000..a8711a20 --- /dev/null +++ b/broker/src/test/java/io/moquette/broker/PostOfficeUnsubscribeTest.java @@ -0,0 +1,352 @@ +package io.moquette.broker; + +import io.moquette.persistence.MemoryStorageService; +import io.moquette.spi.ISessionsStore; +import io.moquette.spi.impl.MockAuthenticator; +import io.moquette.spi.impl.SessionsRepository; +import io.moquette.spi.impl.security.PermitAllAuthorizatorPolicy; +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.impl.subscriptions.Topic; +import io.moquette.spi.security.IAuthenticator; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.mqtt.*; +import org.junit.Before; +import org.junit.Test; + +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.Set; + +import static io.moquette.broker.PostOfficePublishTest.PUBLISHER_ID; +import static io.netty.handler.codec.mqtt.MqttQoS.*; +import static java.util.Collections.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class PostOfficeUnsubscribeTest { + + private static final String FAKE_CLIENT_ID = "FAKE_123"; + private static final String TEST_USER = "fakeuser"; + private static final String TEST_PWD = "fakepwd"; + static final String NEWS_TOPIC = "/news"; + private static final String BAD_FORMATTED_TOPIC = "#MQTTClient"; + + private MQTTConnection connection; + private EmbeddedChannel channel; + private PostOffice sut; + private ISubscriptionsDirectory subscriptions; + private MqttConnectMessage connectMessage; + private IAuthenticator mockAuthenticator; + private SessionRegistry sessionRegistry; + public static final BrokerConfiguration CONFIG = new BrokerConfiguration(true, true, false); + + @Before + public void setUp() { + connectMessage = MqttMessageBuilders.connect() + .clientId(FAKE_CLIENT_ID) + .build(); + + prepareSUT(); + createMQTTConnection(CONFIG); + } + + private void createMQTTConnection(BrokerConfiguration config) { + channel = new EmbeddedChannel(); + connection = createMQTTConnection(config, channel); + } + + private void prepareSUT() { + MemoryStorageService memStorage = new MemoryStorageService(null, null); + ISessionsStore sessionStore = memStorage.sessionsStore(); + mockAuthenticator = new MockAuthenticator(singleton(FAKE_CLIENT_ID), singletonMap(TEST_USER, TEST_PWD)); + + subscriptions = new CTrieSubscriptionDirectory(); + SessionsRepository sessionsRepository = new SessionsRepository(sessionStore, null); + subscriptions.init(sessionsRepository); + + sessionRegistry = new SessionRegistry(subscriptions); + sut = new PostOffice(subscriptions, new PermitAllAuthorizatorPolicy(), new MemoryRetainedRepository(), sessionRegistry); + } + + private MQTTConnection createMQTTConnection(BrokerConfiguration config, Channel channel) { + return new MQTTConnection(channel, config, mockAuthenticator, sessionRegistry, sut); + } + + protected void connect(MQTTConnection connection, String clientId) { + MqttConnectMessage connectMessage = ConnectionTestUtils.buildConnect(clientId); + connect(connection, connectMessage); + } + + protected void connect(MQTTConnection connection, MqttConnectMessage connectMessage) { + connection.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted((EmbeddedChannel) connection.channel); + } + + protected void subscribe(MQTTConnection connection, String topic, MqttQoS desiredQos) { + EmbeddedChannel channel = (EmbeddedChannel) connection.channel; + MqttSubscribeMessage subscribe = MqttMessageBuilders.subscribe() + .addSubscription(desiredQos, topic) + .messageId(1) + .build(); + sut.subscribeClientToTopics(subscribe, connection.getClientId(), null, connection); + + MqttSubAckMessage subAck = channel.readOutbound(); + assertEquals(desiredQos.value(), (int) subAck.payload().grantedQoSLevels().get(0)); + + final String clientId = connection.getClientId(); + Subscription expectedSubscription = new Subscription(clientId, new Topic(topic), desiredQos); + + final Set matchedSubscriptions = subscriptions.matchQosSharpening(new Topic(topic)); + assertEquals(1, matchedSubscriptions.size()); + //assertTrue(matchedSubscriptions.size() >=1); + final Subscription onlyMatchedSubscription = matchedSubscriptions.iterator().next(); + assertEquals(expectedSubscription, onlyMatchedSubscription); + +// assertTrue(matchedSubscriptions.contains(expectedSubscription)); + } + + @Test + public void testUnsubscribeWithBadFormattedTopic() { + connect(this.connection, FAKE_CLIENT_ID); + + // Exercise + sut.unsubscribe(singletonList(BAD_FORMATTED_TOPIC), connection, 1); + + // Verify + assertFalse("Unsubscribe with bad topic MUST close drop the connection, (issue 68)", channel.isOpen()); + } + + @Test + public void testDontNotifyClientSubscribedToTopicAfterDisconnectedAndReconnectOnSameChannel() { + connect(this.connection, FAKE_CLIENT_ID); + subscribe(connection, NEWS_TOPIC, AT_MOST_ONCE); + + // publish on /news + final ByteBuf payload = Unpooled.copiedBuffer("Hello world!", Charset.defaultCharset()); + sut.receivedPublishQos0(new Topic(NEWS_TOPIC), TEST_USER, TEST_PWD, payload, false); + + ConnectionTestUtils.verifyPublishIsReceived(channel, AT_MOST_ONCE, "Hello world!"); + + unsubscribeAndVerifyAck(NEWS_TOPIC); + + // publish on /news + final ByteBuf payload2 = Unpooled.copiedBuffer("Hello world!", Charset.defaultCharset()); + sut.receivedPublishQos0(new Topic(NEWS_TOPIC), TEST_USER, TEST_PWD, payload2, false); + + ConnectionTestUtils.verifyNoPublishIsReceived(channel); + } + + protected void unsubscribeAndVerifyAck(String topic) { + final int messageId = 1; + + sut.unsubscribe(Collections.singletonList(topic), connection, messageId); + + MqttUnsubAckMessage unsubAckMessageAck = channel.readOutbound(); + assertEquals("Unsubscribe must be accepted", messageId, unsubAckMessageAck.variableHeader().messageId()); + } + + @Test + public void testDontNotifyClientSubscribedToTopicAfterDisconnectedAndReconnectOnNewChannel() { + connect(this.connection, FAKE_CLIENT_ID); + subscribe(connection, NEWS_TOPIC, AT_MOST_ONCE); + // publish on /news + final ByteBuf payload = Unpooled.copiedBuffer("Hello world!", Charset.defaultCharset()); + sut.receivedPublishQos0(new Topic(NEWS_TOPIC), TEST_USER, TEST_PWD, payload, false); + + ConnectionTestUtils.verifyPublishIsReceived(channel, AT_MOST_ONCE, "Hello world!"); + + unsubscribeAndVerifyAck(NEWS_TOPIC); + connection.processDisconnect(null); + + // connect on another channel + EmbeddedChannel anotherChannel = new EmbeddedChannel(); + MQTTConnection anotherConnection = createMQTTConnection(CONFIG, anotherChannel); + anotherConnection.processConnect(connectMessage); + ConnectionTestUtils.assertConnectAccepted(anotherChannel); + + // publish on /news + final ByteBuf payload2 = Unpooled.copiedBuffer("Hello world!", Charset.defaultCharset()); + sut.receivedPublishQos0(new Topic(NEWS_TOPIC), TEST_USER, TEST_PWD, payload2, false); + + ConnectionTestUtils.verifyNoPublishIsReceived(anotherChannel); + } + + @Test + public void avoidMultipleNotificationsAfterMultipleReconnection_cleanSessionFalseQoS1() { + final MqttConnectMessage notCleanConnect = ConnectionTestUtils.buildConnectNotClean(FAKE_CLIENT_ID); + connect(connection, notCleanConnect); + subscribe(connection, NEWS_TOPIC, AT_LEAST_ONCE); + connection.processDisconnect(null); + + // connect on another channel + final String firstPayload = "Hello MQTT 1"; + connectPublishDisconnectFromAnotherClient(firstPayload, NEWS_TOPIC); + + // reconnect FAKE_CLIENT on another channel + EmbeddedChannel anotherChannel2 = new EmbeddedChannel(); + MQTTConnection anotherConnection2 = createMQTTConnection(CONFIG, anotherChannel2); + anotherConnection2.processConnect(notCleanConnect); + ConnectionTestUtils.assertConnectAccepted(anotherChannel2); + + ConnectionTestUtils.verifyPublishIsReceived(anotherChannel2, MqttQoS.AT_LEAST_ONCE, firstPayload); + + anotherConnection2.processDisconnect(null); + + final String secondPayload = "Hello MQTT 2"; + connectPublishDisconnectFromAnotherClient(secondPayload, NEWS_TOPIC); + + EmbeddedChannel anotherChannel3 = new EmbeddedChannel(); + MQTTConnection anotherConnection3 = createMQTTConnection(CONFIG, anotherChannel3); + anotherConnection3.processConnect(notCleanConnect); + ConnectionTestUtils.assertConnectAccepted(anotherChannel3); + + ConnectionTestUtils.verifyPublishIsReceived(anotherChannel3, MqttQoS.AT_LEAST_ONCE, secondPayload); + } + + private void connectPublishDisconnectFromAnotherClient(String firstPayload, String topic) { + MQTTConnection anotherConnection = connectNotCleanAs(PUBLISHER_ID); + + // publish from another channel + final ByteBuf anyPayload = Unpooled.copiedBuffer(firstPayload, Charset.defaultCharset()); + sut.receivedPublishQos1(anotherConnection, new Topic(topic), TEST_USER, anyPayload, 1,false, + MqttMessageBuilders.publish() + .payload(Unpooled.copiedBuffer(firstPayload, Charset.defaultCharset())) + .qos(MqttQoS.AT_LEAST_ONCE) + .retained(false) + .topicName(topic).build()); + + // disconnect the other channel + anotherConnection.processDisconnect(null); + } + + private MQTTConnection connectNotCleanAs(String clientId) { + EmbeddedChannel channel = new EmbeddedChannel(); + MQTTConnection connection = createMQTTConnection(CONFIG, channel); + connection.processConnect(ConnectionTestUtils.buildConnectNotClean(clientId)); + ConnectionTestUtils.assertConnectAccepted(channel); + return connection; + } + + private MQTTConnection connectAs(String clientId) { + EmbeddedChannel channel = new EmbeddedChannel(); + MQTTConnection connection = createMQTTConnection(CONFIG, channel); + connection.processConnect(ConnectionTestUtils.buildConnect(clientId)); + ConnectionTestUtils.assertConnectAccepted(channel); + return connection; + } + + @Test + public void testConnectSubPub_cycle_getTimeout_on_second_disconnect_issue142() { + connect(connection, FAKE_CLIENT_ID); + subscribe(connection, NEWS_TOPIC, AT_MOST_ONCE); + // publish on /news + final ByteBuf payload = Unpooled.copiedBuffer("Hello world!", Charset.defaultCharset()); + sut.receivedPublishQos0(new Topic(NEWS_TOPIC), TEST_USER, TEST_PWD, payload, false); + + ConnectionTestUtils.verifyPublishIsReceived((EmbeddedChannel) connection.channel, AT_MOST_ONCE, "Hello world!"); + + connection.processDisconnect(null); + + final MqttConnectMessage notCleanConnect = ConnectionTestUtils.buildConnect(FAKE_CLIENT_ID); + EmbeddedChannel subscriberChannel = new EmbeddedChannel(); + MQTTConnection subscriberConnection = createMQTTConnection(CONFIG, subscriberChannel); + subscriberConnection.processConnect(notCleanConnect); + ConnectionTestUtils.assertConnectAccepted(subscriberChannel); + + subscribe(subscriberConnection, NEWS_TOPIC, AT_MOST_ONCE); + // publish on /news + final ByteBuf payload2 = Unpooled.copiedBuffer("Hello world2!", Charset.defaultCharset()); + sut.receivedPublishQos0(new Topic(NEWS_TOPIC), TEST_USER, TEST_PWD, payload2, false); + + ConnectionTestUtils.verifyPublishIsReceived(subscriberChannel, AT_MOST_ONCE, "Hello world2!"); + + subscriberConnection.processDisconnect(null); + + assertFalse("after a disconnect the client should be disconnected", subscriberChannel.isOpen()); + } + + @Test + public void checkReplayofStoredPublishResumeAfter_a_disconnect_cleanSessionFalseQoS1() { + final MQTTConnection publisher = connectAs("Publisher"); + + connect(this.connection, FAKE_CLIENT_ID); + subscribe(connection, NEWS_TOPIC, AT_LEAST_ONCE); + + // publish from another channel + publishQos1(publisher, NEWS_TOPIC, "Hello world MQTT!!-1", 99); + ConnectionTestUtils.verifyPublishIsReceived(channel, AT_LEAST_ONCE, "Hello world MQTT!!-1"); + connection.processDisconnect(null); + + publishQos1(publisher, NEWS_TOPIC, "Hello world MQTT!!-2", 100); + publishQos1(publisher, NEWS_TOPIC, "Hello world MQTT!!-3", 101); + + createMQTTConnection(CONFIG); + connect(this.connection, FAKE_CLIENT_ID); + ConnectionTestUtils.verifyPublishIsReceived(channel, AT_LEAST_ONCE, "Hello world MQTT!!-2"); + ConnectionTestUtils.verifyPublishIsReceived(channel, AT_LEAST_ONCE, "Hello world MQTT!!-3"); + } + + private void publishQos1(MQTTConnection publisher, String topic, String payload, int messageID) { + final ByteBuf bytePayload = Unpooled.copiedBuffer(payload, Charset.defaultCharset()); + sut.receivedPublishQos1(publisher, new Topic(topic), TEST_USER, bytePayload, messageID,false, + MqttMessageBuilders.publish() + .payload(Unpooled.copiedBuffer(payload, Charset.defaultCharset())) + .qos(MqttQoS.AT_LEAST_ONCE) + .retained(false) + .topicName(NEWS_TOPIC).build()); + } + + private void publishQos2(MQTTConnection connection, String topic, String payload) { + final ByteBuf bytePayload = Unpooled.copiedBuffer(payload, Charset.defaultCharset()); + sut.receivedPublishQos2(connection, MqttMessageBuilders.publish() + .payload(bytePayload) + .qos(MqttQoS.EXACTLY_ONCE) + .retained(true) + .topicName(NEWS_TOPIC).build()); + } + + /** + * subscriber connect and subscribe on "topic" subscriber disconnects publisher connects and + * send two message "hello1" "hello2" to "topic" subscriber connects again and receive "hello1" + * "hello2" + */ + @Test + public void checkQoS2SubscriberDisconnectReceivePersistedPublishes() { + connect(this.connection, FAKE_CLIENT_ID); + subscribe(connection, NEWS_TOPIC, EXACTLY_ONCE); + connection.processDisconnect(null); + + final MQTTConnection publisher = connectAs("Publisher"); + publishQos2(publisher, NEWS_TOPIC, "Hello world MQTT!!-1"); + publishQos2(publisher, NEWS_TOPIC, "Hello world MQTT!!-2"); + + createMQTTConnection(CONFIG); + connect(this.connection, FAKE_CLIENT_ID); + ConnectionTestUtils.verifyPublishIsReceived(channel, EXACTLY_ONCE, "Hello world MQTT!!-1"); + ConnectionTestUtils.verifyPublishIsReceived(channel, EXACTLY_ONCE, "Hello world MQTT!!-2"); + } + + /** + * subscriber connect and subscribe on "a/b" QoS 1 and "a/+" QoS 2 publisher connects and send a + * message "hello" on "a/b" subscriber must receive only a single message not twice + */ + @Test + public void checkSinglePublishOnOverlappingSubscriptions() { + final MQTTConnection publisher = connectAs("Publisher"); + + connect(this.connection, FAKE_CLIENT_ID); + subscribe(connection, "a/b", AT_LEAST_ONCE); + subscribe(connection, "a/+", EXACTLY_ONCE); + + // force the publisher to send + publishQos1(publisher, "a/b", "Hello world MQTT!!", 60); + + ConnectionTestUtils.verifyPublishIsReceived(channel, AT_LEAST_ONCE, "Hello world MQTT!!"); + ConnectionTestUtils.verifyNoPublishIsReceived(channel); + } +} diff --git a/broker/src/test/java/io/moquette/broker/SessionRegistryTest.java b/broker/src/test/java/io/moquette/broker/SessionRegistryTest.java new file mode 100644 index 00000000..6e499ece --- /dev/null +++ b/broker/src/test/java/io/moquette/broker/SessionRegistryTest.java @@ -0,0 +1,126 @@ +package io.moquette.broker; + +import io.moquette.persistence.MemoryStorageService; +import io.moquette.server.netty.NettyUtils; +import io.moquette.spi.ISessionsStore; +import io.moquette.spi.impl.MockAuthenticator; +import io.moquette.spi.impl.SessionsRepository; +import io.moquette.spi.impl.security.PermitAllAuthorizatorPolicy; +import io.moquette.spi.impl.subscriptions.CTrieSubscriptionDirectory; +import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory; +import io.moquette.spi.security.IAuthenticator; +import io.netty.channel.Channel; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.mqtt.MqttConnectMessage; +import io.netty.handler.codec.mqtt.MqttMessageBuilders; +import io.netty.handler.codec.mqtt.MqttVersion; +import org.junit.Before; +import org.junit.Test; + +import static io.moquette.spi.impl.NettyChannelAssertions.assertEqualsConnAck; +import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_ACCEPTED; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonMap; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class SessionRegistryTest { + + static final String FAKE_CLIENT_ID = "FAKE_123"; + static final String FAKE_CLIENT_ID2 = "FAKE_456"; + static final String PUBLISHER_ID = "Publisher"; + private static final String SUBSCRIBER_ID = "Subscriber"; + static final String NEWS_TOPIC = "/news"; + static final String BAD_FORMATTED_TOPIC = "#MQTTClient"; + + static final String TEST_USER = "fakeuser"; + static final String TEST_PWD = "fakepwd"; + static final String EVIL_TEST_USER = "eviluser"; + static final String EVIL_TEST_PWD = "unsecret"; + + private MQTTConnection connection; + private EmbeddedChannel channel; + private SessionRegistry sut; + private MqttMessageBuilders.ConnectBuilder connMsg; + private static final BrokerConfiguration ALLOW_ANONYMOUS_AND_ZEROBYTE_CLIENT_ID = + new BrokerConfiguration(true, true, false); + + @Before + public void setUp() { + connMsg = MqttMessageBuilders.connect().protocolVersion(MqttVersion.MQTT_3_1).cleanSession(true); + + createMQTTConnection(ALLOW_ANONYMOUS_AND_ZEROBYTE_CLIENT_ID); + } + + private void createMQTTConnection(BrokerConfiguration config) { + channel = new EmbeddedChannel(); + connection = createMQTTConnection(config, channel); + } + + private MQTTConnection createMQTTConnection(BrokerConfiguration config, Channel channel) { + MemoryStorageService memStorage = new MemoryStorageService(null, null); + ISessionsStore sessionStore = memStorage.sessionsStore(); + IAuthenticator mockAuthenticator = new MockAuthenticator(singleton(FAKE_CLIENT_ID), singletonMap(TEST_USER, TEST_PWD)); + + ISubscriptionsDirectory subscriptions = new CTrieSubscriptionDirectory(); + SessionsRepository sessionsRepository = new SessionsRepository(sessionStore, null); + subscriptions.init(sessionsRepository); + + sut = new SessionRegistry(subscriptions); + final PostOffice postOffice = new PostOffice(subscriptions, new PermitAllAuthorizatorPolicy(), + new MemoryRetainedRepository(), sut); + return new MQTTConnection(channel, config, mockAuthenticator, sut, postOffice); + } + + @Test + public void testConnAckContainsSessionPresentFlag() { + MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID) + .protocolVersion(MqttVersion.MQTT_3_1_1) + .build(); + NettyUtils.clientID(channel, FAKE_CLIENT_ID); + NettyUtils.cleanSession(channel, false); + + // Connect a first time + sut.bindToSession(connection, msg, FAKE_CLIENT_ID); + // disconnect + sut.disconnect(FAKE_CLIENT_ID); + + // Exercise, reconnect + EmbeddedChannel anotherChannel = new EmbeddedChannel(); + MQTTConnection anotherConnection = createMQTTConnection(ALLOW_ANONYMOUS_AND_ZEROBYTE_CLIENT_ID, anotherChannel); + sut.bindToSession(anotherConnection, msg, FAKE_CLIENT_ID); + + // Verify + assertEqualsConnAck(CONNECTION_ACCEPTED, anotherChannel.readOutbound()); + assertTrue("Connection is accepted and therefore should remain open", anotherChannel.isOpen()); + } + + @Test + public void connectWithCleanSessionUpdateClientSession() { + // first connect with clean session true + MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID).cleanSession(true).build(); + connection.processConnect(msg); + assertEqualsConnAck(CONNECTION_ACCEPTED, channel.readOutbound()); + connection.processDisconnect(null); + assertFalse(channel.isOpen()); + + // second connect with clean session false + EmbeddedChannel anotherChannel = new EmbeddedChannel(); + MQTTConnection anotherConnection = createMQTTConnection(ALLOW_ANONYMOUS_AND_ZEROBYTE_CLIENT_ID, + anotherChannel); + MqttConnectMessage secondConnMsg = MqttMessageBuilders.connect() + .clientId(FAKE_CLIENT_ID) + .protocolVersion(MqttVersion.MQTT_3_1) + .build(); + + anotherConnection.processConnect(secondConnMsg); + assertEqualsConnAck(CONNECTION_ACCEPTED, anotherChannel.readOutbound()); + + // Verify client session is clean false + Session session = sut.retrieve(FAKE_CLIENT_ID); + assertFalse(session.isClean()); + } +} diff --git a/broker/src/test/java/io/moquette/server/ConfigurationClassLoaderTest.java b/broker/src/test/java/io/moquette/server/ConfigurationClassLoaderTest.java index 54ce22e5..b37a1f95 100644 --- a/broker/src/test/java/io/moquette/server/ConfigurationClassLoaderTest.java +++ b/broker/src/test/java/io/moquette/server/ConfigurationClassLoaderTest.java @@ -24,14 +24,14 @@ import io.moquette.persistence.MemoryMessagesStore; import io.moquette.persistence.MemoryStorageService; import io.moquette.persistence.MemorySessionStore; import io.moquette.spi.security.IAuthenticator; -import io.moquette.spi.security.IAuthorizator; +import io.moquette.spi.security.IAuthorizatorPolicy; import org.junit.After; import org.junit.Test; import java.io.IOException; import java.util.Properties; import static org.junit.Assert.*; -public class ConfigurationClassLoaderTest implements IAuthenticator, IAuthorizator { +public class ConfigurationClassLoaderTest implements IAuthenticator, IAuthorizatorPolicy { Server m_server; IConfig m_config; diff --git a/broker/src/test/java/io/moquette/server/ServerIntegrationPahoCanPublishOnReadBlockedTopicTest.java b/broker/src/test/java/io/moquette/server/ServerIntegrationPahoCanPublishOnReadBlockedTopicTest.java index 02182780..926e94a0 100644 --- a/broker/src/test/java/io/moquette/server/ServerIntegrationPahoCanPublishOnReadBlockedTopicTest.java +++ b/broker/src/test/java/io/moquette/server/ServerIntegrationPahoCanPublishOnReadBlockedTopicTest.java @@ -21,7 +21,7 @@ import io.moquette.server.config.IConfig; import io.moquette.server.config.MemoryConfig; import io.moquette.spi.impl.security.AcceptAllAuthenticator; import io.moquette.spi.impl.subscriptions.Topic; -import io.moquette.spi.security.IAuthorizator; +import io.moquette.spi.security.IAuthorizatorPolicy; import io.netty.buffer.Unpooled; import io.netty.handler.codec.mqtt.MqttMessageBuilders; import io.netty.handler.codec.mqtt.MqttPublishMessage; @@ -71,7 +71,7 @@ public class ServerIntegrationPahoCanPublishOnReadBlockedTopicTest { m_config = new MemoryConfig(configProps); canRead = true; - final IAuthorizator switchingAuthorizator = new IAuthorizator() { + final IAuthorizatorPolicy switchingAuthorizator = new IAuthorizatorPolicy() { // int callCount = 0; @Override public boolean canWrite(Topic topic, String user, String client) { diff --git a/broker/src/test/java/io/moquette/spi/impl/AbstractProtocolProcessorCommonUtils.java b/broker/src/test/java/io/moquette/spi/impl/AbstractProtocolProcessorCommonUtils.java index d75e8acc..d657113e 100644 --- a/broker/src/test/java/io/moquette/spi/impl/AbstractProtocolProcessorCommonUtils.java +++ b/broker/src/test/java/io/moquette/spi/impl/AbstractProtocolProcessorCommonUtils.java @@ -15,14 +15,12 @@ */ package io.moquette.spi.impl; -import io.moquette.interception.InterceptHandler; import io.moquette.persistence.MemoryStorageService; import io.moquette.server.netty.NettyUtils; import io.moquette.spi.IMessagesStore; import io.moquette.spi.ISessionsStore; -import io.moquette.spi.impl.security.PermitAllAuthorizator; +import io.moquette.spi.impl.security.PermitAllAuthorizatorPolicy; import io.moquette.spi.impl.subscriptions.*; -import io.moquette.spi.security.IAuthorizator; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.mqtt.*; @@ -81,7 +79,7 @@ abstract class AbstractProtocolProcessorCommonUtils { subscriptions.init(sessionsRepository); m_processor = new ProtocolProcessor(); m_processor.init(subscriptions, m_messagesStore, m_sessionStore, m_mockAuthenticator, true, - new PermitAllAuthorizator(), NO_OBSERVERS_INTERCEPTOR, sessionsRepository, false); + new PermitAllAuthorizatorPolicy(), NO_OBSERVERS_INTERCEPTOR, sessionsRepository, false); } void verifyNoPublishIsReceived() { @@ -126,14 +124,14 @@ abstract class AbstractProtocolProcessorCommonUtils { assertEquals(expectedSubscription, subscription); } - protected MqttSubAckMessage subscribeWithoutVerify(String topic, MqttQoS desiredQos) { - MqttSubscribeMessage subscribe = MqttMessageBuilders.subscribe() - .addSubscription(desiredQos, topic) - .messageId(1) - .build(); - this.m_processor.processSubscribe(m_channel, subscribe); - return m_channel.readOutbound(); - } +// protected MqttSubAckMessage subscribeWithoutVerify(String topic, MqttQoS desiredQos) { +// MqttSubscribeMessage subscribe = MqttMessageBuilders.subscribe() +// .addSubscription(desiredQos, topic) +// .messageId(1) +// .build(); +// this.m_processor.processSubscribe(m_channel, subscribe); +// return m_channel.readOutbound(); +// } protected void subscribeAndNotReadResponse(String topic, MqttQoS desiredQos) { MqttSubscribeMessage subscribe = MqttMessageBuilders.subscribe() @@ -143,16 +141,6 @@ abstract class AbstractProtocolProcessorCommonUtils { this.m_processor.processSubscribe(m_channel, subscribe); } - protected void unsubscribe(String topic) { - final int messageId = 1; - MqttUnsubscribeMessage msg = MqttMessageBuilders.unsubscribe() - .addTopicFilter(topic) - .messageId(messageId) - .build(); - - m_processor.processUnsubscribe(m_channel, msg); - } - protected void unsubscribeAndVerifyAck(String topic) { final int messageId = 1; MqttUnsubscribeMessage msg = MqttMessageBuilders.unsubscribe() diff --git a/broker/src/test/java/io/moquette/spi/impl/ConnectHandlerTest.java b/broker/src/test/java/io/moquette/spi/impl/ConnectHandlerTest.java deleted file mode 100644 index eac4b2ff..00000000 --- a/broker/src/test/java/io/moquette/spi/impl/ConnectHandlerTest.java +++ /dev/null @@ -1,315 +0,0 @@ -package io.moquette.spi.impl; - -import io.moquette.persistence.MemoryStorageService; -import io.moquette.server.ConnectionDescriptorStore; -import io.moquette.server.netty.NettyUtils; -import io.moquette.spi.ClientSession; -import io.moquette.spi.IMessagesStore; -import io.moquette.spi.ISessionsStore; -import io.moquette.spi.impl.security.PermitAllAuthorizator; -import io.moquette.spi.impl.subscriptions.CTrieSubscriptionDirectory; -import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory; -import io.netty.channel.embedded.EmbeddedChannel; -import io.netty.handler.codec.mqtt.MqttConnectMessage; -import io.netty.handler.codec.mqtt.MqttMessageBuilders; -import io.netty.handler.codec.mqtt.MqttVersion; -import org.junit.Before; -import org.junit.Test; - -import static io.moquette.spi.impl.NettyChannelAssertions.assertEqualsConnAck; -import static io.moquette.spi.impl.ProtocolProcessorTest.*; -import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.*; -import static java.util.Collections.singleton; -import static java.util.Collections.singletonMap; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class ConnectHandlerTest { - - EmbeddedChannel session; - MqttMessageBuilders.ConnectBuilder connMsg; - - IMessagesStore m_messagesStore; - ISessionsStore m_sessionStore; - ISubscriptionsDirectory subscriptions; - MockAuthenticator m_mockAuthenticator; - private ConnectHandler sut; - private DisconnectHandler disconnectHandler; - private SessionsRepository sessionsRepository; - - @Before - public void setUp() { - connMsg = MqttMessageBuilders.connect().protocolVersion(MqttVersion.MQTT_3_1).cleanSession(true); - - session = new EmbeddedChannel(); - - MemoryStorageService memStorage = new MemoryStorageService(null, null); - m_messagesStore = memStorage.messagesStore(); - m_sessionStore = memStorage.sessionsStore(); - m_mockAuthenticator = new MockAuthenticator(singleton(FAKE_CLIENT_ID), singletonMap(TEST_USER, TEST_PWD)); - - subscriptions = new CTrieSubscriptionDirectory(); - sessionsRepository = new SessionsRepository(m_sessionStore, null); - subscriptions.init(sessionsRepository); - - final ConnectionDescriptorStore connectionsRegistry = new ConnectionDescriptorStore(); - sut = new ConnectHandler(connectionsRegistry, NO_OBSERVERS_INTERCEPTOR, sessionsRepository, m_sessionStore, m_mockAuthenticator, - new PermitAllAuthorizator(), subscriptions, true, true, false, null); - - disconnectHandler = new DisconnectHandler(connectionsRegistry, sessionsRepository, - subscriptions, NO_OBSERVERS_INTERCEPTOR); - } - - @Test - public void testZeroByteClientIdWithCleanSession() { - // Allow zero byte client ids - sut = new ConnectHandler(new ConnectionDescriptorStore(), NO_OBSERVERS_INTERCEPTOR, new SessionsRepository(this.m_sessionStore, null), m_sessionStore, m_mockAuthenticator, - new PermitAllAuthorizator(), subscriptions, true, true, false, null); - - // Connect message with clean session set to true and client id is null. - MqttConnectMessage msg = connMsg.clientId(null) - .protocolVersion(MqttVersion.MQTT_3_1_1) - .cleanSession(true) - .build(); - - sut.processConnect(session, msg); - assertEqualsConnAck("Connection MUST be accepted, unique clientid MUST be generated and clean session to true", - CONNECTION_ACCEPTED, session.readOutbound()); - assertTrue("Connection should be valid and open,", session.isOpen()); - } - - @Test - public void invalidAuthentication() { - MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID) - .username(TEST_USER + "_fake") - .password(TEST_PWD) - .build(); - - // Exercise - sut.processConnect(session, msg); - - // Verify - assertEqualsConnAck(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD, session.readOutbound()); - assertFalse("Connection should be closed by the broker.", session.isOpen()); - } - - @Test - public void testConnect_badClientID() { - connMsg.clientId("extremely_long_clientID_greater_than_23").build(); - - // Exercise - sut.processConnect(session, connMsg.clientId("extremely_long_clientID_greater_than_23").build()); - - // Verify - assertEqualsConnAck(CONNECTION_ACCEPTED, session.readOutbound()); - } - - @Test - public void testWill() { - MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID).willFlag(true) - .willTopic("topic").willMessage("Topic message").build(); - - // Exercise - // m_handler.setMessaging(mockedMessaging); - sut.processConnect(session, msg); - - // Verify - assertEqualsConnAck(CONNECTION_ACCEPTED, session.readOutbound()); - assertTrue("Connection is accepted and therefore should remain open.", session.isOpen()); - // TODO verify the call - /* - * verify(mockedMessaging).publish(eq("topic"), eq("Topic message".getBytes()), - * any(AbstractMessage.QOSType.class), anyBoolean(), eq("123"), any(IoSession.class)); - */ - } - - @Test - public void connectWithSameClientIDBadCredentialsDoesntDropExistingClient() { - // Connect a client1 - MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID) - .username(TEST_USER) - .password(TEST_PWD) - .build(); - sut.processConnect(session, msg); - assertEqualsConnAck(CONNECTION_ACCEPTED, session.readOutbound()); - - // create another connect same clientID but with bad credentials - MqttConnectMessage evilClientConnMsg = MqttMessageBuilders.connect() - .protocolVersion(MqttVersion.MQTT_3_1) - .clientId(FAKE_CLIENT_ID) - .username(ProtocolProcessorTest.EVIL_TEST_USER) - .password(ProtocolProcessorTest.EVIL_TEST_PWD) - .build(); - - EmbeddedChannel evilSession = new EmbeddedChannel(); - - // Exercise - sut.processConnect(evilSession, evilClientConnMsg); - - // Verify - // the evil client gets a not auth notification - assertEqualsConnAck(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD, evilSession.readOutbound()); - // the good client remains connected - assertTrue(session.isOpen()); - assertFalse(evilSession.isOpen()); - } - - @Test - public void acceptAnonymousClient() { - MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID).build(); - - // Exercise - sut.processConnect(session, msg); - - // Verify - assertEqualsConnAck(CONNECTION_ACCEPTED, session.readOutbound()); - assertTrue("Connection is accepted and therefore should remain open.", session.isOpen()); - } - - @Test - public void validAuthentication() { - MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID) - .username(TEST_USER).password(TEST_PWD).build(); - - // Exercise - sut.processConnect(session, msg); - - // Verify - assertEqualsConnAck(CONNECTION_ACCEPTED, session.readOutbound()); - assertTrue("Connection is accepted and therefore should remain open.", session.isOpen()); - } - - @Test - public void noPasswdAuthentication() { - MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID) - .username(TEST_USER) - .build(); - - // Exercise - sut.processConnect(session, msg); - - // Verify - assertEqualsConnAck(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD, session.readOutbound()); - assertFalse("Connection should be closed by the broker.", session.isOpen()); - } - - @Test - public void prohibitAnonymousClient() { - MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID).build(); - reinitProcessorProhibitingAnonymousClients(); - - // Exercise - sut.processConnect(session, msg); - - // Verify - assertEqualsConnAck(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD, session.readOutbound()); - assertFalse("Connection should be closed by the broker.", session.isOpen()); - } - - protected void reinitProcessorProhibitingAnonymousClients() { - sut = new ConnectHandler(new ConnectionDescriptorStore(), NO_OBSERVERS_INTERCEPTOR, new SessionsRepository(this.m_sessionStore, null), m_sessionStore, m_mockAuthenticator, - new PermitAllAuthorizator(), subscriptions, false, true, false, null); - } - - @Test - public void prohibitAnonymousClient_providingUsername() { - MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID) - .username(TEST_USER + "_fake") - .build(); - reinitProcessorProhibitingAnonymousClients(); - - // Exercise - sut.processConnect(session, msg); - - // Verify - assertEqualsConnAck(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD, session.readOutbound()); - assertFalse("Connection should be closed by the broker.", session.isOpen()); - } - - @Test - public void testZeroByteClientIdNotAllowed() { - sut = new ConnectHandler(new ConnectionDescriptorStore(), NO_OBSERVERS_INTERCEPTOR, new SessionsRepository(this.m_sessionStore, null), m_sessionStore, m_mockAuthenticator, - new PermitAllAuthorizator(), subscriptions, false, false, false, null); - - // Connect message with clean session set to true and client id is null. - MqttConnectMessage msg = connMsg.clientId(null) - .protocolVersion(MqttVersion.MQTT_3_1_1) - .cleanSession(true) - .build(); - - sut.processConnect(session, msg); - assertEqualsConnAck("Zero byte client identifiers are not allowed", - CONNECTION_REFUSED_IDENTIFIER_REJECTED, session.readOutbound()); - - assertFalse("Connection should closed.", session.isOpen()); - } - - @Test - public void testZeroByteClientIdWithoutCleanSession() { - // Allow zero byte client ids - reinitProtocolProcessorWithZeroLengthClientIdAndAnonymousClients(); - - // Connect message without clean session set to true but client id is still null - MqttConnectMessage msg = MqttMessageBuilders.connect().clientId(null).protocolVersion(MqttVersion.MQTT_3_1_1) - .build(); - - sut.processConnect(session, msg); - assertEqualsConnAck( - "Identifier should be rejected due to having clean session set to false.", - CONNECTION_REFUSED_IDENTIFIER_REJECTED, - session.readOutbound()); - - assertFalse("Connection should be closed by the broker.", session.isOpen()); - } - - protected void reinitProtocolProcessorWithZeroLengthClientIdAndAnonymousClients() { - sut = new ConnectHandler(new ConnectionDescriptorStore(), NO_OBSERVERS_INTERCEPTOR, new SessionsRepository(this.m_sessionStore, null), m_sessionStore, m_mockAuthenticator, - new PermitAllAuthorizator(), subscriptions, true, true, false, null); - } - - @Test - public void testConnAckContainsSessionPresentFlag() { - MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID) - .protocolVersion(MqttVersion.MQTT_3_1_1).build(); - NettyUtils.clientID(session, FAKE_CLIENT_ID); - NettyUtils.cleanSession(session, false); - - // Connect a first time - sut.processConnect(session, msg); - // disconnect - disconnectHandler.processDisconnect(session, FAKE_CLIENT_ID); - - // Exercise, reconnect - EmbeddedChannel firstReceiverSession = new EmbeddedChannel(); - sut.processConnect(firstReceiverSession, msg); - - // Verify - assertEqualsConnAck(CONNECTION_ACCEPTED, firstReceiverSession.readOutbound()); - assertTrue("Connection is accepted and therefore should remain open.", firstReceiverSession.isOpen()); - } - - @Test - public void connectWithCleanSessionUpdateClientSession() { - // first connect with clean session true - MqttConnectMessage msg = connMsg.clientId(FAKE_CLIENT_ID).cleanSession(true).build(); - sut.processConnect(session, msg); - assertEqualsConnAck(CONNECTION_ACCEPTED, session.readOutbound()); - disconnectHandler.processDisconnect(session, FAKE_CLIENT_ID); - assertFalse(session.isOpen()); - - // second connect with clean session false - session = new EmbeddedChannel(); - MqttConnectMessage secondConnMsg = MqttMessageBuilders.connect().clientId(FAKE_CLIENT_ID) - .protocolVersion(MqttVersion.MQTT_3_1).build(); - - sut.processConnect(session, secondConnMsg); - assertEqualsConnAck(CONNECTION_ACCEPTED, session.readOutbound()); - - // Verify client session is clean false - ClientSession session = sessionsRepository.sessionForClient(FAKE_CLIENT_ID); - assertFalse(session.isCleanSession()); - - // Verify - // assertEqualsConnAck(CONNECTION_ACCEPTED, session.readOutbound()); - } -} diff --git a/broker/src/test/java/io/moquette/spi/impl/MockAuthenticator.java b/broker/src/test/java/io/moquette/spi/impl/MockAuthenticator.java index ba2dd63a..043c4d02 100644 --- a/broker/src/test/java/io/moquette/spi/impl/MockAuthenticator.java +++ b/broker/src/test/java/io/moquette/spi/impl/MockAuthenticator.java @@ -25,12 +25,12 @@ import static java.nio.charset.StandardCharsets.UTF_8; /** * Test utility to implements authenticator instance. */ -class MockAuthenticator implements IAuthenticator { +public class MockAuthenticator implements IAuthenticator { private Set m_clientIds; private Map m_userPwds; - MockAuthenticator(Set clientIds, Map userPwds) { + public MockAuthenticator(Set clientIds, Map userPwds) { m_clientIds = clientIds; m_userPwds = userPwds; } diff --git a/broker/src/test/java/io/moquette/spi/impl/NettyChannelAssertions.java b/broker/src/test/java/io/moquette/spi/impl/NettyChannelAssertions.java index 9151d994..74932826 100644 --- a/broker/src/test/java/io/moquette/spi/impl/NettyChannelAssertions.java +++ b/broker/src/test/java/io/moquette/spi/impl/NettyChannelAssertions.java @@ -27,13 +27,13 @@ import static org.junit.Assert.assertTrue; /** * Some useful assertions used by Netty's EmbeddedChannel in tests. */ -final class NettyChannelAssertions { +public final class NettyChannelAssertions { - static void assertEqualsConnAck(MqttConnectReturnCode expectedCode, Object connAck) { + public static void assertEqualsConnAck(MqttConnectReturnCode expectedCode, Object connAck) { assertEqualsConnAck(null, expectedCode, connAck); } - static void assertEqualsConnAck(String msg, MqttConnectReturnCode expectedCode, Object connAck) { + public static void assertEqualsConnAck(String msg, MqttConnectReturnCode expectedCode, Object connAck) { assertTrue("connAck is not an instance of ConnAckMessage", connAck instanceof MqttConnAckMessage); MqttConnAckMessage connAckMsg = (MqttConnAckMessage) connAck; @@ -43,12 +43,12 @@ final class NettyChannelAssertions { assertEquals(msg, expectedCode, connAckMsg.variableHeader().connectReturnCode()); } - static void assertConnAckAccepted(EmbeddedChannel channel) { + public static void assertConnAckAccepted(EmbeddedChannel channel) { channel.flush(); assertEqualsConnAck(CONNECTION_ACCEPTED, channel.readOutbound()); } - static void assertEqualsSubAck(/* byte expectedCode, */ Object subAck) { + public static void assertEqualsSubAck(/* byte expectedCode, */ Object subAck) { assertTrue(subAck instanceof MqttSubAckMessage); // SubAckMessage connAckMsg = (SubAckMessage) connAck; // assertEquals(expectedCode, connAckMsg.getReturnCode()); diff --git a/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessorTest.java b/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessorTest.java index b1c0044c..5454fcc0 100644 --- a/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessorTest.java +++ b/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessorTest.java @@ -16,17 +16,18 @@ package io.moquette.spi.impl; +import io.moquette.broker.PostOfficePublishTest; import io.moquette.interception.InterceptHandler; import io.moquette.persistence.MemoryStorageService; import io.moquette.server.netty.NettyUtils; import io.moquette.spi.IMessagesStore; import io.moquette.spi.IMessagesStore.StoredMessage; -import io.moquette.spi.impl.security.PermitAllAuthorizator; +import io.moquette.spi.impl.security.PermitAllAuthorizatorPolicy; 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.impl.subscriptions.Topic; -import io.moquette.spi.security.IAuthorizator; +import io.moquette.spi.security.IAuthorizatorPolicy; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.mqtt.*; @@ -69,201 +70,138 @@ public class ProtocolProcessorTest extends AbstractProtocolProcessorCommonUtils initializeProcessorAndSubsystems(); } - @Test - public void testPublishToItself() { - final Subscription subscription = new Subscription(FAKE_CLIENT_ID, new Topic(NEWS_TOPIC), AT_MOST_ONCE); + //same test moved into PostOfficePublishTest +// @Test +// public void testPublishToItself() { +// final Subscription subscription = new Subscription(FAKE_CLIENT_ID, new Topic(NEWS_TOPIC), AT_MOST_ONCE); +// +// // subscriptions.matches(topic) redefine the method to return true +// ISubscriptionsDirectory subs = new CTrieSubscriptionDirectory() { +// +// @Override +// public List matches(Topic topic) { +// if (topic.toString().equals(NEWS_TOPIC)) { +// return Collections.singletonList(subscription); +// } else { +// throw new IllegalArgumentException("Expected " + NEWS_TOPIC + " buf found " + topic); +// } +// } +// }; +// +// // simulate a connect that register a clientID to an IoSession +// MemoryStorageService storageService = new MemoryStorageService(null, null); +// SessionsRepository sessionsRepository = new SessionsRepository(storageService.sessionsStore(), null); +// subs.init(sessionsRepository); +// m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizatorPolicy(), +// NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false); +// +// connect_v3_1(); +// +// // Exercise +// publishToAs("FakeCLI", NEWS_TOPIC, AT_MOST_ONCE, false); +// +// // Verify +// verifyPublishIsReceived(); +// } - // subscriptions.matches(topic) redefine the method to return true - ISubscriptionsDirectory subs = new CTrieSubscriptionDirectory() { - - @Override - public List matches(Topic topic) { - if (topic.toString().equals(NEWS_TOPIC)) { - return Collections.singletonList(subscription); - } else { - throw new IllegalArgumentException("Expected " + NEWS_TOPIC + " buf found " + topic); - } - } - }; - - // simulate a connect that register a clientID to an IoSession - MemoryStorageService storageService = new MemoryStorageService(null, null); - SessionsRepository sessionsRepository = new SessionsRepository(storageService.sessionsStore(), null); - subs.init(sessionsRepository); - m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizator(), - NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false); - - connect_v3_1(); - - // Exercise - publishToAs("FakeCLI", NEWS_TOPIC, AT_MOST_ONCE, false); - - // Verify - verifyPublishIsReceived(); - } - - @Test - public void testPublishToMultipleSubscribers() { - final Subscription subscription = new Subscription(FAKE_CLIENT_ID, new Topic(NEWS_TOPIC), AT_MOST_ONCE); - final Subscription subscriptionClient2 = new Subscription( - FAKE_CLIENT_ID2, - new Topic(NEWS_TOPIC), - AT_MOST_ONCE); - - // subscriptions.matches(topic) redefine the method to return true - ISubscriptionsDirectory subs = new CTrieSubscriptionDirectory() { - - @Override - public List matches(Topic topic) { - if (topic.toString().equals(NEWS_TOPIC)) { - return Arrays.asList(subscription, subscriptionClient2); - } else { - throw new IllegalArgumentException("Expected " + NEWS_TOPIC + " buf found " + topic); - } - } - }; - - // simulate a connect that register a clientID to an IoSession - MemoryStorageService storageService = new MemoryStorageService(null, null); - SessionsRepository sessionsRepository = new SessionsRepository(storageService.sessionsStore(), null); - subs.init(sessionsRepository); - m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizator(), - NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false); - - EmbeddedChannel firstReceiverChannel = new EmbeddedChannel(); - MqttConnectMessage connectMessage = MqttMessageBuilders.connect().protocolVersion(MqttVersion.MQTT_3_1) - .clientId(FAKE_CLIENT_ID).cleanSession(true).build(); - m_processor.processConnect(firstReceiverChannel, connectMessage); - assertConnAckAccepted(firstReceiverChannel); - - // connect the second fake subscriber - EmbeddedChannel secondReceiverChannel = new EmbeddedChannel(); - MqttConnectMessage connectMessage2 = MqttMessageBuilders.connect().protocolVersion(MqttVersion.MQTT_3_1) - .clientId(FAKE_CLIENT_ID2).cleanSession(true).build(); - m_processor.processConnect(secondReceiverChannel, connectMessage2); - assertConnAckAccepted(secondReceiverChannel); - - // Exercise - MqttPublishMessage msg = MqttMessageBuilders.publish().topicName(NEWS_TOPIC).qos(AT_MOST_ONCE) - .retained(false).payload(Unpooled.copiedBuffer("Hello".getBytes(UTF_8))).build(); - NettyUtils.userName(m_channel, "FakeCLI"); - m_processor.processPublish(m_channel, msg); - - // Verify - firstReceiverChannel.flush(); - MqttPublishMessage pub2FirstSubscriber = firstReceiverChannel.readOutbound(); - assertNotNull(pub2FirstSubscriber); - String firstMessageContent = DebugUtils.payload2Str(pub2FirstSubscriber.payload()); - assertEquals("Hello", firstMessageContent); - - secondReceiverChannel.flush(); - MqttPublishMessage pub2SecondSubscriber = secondReceiverChannel.readOutbound(); - assertNotNull(pub2SecondSubscriber); - String secondMessageContent = DebugUtils.payload2Str(pub2SecondSubscriber.payload()); - assertEquals("Hello", secondMessageContent); - } - - @Test - public void testSubscribe() { - connect(); - - // Exercise & verify - subscribe(NEWS_TOPIC, AT_MOST_ONCE); - } - - @Test - public void testSubscribedToNotAuthorizedTopic() { - final String fakeUserName = "UnAuthUser"; - NettyUtils.userName(m_channel, fakeUserName); - - IAuthorizator mockAuthorizator = mock(IAuthorizator.class); - when(mockAuthorizator.canRead(eq(new Topic(NEWS_TOPIC)), eq(fakeUserName), eq(FAKE_CLIENT_ID))) - .thenReturn(false); - - m_processor.init(subscriptions, m_messagesStore, m_sessionStore, m_mockAuthenticator, true, mockAuthorizator, - NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false); - - connect(); - - //Exercise - MqttSubAckMessage subAckMsg = subscribeWithoutVerify(NEWS_TOPIC, AT_MOST_ONCE); - - verifyFailureQos(subAckMsg); - } - - private void verifyFailureQos(MqttSubAckMessage subAckMsg) { - List grantedQoSes = subAckMsg.payload().grantedQoSLevels(); - assertEquals(1, grantedQoSes.size()); - assertTrue(grantedQoSes.contains(MqttQoS.FAILURE.value())); - } - - @Test - public void testDoubleSubscribe() { - connect(); - assertEquals(0, subscriptions.size()); - subscribe(NEWS_TOPIC, AT_MOST_ONCE); - assertEquals(1, subscriptions.size()); - - //Exercise & verify - subscribe(NEWS_TOPIC, AT_MOST_ONCE); - } - - @Test - public void testSubscribeWithBadFormattedTopic() { - connect(); - assertEquals(0, subscriptions.size()); - - //Exercise - MqttSubAckMessage subAckMsg = subscribeWithoutVerify(BAD_FORMATTED_TOPIC, AT_MOST_ONCE); - - assertEquals(0, subscriptions.size()); - verifyFailureQos(subAckMsg); - } - - @Test - public void testUnsubscribeWithBadFormattedTopic() { - // Exercise - unsubscribe(BAD_FORMATTED_TOPIC); - - // Verify - assertFalse("If client unsubscribe with bad topic than channel must be closed, (issue 68)", m_channel.isOpen()); - } - - @Test - public void testPublishOfRetainedMessage_afterNewSubscription() throws Exception { - // simulate a connect that register a clientID to an IoSession - final Subscription subscription = - new Subscription(PUBLISHER_ID, new Topic(NEWS_TOPIC), AT_MOST_ONCE); - - // subscriptions.matches(topic) redefine the method to return true - ISubscriptionsDirectory subs = new CTrieSubscriptionDirectory() { - - @Override - public List matches(Topic topic) { - if (topic.toString().equals(NEWS_TOPIC)) { - return Collections.singletonList(subscription); - } else { - throw new IllegalArgumentException("Expected " + NEWS_TOPIC + " buf found " + topic); - } - } - }; - MemoryStorageService storageService = new MemoryStorageService(null, null); - SessionsRepository sessionsRepository = new SessionsRepository(storageService.sessionsStore(), null); - subs.init(sessionsRepository); - - // simulate a connect that register a clientID to an IoSession - m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizator(), - NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false); - connect_v3_1_asClient(PUBLISHER_ID); - publishToAs(PUBLISHER_ID, NEWS_TOPIC, AT_MOST_ONCE, true); - NettyUtils.cleanSession(m_channel, false); - - // Exercise - subscribeAndNotReadResponse("#", AT_MOST_ONCE); - - // Verify - verifyPublishIsReceived(); - } + //same test moved into PostOfficePublishTest +// @Test +// public void testPublishToMultipleSubscribers() { +// final Subscription subscription = new Subscription(FAKE_CLIENT_ID, new Topic(NEWS_TOPIC), AT_MOST_ONCE); +// final Subscription subscriptionClient2 = new Subscription( +// FAKE_CLIENT_ID2, +// new Topic(NEWS_TOPIC), +// AT_MOST_ONCE); +// +// // subscriptions.matches(topic) redefine the method to return true +// ISubscriptionsDirectory subs = new CTrieSubscriptionDirectory() { +// +// @Override +// public List matches(Topic topic) { +// if (topic.toString().equals(NEWS_TOPIC)) { +// return Arrays.asList(subscription, subscriptionClient2); +// } else { +// throw new IllegalArgumentException("Expected " + NEWS_TOPIC + " buf found " + topic); +// } +// } +// }; +// +// // simulate a connect that register a clientID to an IoSession +// MemoryStorageService storageService = new MemoryStorageService(null, null); +// SessionsRepository sessionsRepository = new SessionsRepository(storageService.sessionsStore(), null); +// subs.init(sessionsRepository); +// m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizatorPolicy(), +// NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false); +// +// EmbeddedChannel firstReceiverChannel = new EmbeddedChannel(); +// MqttConnectMessage connectMessage = MqttMessageBuilders.connect().protocolVersion(MqttVersion.MQTT_3_1) +// .clientId(FAKE_CLIENT_ID).cleanSession(true).build(); +// m_processor.processConnect(firstReceiverChannel, connectMessage); +// assertConnAckAccepted(firstReceiverChannel); +// +// // connect the second fake subscriber +// EmbeddedChannel secondReceiverChannel = new EmbeddedChannel(); +// MqttConnectMessage connectMessage2 = MqttMessageBuilders.connect().protocolVersion(MqttVersion.MQTT_3_1) +// .clientId(FAKE_CLIENT_ID2).cleanSession(true).build(); +// m_processor.processConnect(secondReceiverChannel, connectMessage2); +// assertConnAckAccepted(secondReceiverChannel); +// +// // Exercise +// MqttPublishMessage msg = MqttMessageBuilders.publish().topicName(NEWS_TOPIC).qos(AT_MOST_ONCE) +// .retained(false).payload(Unpooled.copiedBuffer("Hello".getBytes(UTF_8))).build(); +// NettyUtils.userName(m_channel, "FakeCLI"); +// m_processor.processPublish(m_channel, msg); +// +// // Verify +// firstReceiverChannel.flush(); +// MqttPublishMessage pub2FirstSubscriber = firstReceiverChannel.readOutbound(); +// assertNotNull(pub2FirstSubscriber); +// String firstMessageContent = DebugUtils.payload2Str(pub2FirstSubscriber.payload()); +// assertEquals("Hello", firstMessageContent); +// +// secondReceiverChannel.flush(); +// MqttPublishMessage pub2SecondSubscriber = secondReceiverChannel.readOutbound(); +// assertNotNull(pub2SecondSubscriber); +// String secondMessageContent = DebugUtils.payload2Str(pub2SecondSubscriber.payload()); +// assertEquals("Hello", secondMessageContent); +// } +// +// //TODO move this test on PostOfficeSubscribeTest +// @Test +// public void testPublishOfRetainedMessage_afterNewSubscription() throws Exception { +// // simulate a connect that register a clientID to an IoSession +// final Subscription subscription = +// new Subscription(PUBLISHER_ID, new Topic(NEWS_TOPIC), AT_MOST_ONCE); +// +// // subscriptions.matches(topic) redefine the method to return true +// ISubscriptionsDirectory subs = new CTrieSubscriptionDirectory() { +// +// @Override +// public List matches(Topic topic) { +// if (topic.toString().equals(NEWS_TOPIC)) { +// return Collections.singletonList(subscription); +// } else { +// throw new IllegalArgumentException("Expected " + NEWS_TOPIC + " buf found " + topic); +// } +// } +// }; +// MemoryStorageService storageService = new MemoryStorageService(null, null); +// SessionsRepository sessionsRepository = new SessionsRepository(storageService.sessionsStore(), null); +// subs.init(sessionsRepository); +// +// // simulate a connect that register a clientID to an IoSession +// m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizatorPolicy(), +// NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false); +// connect_v3_1_asClient(PUBLISHER_ID); +// publishToAs(PUBLISHER_ID, NEWS_TOPIC, AT_MOST_ONCE, true); +// NettyUtils.cleanSession(m_channel, false); +// +// // Exercise +// subscribeAndNotReadResponse("#", AT_MOST_ONCE); +// +// // Verify +// verifyPublishIsReceived(); +// } @Test public void testRepublishAndConsumePersistedMessages_onReconnect() { @@ -276,7 +214,7 @@ public class ProtocolProcessorTest extends AbstractProtocolProcessorCommonUtils retainedMessage.setClientID(PUBLISHER_ID); m_messagesStore.storeRetained(new Topic("/topic"), retainedMessage); - m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizator(), + m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizatorPolicy(), NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false); connect_v3_1_asClient(PUBLISHER_ID); @@ -285,84 +223,85 @@ public class ProtocolProcessorTest extends AbstractProtocolProcessorCommonUtils assertTrue(this.sessionsRepository.sessionForClient(PUBLISHER_ID).isEmptyQueue()); } - @Test - public void publishNoPublishToInactiveSession() { - // create an inactive session for Subscriber - ISubscriptionsDirectory mockedSubscriptions = mock(ISubscriptionsDirectory.class); - Subscription inactiveSub = new Subscription("Subscriber", new Topic("/topic"), MqttQoS.AT_LEAST_ONCE); - List inactiveSubscriptions = Collections.singletonList(inactiveSub); - when(mockedSubscriptions.matches(eq(new Topic("/topic")))).thenReturn(inactiveSubscriptions); - m_processor = new ProtocolProcessor(); - m_processor.init(mockedSubscriptions, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizator(), - NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false); - - m_processor.sessionsRepository.createNewSession("Subscriber", false); - - // Exercise - connectAsClient("Publisher"); - publishToAs("Publisher", "/topic", AT_MOST_ONCE, true); - - verifyNoPublishIsReceived(); - } - - /** - * Verify that receiving a publish with retained message and with Q0S = 0 clean the existing - * retained messages for that topic. - */ - @Test - public void testCleanRetainedStoreAfterAQoS0AndRetainedTrue() { - // force a connect - connect_v3_1_asClient("Publisher"); - - // prepare and existing retained store - publishToAs("Publisher", NEWS_TOPIC, AT_LEAST_ONCE, 100, true); - - Collection messages = m_messagesStore - .searchMatching(key -> key.match(new Topic(NEWS_TOPIC))); - assertFalse(messages.isEmpty()); - - // Exercise - // send a message that clean the previous retained publish - publishToAs("Publisher", NEWS_TOPIC, AT_MOST_ONCE, true); - - // Verify - messages = m_messagesStore.searchMatching(key -> key.match(new Topic(NEWS_TOPIC))); - assertTrue(messages.isEmpty()); - } - - @Test - public void testLowerTheQosToTheRequestedBySubscription() { - Subscription subQos1 = new Subscription("Sub A", new Topic("a/b"), MqttQoS.AT_LEAST_ONCE); - assertEquals(MqttQoS.AT_LEAST_ONCE, lowerQosToTheSubscriptionDesired(subQos1, EXACTLY_ONCE)); - - Subscription subQos2 = new Subscription("Sub B", new Topic("a/+"), EXACTLY_ONCE); - assertEquals(EXACTLY_ONCE, lowerQosToTheSubscriptionDesired(subQos2, EXACTLY_ONCE)); - } - - @Test - public void testSubscribeReceivePublishedMessageAtSubscriberQoSAndNotPublisherQoS() { - ISubscriptionsDirectory subs = new CTrieSubscriptionDirectory(); - MemoryStorageService storageService = new MemoryStorageService(null, null); - SessionsRepository sessionsRepository = new SessionsRepository(storageService.sessionsStore(), null); - subs.init(sessionsRepository); - - // simulate a connect that register a clientID to an IoSession - m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizator(), - NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false); - connect_v3_1_asClient(PUBLISHER_ID); - publishQoS2ToAs(this.m_channel, PUBLISHER_ID, NEWS_TOPIC, 1, true); - disconnect(); - - // connect from the subscriber - EmbeddedChannel subscriberChannel = new EmbeddedChannel(); - NettyUtils.clientID(subscriberChannel, SUBSCRIBER_ID); - NettyUtils.cleanSession(subscriberChannel, false); - connectAsClient(subscriberChannel, SUBSCRIBER_ID); - - // subscribe to the topic news with granted QoS0 - subscribe(subscriberChannel, NEWS_TOPIC, MqttQoS.AT_MOST_ONCE); - - // Verify the retained message arrives with QoS0 instead of QoS2 - verifyPublishIsReceived(subscriberChannel, HELLO_WORLD_MQTT, MqttQoS.AT_MOST_ONCE); - } +// Moved to PostOfficePublishTest +// @Test +// public void publishNoPublishToInactiveSession() { +// // create an inactive session for Subscriber +// ISubscriptionsDirectory mockedSubscriptions = mock(ISubscriptionsDirectory.class); +// Subscription inactiveSub = new Subscription("Subscriber", new Topic("/topic"), MqttQoS.AT_LEAST_ONCE); +// List inactiveSubscriptions = Collections.singletonList(inactiveSub); +// when(mockedSubscriptions.matches(eq(new Topic("/topic")))).thenReturn(inactiveSubscriptions); +// m_processor = new ProtocolProcessor(); +// m_processor.init(mockedSubscriptions, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizatorPolicy(), +// NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false); +// +// m_processor.sessionsRepository.createNewSession("Subscriber", false); +// +// // Exercise +// connectAsClient("Publisher"); +// publishToAs("Publisher", "/topic", AT_MOST_ONCE, true); +// +// verifyNoPublishIsReceived(); +// } +// +// /** +// * Verify that receiving a publish with retained message and with Q0S = 0 clean the existing +// * retained messages for that topic. +// */ +// @Test +// public void testCleanRetainedStoreAfterAQoS0AndRetainedTrue() { +// // force a connect +// connect_v3_1_asClient("Publisher"); +// +// // prepare and existing retained store +// publishToAs("Publisher", NEWS_TOPIC, AT_LEAST_ONCE, 100, true); +// +// Collection messages = m_messagesStore +// .searchMatching(key -> key.match(new Topic(NEWS_TOPIC))); +// assertFalse(messages.isEmpty()); +// +// // Exercise +// // send a message that clean the previous retained publish +// publishToAs("Publisher", NEWS_TOPIC, AT_MOST_ONCE, true); +// +// // Verify +// messages = m_messagesStore.searchMatching(key -> key.match(new Topic(NEWS_TOPIC))); +// assertTrue(messages.isEmpty()); +// } +// +// @Test +// public void testLowerTheQosToTheRequestedBySubscription() { +// Subscription subQos1 = new Subscription("Sub A", new Topic("a/b"), MqttQoS.AT_LEAST_ONCE); +// assertEquals(MqttQoS.AT_LEAST_ONCE, lowerQosToTheSubscriptionDesired(subQos1, EXACTLY_ONCE)); +// +// Subscription subQos2 = new Subscription("Sub B", new Topic("a/+"), EXACTLY_ONCE); +// assertEquals(EXACTLY_ONCE, lowerQosToTheSubscriptionDesired(subQos2, EXACTLY_ONCE)); +// } +// +// @Test +// public void testSubscribeReceivePublishedMessageAtSubscriberQoSAndNotPublisherQoS() { +// ISubscriptionsDirectory subs = new CTrieSubscriptionDirectory(); +// MemoryStorageService storageService = new MemoryStorageService(null, null); +// SessionsRepository sessionsRepository = new SessionsRepository(storageService.sessionsStore(), null); +// subs.init(sessionsRepository); +// +// // simulate a connect that register a clientID to an IoSession +// m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizatorPolicy(), +// NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false); +// connect_v3_1_asClient(PUBLISHER_ID); +// publishQoS2ToAs(this.m_channel, PUBLISHER_ID, NEWS_TOPIC, 1, true); +// disconnect(); +// +// // connect from the subscriber +// EmbeddedChannel subscriberChannel = new EmbeddedChannel(); +// NettyUtils.clientID(subscriberChannel, SUBSCRIBER_ID); +// NettyUtils.cleanSession(subscriberChannel, false); +// connectAsClient(subscriberChannel, SUBSCRIBER_ID); +// +// // subscribe to the topic news with granted QoS0 +// subscribe(subscriberChannel, NEWS_TOPIC, MqttQoS.AT_MOST_ONCE); +// +// // Verify the retained message arrives with QoS0 instead of QoS2 +// verifyPublishIsReceived(subscriberChannel, HELLO_WORLD_MQTT, MqttQoS.AT_MOST_ONCE); +// } } diff --git a/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_CONNECT_Test.java b/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_CONNECT_Test.java index ca2e262f..64261a45 100644 --- a/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_CONNECT_Test.java +++ b/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_CONNECT_Test.java @@ -20,7 +20,7 @@ import io.moquette.persistence.MemoryStorageService; import io.moquette.server.netty.NettyUtils; import io.moquette.spi.IMessagesStore; import io.moquette.spi.ISessionsStore; -import io.moquette.spi.impl.security.PermitAllAuthorizator; +import io.moquette.spi.impl.security.PermitAllAuthorizatorPolicy; import io.moquette.spi.impl.subscriptions.CTrieSubscriptionDirectory; import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory; import io.moquette.spi.impl.subscriptions.Subscription; @@ -60,7 +60,7 @@ public class ProtocolProcessor_CONNECT_Test { subscriptions.init(sessionsRepository); processor = new ProtocolProcessor(); processor.init(subscriptions, m_messagesStore, sessionStore, mockAuthenticator, true, - new PermitAllAuthorizator(), NO_OBSERVERS_INTERCEPTOR, + new PermitAllAuthorizatorPolicy(), NO_OBSERVERS_INTERCEPTOR, new SessionsRepository(this.sessionStore, null), false); } diff --git a/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_InternalPublish_Test.java b/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_InternalPublish_Test.java index 531876ad..ab7a7402 100644 --- a/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_InternalPublish_Test.java +++ b/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_InternalPublish_Test.java @@ -16,9 +16,6 @@ package io.moquette.spi.impl; import io.moquette.server.netty.NettyUtils; -import io.moquette.spi.impl.security.PermitAllAuthorizator; -import io.moquette.spi.impl.subscriptions.Topic; -import io.moquette.spi.security.IAuthorizator; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.mqtt.MqttQoS; import org.junit.Before; diff --git a/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_VariousUseCases_Test.java b/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_VariousUseCases_Test.java index bd84b231..9caddac4 100644 --- a/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_VariousUseCases_Test.java +++ b/broker/src/test/java/io/moquette/spi/impl/ProtocolProcessor_VariousUseCases_Test.java @@ -31,331 +31,333 @@ public class ProtocolProcessor_VariousUseCases_Test extends AbstractProtocolProc initializeProcessorAndSubsystems(); } - @Test - public void testCleanSession_maintainClientSubscriptions() { - connectAsClient(this.m_channel, "TestClient"); - subscribe("/topic", AT_MOST_ONCE); - disconnect(); - - // reconnect and publish - EmbeddedChannel anotherChannel = new EmbeddedChannel(); - connectAsClient(anotherChannel, "TestClient"); - - publishToAs(anotherChannel, "TestClient", "/topic", AT_MOST_ONCE, false); - - verifyPublishIsReceived(anotherChannel); - } - - @Ignore("At the end it's equal to testCleanSession_maintainClientSubscriptions") - @Test - public void testCleanSession_maintainClientSubscriptions_againstClientDestruction() { - connectAsClient("TestClient"); - subscribe("/topic", AT_MOST_ONCE); - disconnect(); - - this.m_channel = new EmbeddedChannel(); - // reconnect and publish - connectAsClient("TestClient"); - - publishToAs("TestClient", "/topic", AT_MOST_ONCE, false); - - verifyPublishIsReceived(); - } - - /** - * Check that after a client has connected with clean session false, subscribed to some topic - * and exited, if it reconnects with clean session true, the broker correctly cleanup every - * previous subscription - */ - @Test - public void testCleanSession_correctlyClientSubscriptions() { - connectAsClient("TestClient"); - subscribe("/topic", AT_MOST_ONCE); - disconnect(); - - this.m_channel = new EmbeddedChannel(); - // reconnect and publish - connectWithCleanSession("TestClient"); - - publishToAs("TestClient", "/topic", AT_MOST_ONCE, false); - - verifyNoPublishIsReceived(); - } - - @Test - public void testRetain_maintainMessage_againstClientDestruction() { - connect(); - publishToAs(FAKE_CLIENT_ID, "/topic", AT_LEAST_ONCE, 66, true); - disconnect(); - - // reconnect and publish - this.m_channel = new EmbeddedChannel(); - connect(); - subscribe("/topic", AT_MOST_ONCE); - - verifyPublishIsReceived(); - } - - @Test - public void testUnsubscribe_do_not_notify_anymore_same_session() { - connect(); - subscribe("/topic", AT_MOST_ONCE); - publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, false); - verifyPublishIsReceived(); - - unsubscribeAndVerifyAck("/topic"); - publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, false); - verifyNoPublishIsReceived(); - } - - @Test - public void testUnsubscribe_do_not_notify_anymore_new_session() { - connect(); - subscribe("/topic", AT_MOST_ONCE); - publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, false); - verifyPublishIsReceived(); - - unsubscribeAndVerifyAck("/topic"); - disconnect(); - - this.m_channel = new EmbeddedChannel(); - connect(); - publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, false); - verifyNoPublishIsReceived(); - } - - @Test - public void testPublishWithQoS1() { - connect(); - subscribe("/topic", AT_LEAST_ONCE); - publishToAs(FAKE_CLIENT_ID, "/topic", AT_LEAST_ONCE, 66, false); - disconnect(); - - verifyPublishIsReceived(AT_LEAST_ONCE); - } - - @Test - public void testPublishWithQoS1_notCleanSession() { - connect(); - subscribe("/topic", AT_LEAST_ONCE); - disconnect(); - - // publish a QoS 1 message another client publish a message on the topic - publishFromAnotherClient("/topic", AT_LEAST_ONCE); - - this.m_channel = new EmbeddedChannel(); - connect(); - verifyPublishIsReceived(); - } - - private void publishFromAnotherClient(String topic, MqttQoS qos) { - EmbeddedChannel anotherChannel = new EmbeddedChannel(); - connectAsClient(anotherChannel, "AnotherClient"); - publishToAs(anotherChannel, "AnotherClient", topic, qos, 67, false); - disconnect(anotherChannel); - } - - private void publishFromAnotherClient(String topic, String payload, MqttQoS qos) { - EmbeddedChannel anotherChannel = new EmbeddedChannel(); - connectAsClient(anotherChannel, "AnotherClient"); - publishToAs(anotherChannel, "AnotherClient", topic, payload, qos, 68, false); - disconnect(anotherChannel); - } - - private void publishQoS2FromAnotherClient(String topic) { - EmbeddedChannel anotherChannel = new EmbeddedChannel(); - connectAsClient(anotherChannel, "AnotherClient"); - publishQoS2ToAs(anotherChannel, "AnotherClient", topic, 67, false); - disconnect(anotherChannel); - } - - @Test - public void checkReceivePublishedMessage_after_a_reconnect_with_notCleanSession() { - connect(); - subscribe("/topic", AT_LEAST_ONCE); - disconnect(); - - this.m_channel = new EmbeddedChannel(); - connect(); - subscribe("/topic", AT_LEAST_ONCE); - - // publish a QoS 1 message another client publish a message on the topic - publishFromAnotherClient("/topic", AT_LEAST_ONCE); - - // Verify that after a reconnection the client receive the message - verifyPublishIsReceived(); - } - - @Test - public void testPublishWithQoS2() { - connect(); - subscribe("/topic", EXACTLY_ONCE); - disconnect(); - - publishQoS2FromAnotherClient("/topic"); - - this.m_channel = new EmbeddedChannel(); - connect(); - - verifyPublishIsReceived(EXACTLY_ONCE); - } - - @Test - public void avoidMultipleNotificationsAfterMultipleReconnection_cleanSessionFalseQoS1() { - connect(); - subscribe("/topic", AT_LEAST_ONCE); - disconnect(); - - publishFromAnotherClient("/topic", "Hello MQTT 1", AT_LEAST_ONCE); - - this.m_channel = new EmbeddedChannel(); - connect(); - verifyPublishIsReceived("Hello MQTT 1", AT_LEAST_ONCE); - disconnect(); - - // publish other message - publishFromAnotherClient("/topic", "Hello MQTT 2", AT_LEAST_ONCE); - - this.m_channel = new EmbeddedChannel(); - connect(); - verifyPublishIsReceived("Hello MQTT 2", AT_LEAST_ONCE); - } - - @Test - public void testConnectSubPub_cycle_getTimeout_on_second_disconnect_issue142() { - connect(); - subscribe("/topic", AT_MOST_ONCE); - publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, true); - disconnect(); - - // second loop - this.m_channel = new EmbeddedChannel(); - connect(); - subscribe("/topic", AT_MOST_ONCE); - publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, true); - disconnect(); - - assertFalse("after a disconnect the client should be disconnected", this.m_channel.isOpen()); - } - - @Test - public void checkReplayofStoredPublishResumeAfter_a_disconnect_cleanSessionFalseQoS1() { - EmbeddedChannel publisher = new EmbeddedChannel(); - connectAsClient(publisher, "Publisher"); - - connect(); - subscribe("/topic", AT_LEAST_ONCE); - - publishToAs(publisher, "Publisher", "/topic", "Hello world MQTT!!-1", AT_LEAST_ONCE, 99, false); - verifyPublishIsReceived("Hello world MQTT!!-1", AT_LEAST_ONCE); - disconnect(); - - publishToAs(publisher, "Publisher", "/topic", "Hello world MQTT!!-2", AT_LEAST_ONCE, 100, false); - publishToAs(publisher, "Publisher", "/topic", "Hello world MQTT!!-3", AT_LEAST_ONCE, 101, false); - - this.m_channel = new EmbeddedChannel(); - connect(); - verifyPublishIsReceived("Hello world MQTT!!-2", AT_LEAST_ONCE); - verifyPublishIsReceived("Hello world MQTT!!-3", AT_LEAST_ONCE); - } - - @Test - public void checkReplayStoredPublish_forNoCleanSession_qos1() { - EmbeddedChannel publisher = new EmbeddedChannel(); - connectAsClient(publisher, "Publisher"); - - connect(); - subscribe("/topic", AT_LEAST_ONCE); - disconnect(); - - publishToAs(publisher, "Publisher", "/topic", "Hello world MQTT!!-1", AT_LEAST_ONCE, 99, false); - - this.m_channel = new EmbeddedChannel(); - connect(); - subscribeAndNotReadResponse("/topic", AT_LEAST_ONCE); - verifyPublishIsReceived("Hello world MQTT!!-1", AT_LEAST_ONCE); - } - - /** - * subscriber connect and subscribe on "topic" subscriber disconnects publisher connects and - * send two message "hello1" "hello2" to "topic" subscriber connects again and receive "hello1" - * "hello2" - */ - @Test - public void checkQoS2SubscriberDisconnectReceivePersistedPublishes() { - connect(); - subscribe("topic", EXACTLY_ONCE); - disconnect(); - - EmbeddedChannel publisher = new EmbeddedChannel(); - connectAsClient(publisher, "Publisher"); - publishQoS2ToAs(publisher, "Publisher", "topic", "Hello1", 101, true); - publishQoS2ToAs(publisher, "Publisher", "topic", "Hello2", 102, true); - disconnect(publisher); - - this.m_channel = new EmbeddedChannel(); - connect(); - verifyPublishIsReceived("Hello1", EXACTLY_ONCE); - verifyPublishIsReceived("Hello2", EXACTLY_ONCE); - } - - /** - * subscriber connect and subscribe on "a/b" QoS 1 and "a/+" QoS 2 publisher connects and send a - * message "hello" on "a/b" subscriber must receive only a single message not twice - */ - @Test - public void checkSinglePublishOnOverlappingSubscriptions() { - EmbeddedChannel publisher = new EmbeddedChannel(); - connectAsClient(publisher, "Publisher"); - - connect(); - subscribe("a/+", EXACTLY_ONCE); - subscribe("a/b", AT_LEAST_ONCE); - - // force the publisher to send - publishToAs(publisher, "Publisher", "a/b", "Hello world MQTT!!", AT_LEAST_ONCE, 60, false); - - verifyPublishIsReceived("Hello world MQTT!!", AT_LEAST_ONCE); - // try to listen a second publish - verifyNoPublishIsReceived(); - } - - @Test - public void testForceClientDisconnection_issue116() { - EmbeddedChannel clientXA = new EmbeddedChannel(); - connectAsClient(clientXA, "subscriber"); - subscribe(clientXA, "topic", AT_MOST_ONCE); - - EmbeddedChannel clientXB = new EmbeddedChannel(); - connectAsClient(clientXB, "publisher"); - publishQoS2ToAs(clientXB, "publisher", "topic", "Hello", 20, false); - - EmbeddedChannel clientYA = new EmbeddedChannel(); - connectAsClient(clientYA, "subscriber"); - subscribe(clientYA, "topic", AT_MOST_ONCE); - - EmbeddedChannel clientYB = new EmbeddedChannel(); - connectAsClient(clientYB, "publisher"); - publishQoS2ToAs(clientYB, "publisher", "topic", "Hello 2", 20, true); - - assertFalse("First 'subscriber' channel MUST be closed by the broker", clientXA.isOpen()); - verifyPublishIsReceived(clientYA, "Hello 2", AT_MOST_ONCE); - } - - @Test - public void testPublishToEmptyTopic() { - EmbeddedChannel subscriber = new EmbeddedChannel(); - connectAsClient(subscriber, "Subscriber"); - - subscribe(subscriber, "#", AT_LEAST_ONCE); - - // Exercise - connect(); - publishToAs(FAKE_CLIENT_ID, "", AT_LEAST_ONCE, 1, false); - - // Verify - verifyNoPublishIsReceived(subscriber); - assertFalse("On empty topic subscription the channel MUST be closed", m_channel.isOpen()); - } +// @Test +// public void testCleanSession_maintainClientSubscriptions() { +// connectAsClient(this.m_channel, "TestClient"); +// subscribe("/topic", AT_MOST_ONCE); +// disconnect(); +// +// // reconnect and publish +// EmbeddedChannel anotherChannel = new EmbeddedChannel(); +// connectAsClient(anotherChannel, "TestClient"); +// +// publishToAs(anotherChannel, "TestClient", "/topic", AT_MOST_ONCE, false); +// +// verifyPublishIsReceived(anotherChannel); +// } +// +// @Ignore("At the end it's equal to testCleanSession_maintainClientSubscriptions") +// @Test +// public void testCleanSession_maintainClientSubscriptions_againstClientDestruction() { +// connectAsClient("TestClient"); +// subscribe("/topic", AT_MOST_ONCE); +// disconnect(); +// +// this.m_channel = new EmbeddedChannel(); +// // reconnect and publish +// connectAsClient("TestClient"); +// +// publishToAs("TestClient", "/topic", AT_MOST_ONCE, false); +// +// verifyPublishIsReceived(); +// } + +// /** +// * Check that after a client has connected with clean session false, subscribed to some topic +// * and exited, if it reconnects with clean session true, the broker correctly cleanup every +// * previous subscription +// */ +// @Test +// public void testCleanSession_correctlyClientSubscriptions() { +// connectAsClient("TestClient"); +// subscribe("/topic", AT_MOST_ONCE); +// disconnect(); +// +// this.m_channel = new EmbeddedChannel(); +// // reconnect and publish +// connectWithCleanSession("TestClient"); +// +// publishToAs("TestClient", "/topic", AT_MOST_ONCE, false); +// +// verifyNoPublishIsReceived(); +// } + +// Duplicated, just verify retain works correctly +// @Test +// public void testRetain_maintainMessage_againstClientDestruction() { +// connect(); +// publishToAs(FAKE_CLIENT_ID, "/topic", AT_LEAST_ONCE, 66, true); +// disconnect(); +// +// // reconnect and publish +// this.m_channel = new EmbeddedChannel(); +// connect(); +// subscribe("/topic", AT_MOST_ONCE); +// +// verifyPublishIsReceived(); +// } + +// @Test +// public void testUnsubscribe_do_not_notify_anymore_same_session() { +// connect(); +// subscribe("/topic", AT_MOST_ONCE); +// publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, false); +// verifyPublishIsReceived(); +// +// unsubscribeAndVerifyAck("/topic"); +// publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, false); +// verifyNoPublishIsReceived(); +// } + +// @Test +// public void testUnsubscribe_do_not_notify_anymore_new_session() { +// connect(); +// subscribe("/topic", AT_MOST_ONCE); +// publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, false); +// verifyPublishIsReceived(); +// +// unsubscribeAndVerifyAck("/topic"); +// disconnect(); +// +// this.m_channel = new EmbeddedChannel(); +// connect(); +// publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, false); +// verifyNoPublishIsReceived(); +// } +// Moved to PostOfficePublishTest +// @Test +// public void testPublishWithQoS1() { +// connect(); +// subscribe("/topic", AT_LEAST_ONCE); +// publishToAs(FAKE_CLIENT_ID, "/topic", AT_LEAST_ONCE, 66, false); +// disconnect(); +// +// verifyPublishIsReceived(AT_LEAST_ONCE); +// } + + // Moved to PostOfficePublishTest.forwardQoS1PublishesWhenNotCleanSessionReconnects +// @Test +// public void testPublishWithQoS1_notCleanSession() { +// connect(); +// subscribe("/topic", AT_LEAST_ONCE); +// disconnect(); +// +// // publish a QoS 1 message another client publish a message on the topic +// publishFromAnotherClient("/topic", AT_LEAST_ONCE); +// +// this.m_channel = new EmbeddedChannel(); +// connect(); +// verifyPublishIsReceived(); +// } + +// private void publishFromAnotherClient(String topic, MqttQoS qos) { +// EmbeddedChannel anotherChannel = new EmbeddedChannel(); +// connectAsClient(anotherChannel, "AnotherClient"); +// publishToAs(anotherChannel, "AnotherClient", topic, qos, 67, false); +// disconnect(anotherChannel); +// } + +// private void publishFromAnotherClient(String topic, String payload, MqttQoS qos) { +// EmbeddedChannel anotherChannel = new EmbeddedChannel(); +// connectAsClient(anotherChannel, "AnotherClient"); +// publishToAs(anotherChannel, "AnotherClient", topic, payload, qos, 68, false); +// disconnect(anotherChannel); +// } + +// private void publishQoS2FromAnotherClient(String topic) { +// EmbeddedChannel anotherChannel = new EmbeddedChannel(); +// connectAsClient(anotherChannel, "AnotherClient"); +// publishQoS2ToAs(anotherChannel, "AnotherClient", topic, 67, false); +// disconnect(anotherChannel); +// } + +// @Test +// public void checkReceivePublishedMessage_after_a_reconnect_with_notCleanSession() { +// connect(); +// subscribe("/topic", AT_LEAST_ONCE); +// disconnect(); +// +// this.m_channel = new EmbeddedChannel(); +// connect(); +// subscribe("/topic", AT_LEAST_ONCE); +// +// // publish a QoS 1 message another client publish a message on the topic +// publishFromAnotherClient("/topic", AT_LEAST_ONCE); +// +// // Verify that after a reconnection the client receive the message +// verifyPublishIsReceived(); +// } + +// @Test +// public void testPublishWithQoS2() { +// connect(); +// subscribe("/topic", EXACTLY_ONCE); +// disconnect(); +// +// publishQoS2FromAnotherClient("/topic"); +// +// this.m_channel = new EmbeddedChannel(); +// connect(); +// +// verifyPublishIsReceived(EXACTLY_ONCE); +// } +// +// @Test +// public void avoidMultipleNotificationsAfterMultipleReconnection_cleanSessionFalseQoS1() { +// connect(); +// subscribe("/topic", AT_LEAST_ONCE); +// disconnect(); +// +// publishFromAnotherClient("/topic", "Hello MQTT 1", AT_LEAST_ONCE); +// +// this.m_channel = new EmbeddedChannel(); +// connect(); +// verifyPublishIsReceived("Hello MQTT 1", AT_LEAST_ONCE); +// disconnect(); +// +// // publish other message +// publishFromAnotherClient("/topic", "Hello MQTT 2", AT_LEAST_ONCE); +// +// this.m_channel = new EmbeddedChannel(); +// connect(); +// verifyPublishIsReceived("Hello MQTT 2", AT_LEAST_ONCE); +// } + +// @Test +// public void testConnectSubPub_cycle_getTimeout_on_second_disconnect_issue142() { +// connect(); +// subscribe("/topic", AT_MOST_ONCE); +// publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, true); +// disconnect(); +// +// // second loop +// this.m_channel = new EmbeddedChannel(); +// connect(); +// subscribe("/topic", AT_MOST_ONCE); +// publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, true); +// disconnect(); +// +// assertFalse("after a disconnect the client should be disconnected", this.m_channel.isOpen()); +// } +// +// @Test +// public void checkReplayofStoredPublishResumeAfter_a_disconnect_cleanSessionFalseQoS1() { +// EmbeddedChannel publisher = new EmbeddedChannel(); +// connectAsClient(publisher, "Publisher"); +// +// connect(); +// subscribe("/topic", AT_LEAST_ONCE); +// +// publishToAs(publisher, "Publisher", "/topic", "Hello world MQTT!!-1", AT_LEAST_ONCE, 99, false); +// verifyPublishIsReceived("Hello world MQTT!!-1", AT_LEAST_ONCE); +// disconnect(); +// +// publishToAs(publisher, "Publisher", "/topic", "Hello world MQTT!!-2", AT_LEAST_ONCE, 100, false); +// publishToAs(publisher, "Publisher", "/topic", "Hello world MQTT!!-3", AT_LEAST_ONCE, 101, false); +// +// this.m_channel = new EmbeddedChannel(); +// connect(); +// verifyPublishIsReceived("Hello world MQTT!!-2", AT_LEAST_ONCE); +// verifyPublishIsReceived("Hello world MQTT!!-3", AT_LEAST_ONCE); +// } +// +// @Test +// public void checkReplayStoredPublish_forNoCleanSession_qos1() { +// EmbeddedChannel publisher = new EmbeddedChannel(); +// connectAsClient(publisher, "Publisher"); +// +// connect(); +// subscribe("/topic", AT_LEAST_ONCE); +// disconnect(); +// +// publishToAs(publisher, "Publisher", "/topic", "Hello world MQTT!!-1", AT_LEAST_ONCE, 99, false); +// +// this.m_channel = new EmbeddedChannel(); +// connect(); +// subscribeAndNotReadResponse("/topic", AT_LEAST_ONCE); +// verifyPublishIsReceived("Hello world MQTT!!-1", AT_LEAST_ONCE); +// } +// +// /** +// * subscriber connect and subscribe on "topic" subscriber disconnects publisher connects and +// * send two message "hello1" "hello2" to "topic" subscriber connects again and receive "hello1" +// * "hello2" +// */ +// @Test +// public void checkQoS2SubscriberDisconnectReceivePersistedPublishes() { +// connect(); +// subscribe("topic", EXACTLY_ONCE); +// disconnect(); +// +// EmbeddedChannel publisher = new EmbeddedChannel(); +// connectAsClient(publisher, "Publisher"); +// publishQoS2ToAs(publisher, "Publisher", "topic", "Hello1", 101, true); +// publishQoS2ToAs(publisher, "Publisher", "topic", "Hello2", 102, true); +// disconnect(publisher); +// +// this.m_channel = new EmbeddedChannel(); +// connect(); +// verifyPublishIsReceived("Hello1", EXACTLY_ONCE); +// verifyPublishIsReceived("Hello2", EXACTLY_ONCE); +// } + +// /** +// * subscriber connect and subscribe on "a/b" QoS 1 and "a/+" QoS 2 publisher connects and send a +// * message "hello" on "a/b" subscriber must receive only a single message not twice +// */ +// @Test +// public void checkSinglePublishOnOverlappingSubscriptions() { +// EmbeddedChannel publisher = new EmbeddedChannel(); +// connectAsClient(publisher, "Publisher"); +// +// connect(); +// subscribe("a/+", EXACTLY_ONCE); +// subscribe("a/b", AT_LEAST_ONCE); +// +// // force the publisher to send +// publishToAs(publisher, "Publisher", "a/b", "Hello world MQTT!!", AT_LEAST_ONCE, 60, false); +// +// verifyPublishIsReceived("Hello world MQTT!!", AT_LEAST_ONCE); +// // try to listen a second publish +// verifyNoPublishIsReceived(); +// } + +// @Test +// public void testForceClientDisconnection_issue116() { +// EmbeddedChannel clientXA = new EmbeddedChannel(); +// connectAsClient(clientXA, "subscriber"); +// subscribe(clientXA, "topic", AT_MOST_ONCE); +// +// EmbeddedChannel clientXB = new EmbeddedChannel(); +// connectAsClient(clientXB, "publisher"); +// publishQoS2ToAs(clientXB, "publisher", "topic", "Hello", 20, false); +// +// EmbeddedChannel clientYA = new EmbeddedChannel(); +// connectAsClient(clientYA, "subscriber"); +// subscribe(clientYA, "topic", AT_MOST_ONCE); +// +// EmbeddedChannel clientYB = new EmbeddedChannel(); +// connectAsClient(clientYB, "publisher"); +// publishQoS2ToAs(clientYB, "publisher", "topic", "Hello 2", 20, true); +// +// assertFalse("First 'subscriber' channel MUST be closed by the broker", clientXA.isOpen()); +// verifyPublishIsReceived(clientYA, "Hello 2", AT_MOST_ONCE); +// } + +// @Test +// public void testPublishToEmptyTopic() { +// EmbeddedChannel subscriber = new EmbeddedChannel(); +// connectAsClient(subscriber, "Subscriber"); +// +// subscribe(subscriber, "#", AT_LEAST_ONCE); +// +// // Exercise +// connect(); +// publishToAs(FAKE_CLIENT_ID, "", AT_LEAST_ONCE, 1, false); +// +// // Verify +// verifyNoPublishIsReceived(subscriber); +// assertFalse("On empty topic subscription the channel MUST be closed", m_channel.isOpen()); +// } } diff --git a/broker/src/test/resources/log4j.properties b/broker/src/test/resources/log4j.properties index ea5a247a..ae9cd4f3 100644 --- a/broker/src/test/resources/log4j.properties +++ b/broker/src/test/resources/log4j.properties @@ -1,7 +1,8 @@ -# Set root logger level to DEBUG and its only appender to A1. log4j.rootLogger=ERROR, stdout, file log4j.logger.io.moquette=WARN +log4j.logger.io.moquette.broker=DEBUG +#log4j.logger.io.moquette.broker=WARN log4j.logger.io.moquette.spi.impl.ProtocolProcessor=WARN @@ -17,9 +18,11 @@ log4j.logger.io.moquette.spi.persistence.MapDBSessionsStore=WARN log4j.logger.io.moquette.interception.HazelcastInterceptHandler=WARN log4j.logger.io.moquette.server.HazelcastListener=WARN -# stdout appender is set to be a ConsoleAppender. +# stdout appender is set to be consoleAppender. log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.Threshold=INFO +# only for test log4j.appender.stdout.Threshold=TRACE + # for debug trace log4j.appender.stdout.layout=org.apache.log4j.PatternLayout #log4j.appender.stdout.layout.ConversionPattern=%-4r [%t] %-5p %c{1} %x - %m%n @@ -28,6 +31,7 @@ log4j.appender.stdout.layout.ConversionPattern=%d{dd/MM/yyyy HH:mm:ss,SSS} [%t] #file appender log4j.appender.file=org.apache.log4j.RollingFileAppender log4j.appender.file.Threshold=INFO +# only for tests log4j.appender.file.Threshold=TRACE log4j.appender.file.File=moquette.log log4j.appender.file.MaxFileSize=100MB log4j.appender.file.MaxBackupIndex=1 diff --git a/perf/build.gradle b/perf/build.gradle index f71caeae..9a1bc5e7 100644 --- a/perf/build.gradle +++ b/perf/build.gradle @@ -1,9 +1,17 @@ description = 'Moquette - performance tests' + +repositories { + jcenter() + mavenCentral() + maven { url 'https://oss.jfrog.org/artifactory/oss-snapshot-local' } +} + dependencies { compile project(':moquette-broker') compile group: 'io.netty', name: 'netty-codec-mqtt', version:"${nettyVersion}" compile group: 'org.hdrhistogram', name: 'HdrHistogram', version:'2.1.2' compile group: 'org.eclipse.paho', name: 'org.eclipse.paho.client.mqttv3', version:'1.2.0' compile group: 'org.eclipse.jetty.toolchain', name: 'jetty-perf-helper', version:'1.0.5' + compile(group: 'org.mqttbee', name: 'mqtt-bee', version: '1.0.0-SNAPSHOT') } diff --git a/perf/src/main/java/io/moquette/performance/clients/mqttbee/BeeBenchmarker.java b/perf/src/main/java/io/moquette/performance/clients/mqttbee/BeeBenchmarker.java new file mode 100644 index 00000000..4d27823e --- /dev/null +++ b/perf/src/main/java/io/moquette/performance/clients/mqttbee/BeeBenchmarker.java @@ -0,0 +1,100 @@ +package io.moquette.performance.clients.mqttbee; + +import io.reactivex.Flowable; +import io.reactivex.Single; +import org.mqttbee.api.mqtt.MqttClient; +import org.mqttbee.api.mqtt.datatypes.MqttQos; +import org.mqttbee.api.mqtt.mqtt3.Mqtt3Client; +import org.mqttbee.api.mqtt.mqtt3.message.connect.connack.Mqtt3ConnAck; +import org.mqttbee.api.mqtt.mqtt3.message.publish.Mqtt3Publish; +import org.mqttbee.api.mqtt.mqtt3.message.publish.Mqtt3PublishResult; +import org.mqttbee.api.mqtt.mqtt3.message.subscribe.suback.Mqtt3SubAck; +import org.mqttbee.rx.FlowableWithSingle; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class BeeBenchmarker { + + public static void main(String[] args) { + final Mqtt3Client client = MqttClient.builder() + .identifier(UUID.randomUUID().toString()) + .serverHost("localhost") + .serverPort(1883) + .useMqttVersion3() + .buildReactive(); + + /* + * As we use the reactive API, the following line does not connect yet, but return a reactive type. + * e.g. Single is something like a lazy and reusable future. + */ + final Single connAckSingle = client.connect().keepAlive(10, TimeUnit.SECONDS).done(); + +// /* +// * Same here: the following line does not subscribe yet, but return a reactive type. +// * FlowableWithSingle is a combination of the single SUBACK message and a Flowable of PUBLISH messages. +// * A Flowable is an asynchronous stream, that enables back pressure from the application over MQTT Bee to the broker. +// */ +// final FlowableWithSingle subAckAndMatchingPublishes = client.subscribeWithStream() +// .addSubscription().topicFilter("a/b/c").qos(MqttQos.AT_LEAST_ONCE).done() +// .addSubscription().topicFilter("a/b/c/d").qos(MqttQos.EXACTLY_ONCE).done() +// .done(); +// +// /* +// * The reactive types offer as many operators that will not be covered here. +// * Here we register simple callbacks to print messages when we received the CONNACK, SUBACK and matching PUBLISH messages. +// */ +// final Single connectScenario = connAckSingle +// .doOnSuccess(connAck -> System.out.println("Connected with return code " + connAck.getReturnCode())) +// .doOnError(throwable -> System.out.println("Connection failed, " + throwable.getMessage())); +// +// final Flowable subscribeScenario = subAckAndMatchingPublishes +// .doOnSingle((subAck, subscription) -> +// System.out.println("Subscribed with return codes " + subAck.getReturnCodes())) +// .doOnNext(publish -> +// System.out.println("Received publish" + +// ", topic: " + publish.getTopic() + +// ", QoS: " + publish.getQos() + +// ", payload: " + new String(publish.getPayloadAsBytes()) +// ) +// ); +// +// /* +// * Reactive types let us compose a sequence of actions +// */ +// final Flowable connectAndSubscribe = connectScenario.toCompletable().andThen(subscribeScenario); +// +// /* +// * By subscribing to reactive types, the sequence is executed +// */ +// connectAndSubscribe.blockingSubscribe(); + + + + /* + * Fake a stream of PUBLISH messages to publish with incrementing ids + */ + final AtomicInteger counter = new AtomicInteger(); + Flowable messagesToPublish = Flowable.generate(emitter -> { + final int id = counter.incrementAndGet(); + final Mqtt3Publish publishMessage = Mqtt3Publish.builder() + .topic("a/b/c").qos(MqttQos.AT_LEAST_ONCE).payload(("test " + id).getBytes()).build(); + emitter.onNext(publishMessage); + }); + /* + * Emit 1 message only every 100 milliseconds + */ + messagesToPublish = messagesToPublish.zipWith(Flowable.interval(10, TimeUnit.MILLISECONDS), (publish, aLong) -> publish); + + final Single connectScenario = connAckSingle + .doOnSuccess(connAck -> System.out.println("Connected with return code " + connAck.getReturnCode())) + .doOnError(throwable -> System.out.println("Connection failed, " + throwable.getMessage())); + + final Flowable publishScenario = client.publish(messagesToPublish) + .doOnNext(publishResult -> System.out.println("Publish acknowledged: " + new String(publishResult.getPublish().getPayloadAsBytes()))); + + connectScenario.toCompletable().andThen(publishScenario).blockingSubscribe(); + + } +} diff --git a/perf/src/main/java/io/moquette/performance/clients/paho/PahoBenchmarker.java b/perf/src/main/java/io/moquette/performance/clients/paho/PahoBenchmarker.java new file mode 100644 index 00000000..5a3e4c95 --- /dev/null +++ b/perf/src/main/java/io/moquette/performance/clients/paho/PahoBenchmarker.java @@ -0,0 +1,56 @@ +package io.moquette.performance.clients.paho; + +import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence; + +import java.nio.ByteBuffer; + +public class PahoBenchmarker { + + private static void publishQos0(IMqttAsyncClient client, long numMessages) throws MqttException { + System.out.println("publishing.."); + long startTime = System.currentTimeMillis(); + for (int i = 0; i < numMessages; i++) { + byte[] bytes = ByteBuffer.allocate(8).putLong(System.currentTimeMillis()).array(); + client.publish("topic", bytes, 0, false); + } + client.publish("/exit", "Exit".getBytes(), 0, false); + long stopTime = System.currentTimeMillis(); + long spentTime = stopTime -startTime; + System.out.println("published in " + spentTime + " ms"); + } + + public static void main(String[] args) throws MqttException, InterruptedException { + if (args.length < 2) { + System.out.println("Usage benchmarker "); + System.out.println("java io.moquette.performance.clients.paho.PahoBenchmarker localhost 1000 500 test1"); + return; + } + + String host = args[0]; + long numMessages = Long.parseLong(args[1]); + String tmpDir = System.getProperty("java.io.tmpdir"); + MqttDefaultFilePersistence dataStore = new MqttDefaultFilePersistence(tmpDir); + //MemoryPersistence dataStore = new MemoryPersistence() + + int rnd = (int) (Math.random() * 100); + MqttAsyncClient client = new MqttAsyncClient("tcp://" + host +":1883", + "SubscriberClient" + rnd, dataStore); + SubscriberCallback callback = new SubscriberCallback(); + client.setCallback(callback); + client.connect().waitForCompletion(1000); + client.subscribe("topic", 0); + System.out.println("subscribed to topic"); + client.subscribe("/exit", 0); + System.out.println("subscribed to /exit"); + + publishQos0(client, numMessages); + + callback.waitFinish(); +// Map sorted = callback.distribution.sort( { k1, k2 -> k1 <=> k2 } as Comparator) +// File csv = new File("perf.csv") +// sorted.each {delay, count -> csv << "${delay}, ${count}\r\n"} + } +} diff --git a/perf/src/main/java/io/moquette/performance/clients/paho/SubscriberCallback.java b/perf/src/main/java/io/moquette/performance/clients/paho/SubscriberCallback.java new file mode 100644 index 00000000..3f133377 --- /dev/null +++ b/perf/src/main/java/io/moquette/performance/clients/paho/SubscriberCallback.java @@ -0,0 +1,63 @@ +package io.moquette.performance.clients.paho; + +import org.eclipse.paho.client.mqttv3.*; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +class SubscriberCallback implements MqttCallback { + + int m_numReceived = 0; + long m_startTime; + boolean firstMessageReceived = false; + + private CountDownLatch m_latch = new CountDownLatch(1); + + //Maps delay to num of messages reached with that delay + protected Map distribution = new HashMap<>(); + + IMqttAsyncClient client; + + void waitFinish() throws InterruptedException { + m_latch.await(); + } + + public void connectionLost(Throwable cause) { + m_latch.countDown(); + } + + public void messageArrived(String topic, MqttMessage message) throws Exception { + //println "Received message ${new String(message.payload)} on topic [${topic}]" + if (!firstMessageReceived) { + m_startTime = System.currentTimeMillis(); + firstMessageReceived = true; + } + if (topic == "/exit") { + long stopTime = System.currentTimeMillis(); + long spentTime = stopTime - m_startTime; + System.out.println("/exit received"); + System.out.println("subscriber disconnected, received ${m_numReceived} messages in ${spentTime} ms (from first received to last one on topic /exit)"); + client.disconnect(); + m_latch.countDown(); + } else { + ByteBuffer bb = ByteBuffer.wrap(message.getPayload()); + long sentTime = bb.getLong(); + long delay = System.currentTimeMillis() - sentTime; + //println "-received in ${delay}(ms) on ${topic} with QoS ${message.qos}" + //distribution[delay] = distribution[delay] != null ? distribution[delay]++ : 1 + Long value = distribution.get(delay); + if (value != null) { + distribution.put(delay, value++); + } else { + distribution.put(delay, 1L); + } + m_numReceived++; + } + } + + public void deliveryComplete(IMqttDeliveryToken token) { + //To change body of implemented methods use File | Settings | File Templates. + } +} diff --git a/tools_scripts/benchmark/benchmarker.groovy b/tools_scripts/benchmark/benchmarker.groovy index fb99460a..9b8d9b9b 100644 --- a/tools_scripts/benchmark/benchmarker.groovy +++ b/tools_scripts/benchmark/benchmarker.groovy @@ -1,14 +1,16 @@ //@GrabResolver(name='Paho', root='https://repo.eclipse.org/content/repositories/paho-snapshots/') //@Grab(group='org.eclipse.paho', module='org.eclipse.paho.client.mqttv3', version='1.0.1-SNAPSHOT') +//@GrabResolver(name='Paho', root='https://repo.eclipse.org/content/repositories/paho-releases/') +//@Grab(group='org.eclipse.paho', module='mqtt-client', version='0.4.0') @GrabResolver(name='Paho', root='https://repo.eclipse.org/content/repositories/paho-releases/') -@Grab(group='org.eclipse.paho', module='mqtt-client', version='0.4.0') +@Grab(group='org.eclipse.paho', module='org.eclipse.paho.client.mqttv3', version='1.2.0') import org.eclipse.paho.client.mqttv3.MqttAsyncClient import org.eclipse.paho.client.mqttv3.MqttMessage -import org.eclipse.paho.client.mqttv3.MqttException import org.eclipse.paho.client.mqttv3.MqttCallback import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence +//import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence import java.util.concurrent.CountDownLatch import static groovy.json.JsonOutput.* import java.nio.ByteBuffer @@ -71,7 +73,7 @@ def publishQos0(client, long numMessages) { print "publishing.." long startTime = System.currentTimeMillis() (1..numMessages).each { - byte[] bytes = ByteBuffer.allocate(8).putLong(System.currentTimeMillis()).array(); + byte[] bytes = ByteBuffer.allocate(8).putLong(System.currentTimeMillis()).array() client.publish('topic', bytes, 0, false) } client.publish('/exit', 'Exit'.bytes, 0, false) @@ -82,6 +84,7 @@ def publishQos0(client, long numMessages) { if (args.size() < 2) { println "Usage benchmarker " + println "groovy benchmarker.groovy localhost 1000 500 test1" return } @@ -89,12 +92,13 @@ String host = args[0] long numMessages = args[1] as long String tmpDir = System.getProperty("java.io.tmpdir") MqttDefaultFilePersistence dataStore = new MqttDefaultFilePersistence(tmpDir) +//MemoryPersistence dataStore = new MemoryPersistence() int rnd = (Math.random() * 100) as int MqttAsyncClient client = new MqttAsyncClient("tcp://${host}:1883", "SubscriberClient${rnd}", dataStore) def callback = new SubscriberCallback() client.callback = callback -client.connect().waitForCompletion(1000); +client.connect().waitForCompletion(1000) client.subscribe("topic", 0) println "subscribed to topic" client.subscribe("/exit", 0) diff --git a/tools_scripts/integration/allAroundWalk.groovy b/tools_scripts/integration/allAroundWalk.groovy new file mode 100644 index 00000000..adf36e22 --- /dev/null +++ b/tools_scripts/integration/allAroundWalk.groovy @@ -0,0 +1,96 @@ +@GrabResolver(name='Paho', root='https://repo.eclipse.org/content/repositories/paho-releases/') +@Grab(group='org.eclipse.paho', module='org.eclipse.paho.client.mqttv3', version='1.2.0', ext='jar') + + +import org.eclipse.paho.client.mqttv3.MqttClient +import org.eclipse.paho.client.mqttv3.MqttMessage +import org.eclipse.paho.client.mqttv3.MqttCallback +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken +import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +/** + * A client that try all the simple MQTT commands + * */ + +if (args.size() != 2) { + println "usage: " + return +} + +class SubscriberCallback implements MqttCallback { + + private CountDownLatch m_latch = new CountDownLatch(1) + + void waitFinish() { + boolean reachZero = m_latch.await(4, TimeUnit.SECONDS) + if (!reachZero) { + println "Expired the time to receive the publish" + } + } + + void renewWaiter() { + m_latch = new CountDownLatch(1) + } + + void messageArrived(String topic, MqttMessage message) throws Exception { + println "Received on [$topic] message: [$message]" + m_latch.countDown() + } + + void deliveryComplete(IMqttDeliveryToken token) { + println "Delivery complete" + } + + void connectionLost(Throwable th) { + println "Connection lost" + m_latch.countDown() + } +} + + +String host = args[0] +String clientName = args[1] + +String tmpDir = System.getProperty("java.io.tmpdir") +MqttDefaultFilePersistence dataStore1 = new MqttDefaultFilePersistence(tmpDir + "/" + clientName) +MqttClient client1 = new MqttClient("tcp://${host}:1883", "ConCli${clientName}", dataStore1) +def callback = new SubscriberCallback() +client1.callback = callback + +println "start test" +print "connect..." +client1.connect() +println "OK!" + +print "subscribe..." +client1.subscribe("/news", 0) +println "OK!" + +/*print "publish qos0..." +client1.publish("/news", "Moquette is going to big refactoring, QoS0".bytes, 0, false) +callback.waitFinish() +println "OK!" +callback.renewWaiter() + +print "publish qos1..." +client1.publish("/news", "Moquette is going to big refactoring, QoS1".bytes, 1, false) +callback.waitFinish() +println "OK!" +callback.renewWaiter()*/ + +print "publish qos2..." +client1.publish("/news", "Moquette is going to big refactoring, QoS2".bytes, 2, false) +callback.waitFinish() +println "OK!" + +print "unsubscribe..." +client1.unsubscribe("/news") +println "OK!" + +print "disconnect..." +client1.disconnect() +println "OK!" +println "Done" +System.exit(0) diff --git a/tools_scripts/integration/issue39_loopingConnecter.groovy b/tools_scripts/integration/issue39_loopingConnecter.groovy index 5e215c77..f7ef3709 100644 --- a/tools_scripts/integration/issue39_loopingConnecter.groovy +++ b/tools_scripts/integration/issue39_loopingConnecter.groovy @@ -13,7 +13,7 @@ import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence * */ if (args.size() != 3) { - println "usage: " + println "usage: " return } @@ -31,4 +31,4 @@ println "start loppoing to ${numSerialConnection} conn-discon" client1.disconnect() print "." } -println "Done" \ No newline at end of file +println "Done" diff --git a/tools_scripts/integration/pahoConnect.groovy b/tools_scripts/integration/pahoConnect.groovy new file mode 100644 index 00000000..f6114286 --- /dev/null +++ b/tools_scripts/integration/pahoConnect.groovy @@ -0,0 +1,19 @@ +@GrabResolver(name='Paho', root='https://repo.eclipse.org/content/repositories/paho-releases/') +@Grab(group='org.eclipse.paho', module='org.eclipse.paho.client.mqttv3', version='1.0.1', ext='jar') + + +import org.eclipse.paho.client.mqttv3.MqttClient +import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence + +String host = args[0] + +String tmpDir = System.getProperty("java.io.tmpdir") +MqttDefaultFilePersistence dataStore1 = new MqttDefaultFilePersistence(tmpDir + "/client1") +MqttClient client1 = new MqttClient("tcp://${host}:1883", "ConnectedClient", dataStore1) +client1.connect() +println "Client1 connected" + +println "Ok mate, done!" + +client1.disconnect() +println "Client1 disconnected"