Rearchitected the broker, switching from ProtocolProcessor and publisher to MQTTConnection, Session and PostOffice

This commit is contained in:
andsel
2018-11-02 12:33:59 +01:00
parent 0ad6ffdd3c
commit a2266e5bbe
60 changed files with 5225 additions and 1025 deletions
+1 -1
View File
@@ -23,9 +23,9 @@ dependencies {
compile group: 'com.bugsnag', name: 'bugsnag', version:'[3.0,4.0)' compile group: 'com.bugsnag', name: 'bugsnag', version:'[3.0,4.0)'
compile group: 'commons-codec', name: 'commons-codec', version:'1.10' 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' 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.slf4j', name: 'slf4j-log4j12', version:'1.7.5'
testCompile group: 'org.fusesource.mqtt-client', name: 'mqtt-client', version:'1.12' testCompile group: 'org.fusesource.mqtt-client', name: 'mqtt-client', version:'1.12'
@@ -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<MqttTopicSubscription> verifyTopicsReadAccess(String clientID, String username, MqttSubscribeMessage msg) {
List<MqttTopicSubscription> 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);
}
}
@@ -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;
}
}
@@ -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);
}
@@ -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());
}
}
@@ -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<String> 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<String> 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 + '}';
}
}
@@ -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);
}
}
@@ -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<String, MqttPublishMessage> 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);
}
}
@@ -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<BinaryWebSocketFrame> {
@Override
protected void decode(ChannelHandlerContext chc, BinaryWebSocketFrame frame, List<Object> out)
throws Exception {
// convert the frame to a ByteBuf
ByteBuf bb = frame.content();
// System.out.println("WebSocketFrameToByteBufDecoder decode - " +
// ByteBufUtil.hexDump(bb));
bb.retain();
out.add(bb);
}
}
static class ByteBufToWebSocketFrameEncoder extends MessageToMessageEncoder<ByteBuf> {
@Override
protected void encode(ChannelHandlerContext chc, ByteBuf bb, List<Object> out) throws Exception {
// convert the ByteBuf to a WebSocketFrame
BinaryWebSocketFrame result = new BinaryWebSocketFrame();
// System.out.println("ByteBufToWebSocketFrameEncoder encode - " +
// ByteBufUtil.hexDump(bb));
result.content().writeBytes(bb);
out.add(result);
}
}
private abstract static class PipelineInitializer {
abstract void init(SocketChannel channel) throws Exception;
}
private static final Logger LOG = LoggerFactory.getLogger(NewNettyAcceptor.class);
EventLoopGroup m_bossGroup;
EventLoopGroup m_workerGroup;
BytesMetricsCollector m_bytesMetricsCollector = new BytesMetricsCollector();
MessageMetricsCollector m_metricsCollector = new MessageMetricsCollector();
private Optional<? extends ChannelInboundHandler> metrics;
private Optional<? extends ChannelInboundHandler> errorsCather;
private int nettySoBacklog;
private boolean nettySoReuseaddr;
private boolean nettyTcpNodelay;
private boolean nettySoKeepalive;
private int nettyChannelTimeoutSeconds;
private int maxBytesInMessage;
private Class<? extends ServerSocketChannel> channelClass;
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<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
pipeliner.init(ch);
}
})
.option(ChannelOption.SO_BACKLOG, nettySoBacklog)
.option(ChannelOption.SO_REUSEADDR, nettySoReuseaddr)
.childOption(ChannelOption.TCP_NODELAY, nettyTcpNodelay)
.childOption(ChannelOption.SO_KEEPALIVE, nettySoKeepalive);
try {
LOG.debug("Binding server. host={}, port={}", host, port);
// Bind and start to accept incoming connections.
ChannelFuture f = b.bind(host, port);
LOG.info("Server bound to host={}, port={}, protocol={}", host, port, protocol);
f.sync().addListener(FIRE_EXCEPTION_ON_FAILURE);
} catch (InterruptedException ex) {
LOG.error("An interruptedException was caught while initializing server. Protocol={}", protocol, ex);
}
}
private void initializePlainTCPTransport(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);
// }
}
@@ -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<Object> 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);
}
}
@@ -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<MqttTopicSubscription> ackTopics = authorizator.verifyTopicsReadAccess(clientID, username, msg);
MqttSubAckMessage ackMessage = doAckMessageFromValidateFilters(ackTopics, messageID);
// store topics subscriptions in session
List<Subscription> 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<Subscription> 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<MqttTopicSubscription> topicFilters, int messageId) {
List<Integer> grantedQoSLevels = new ArrayList<>();
for (MqttTopicSubscription req : topicFilters) {
grantedQoSLevels.add(req.qualityOfService().value());
}
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.SUBACK, false, AT_MOST_ONCE,
false, 0);
MqttSubAckPayload payload = new MqttSubAckPayload(grantedQoSLevels);
return new MqttSubAckMessage(fixedHeader, from(messageId), payload);
}
public void unsubscribe(List<String> 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<Subscription> 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);
}
}
@@ -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<? extends InterceptHandler> handlers) throws IOException {
LOG.debug("Starting moquette server using IConfig instance and intercept handlers");
startServer(config, handlers, null, null, null);
}
public void startServer(IConfig config, List<? extends InterceptHandler> handlers, ISslContextCreator sslCtxCreator,
IAuthenticator authenticator, IAuthorizatorPolicy 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);
}
}
@@ -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<SessionRegistry.EnqueuedMessage> sessionQueue;
private final AtomicReference<SessionStatus> status = new AtomicReference<>(SessionStatus.DISCONNECTED);
private MQTTConnection mqttConnection;
private List<Subscription> subscriptions = new ArrayList<>();
private final Map<Integer, SessionRegistry.EnqueuedMessage> inflightWindow = new HashMap<>();
private final DelayQueue<InFlightPacket> inflightTimeouts = new DelayQueue<>();
private final Map<Integer, MqttPublishMessage> qos2Receiving = new HashMap<>();
private final AtomicInteger inflightSlots = new AtomicInteger(INFLIGHT_WINDOW_SIZE); // this should be configurable
Session(String clientId, boolean clean, Will will, Queue<SessionRegistry.EnqueuedMessage> sessionQueue) {
this.clientId = clientId;
this.clean = clean;
this.will = will;
this.sessionQueue = sessionQueue;
}
Session(boolean clean, String clientId, Queue<SessionRegistry.EnqueuedMessage> 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<Subscription> getSubscriptions() {
return new ArrayList<>(subscriptions);
}
public void addSubscriptions(List<Subscription> 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<InFlightPacket> 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 +
'}';
}
}
@@ -0,0 +1,10 @@
package io.moquette.broker;
public class SessionCorruptedException extends RuntimeException {
private static final long serialVersionUID = 5848069213104389412L;
SessionCorruptedException(String msg) {
super(msg);
}
}
@@ -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<String, Session> pool = new ConcurrentHashMap<>();
private final ISubscriptionsDirectory subscriptionsDirectory;
private final ConcurrentMap<String, Queue<SessionRegistry.EnqueuedMessage>> 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<SessionRegistry.EnqueuedMessage> 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);
}
}
@@ -25,7 +25,7 @@ import io.moquette.spi.impl.ProtocolProcessor;
import io.moquette.spi.impl.ProtocolProcessorBootstrapper; import io.moquette.spi.impl.ProtocolProcessorBootstrapper;
import io.moquette.spi.impl.subscriptions.Subscription; import io.moquette.spi.impl.subscriptions.Subscription;
import io.moquette.spi.security.IAuthenticator; 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.moquette.spi.security.ISslContextCreator;
import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -141,7 +141,7 @@ public class Server {
} }
public void startServer(IConfig config, List<? extends InterceptHandler> handlers, ISslContextCreator sslCtxCreator, public void startServer(IConfig config, List<? extends InterceptHandler> handlers, ISslContextCreator sslCtxCreator,
IAuthenticator authenticator, IAuthorizator authorizator) throws IOException { IAuthenticator authenticator, IAuthorizatorPolicy authorizator) throws IOException {
final long start = System.currentTimeMillis(); final long start = System.currentTimeMillis();
if (handlers == null) { if (handlers == null) {
handlers = Collections.emptyList(); handlers = Collections.emptyList();
@@ -27,7 +27,7 @@ import io.netty.handler.timeout.IdleStateEvent;
import static io.netty.channel.ChannelFutureListener.CLOSE_ON_FAILURE; import static io.netty.channel.ChannelFutureListener.CLOSE_ON_FAILURE;
@Sharable @Sharable
class MoquetteIdleTimeoutHandler extends ChannelDuplexHandler { public class MoquetteIdleTimeoutHandler extends ChannelDuplexHandler {
private static final Logger LOG = LoggerFactory.getLogger(MoquetteIdleTimeoutHandler.class); private static final Logger LOG = LoggerFactory.getLogger(MoquetteIdleTimeoutHandler.class);
@@ -10,7 +10,7 @@ import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory;
import io.moquette.spi.impl.subscriptions.Subscription; import io.moquette.spi.impl.subscriptions.Subscription;
import io.moquette.spi.impl.subscriptions.Topic; import io.moquette.spi.impl.subscriptions.Topic;
import io.moquette.spi.security.IAuthenticator; 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.Channel;
import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelFutureListener;
@@ -40,7 +40,7 @@ public class ConnectHandler {
private SessionsRepository sessionsRepository; private SessionsRepository sessionsRepository;
private ISessionsStore m_sessionsStore; private ISessionsStore m_sessionsStore;
private IAuthenticator m_authenticator; private IAuthenticator m_authenticator;
private IAuthorizator m_authorizator; private IAuthorizatorPolicy m_authorizator;
private ISubscriptionsDirectory subscriptions; private ISubscriptionsDirectory subscriptions;
private boolean allowAnonymous; private boolean allowAnonymous;
private boolean allowZeroByteClientId; private boolean allowZeroByteClientId;
@@ -48,7 +48,7 @@ public class ConnectHandler {
private InternalRepublisher internalRepublisher; private InternalRepublisher internalRepublisher;
ConnectHandler(IConnectionsManager connectedClients, BrokerInterceptor interceptor, SessionsRepository sessionsRepository, 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, ISubscriptionsDirectory subscriptions, boolean allowAnonymous, boolean allowZeroByteClientId,
boolean reauthorizeSubscriptionsOnConnect, InternalRepublisher internalRepublisher) { boolean reauthorizeSubscriptionsOnConnect, InternalRepublisher internalRepublisher) {
this.connectionDescriptors = connectedClients; this.connectionDescriptors = connectedClients;
@@ -20,10 +20,19 @@ import io.netty.buffer.ByteBuf;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
final class DebugUtils { public final class DebugUtils {
static String payload2Str(ByteBuf content) { public static String payload2Str(ByteBuf content) {
return new String(content.copy().array(), StandardCharsets.UTF_8); 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() { private DebugUtils() {
@@ -31,7 +31,7 @@ import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory;
import io.moquette.spi.impl.subscriptions.Subscription; import io.moquette.spi.impl.subscriptions.Subscription;
import io.moquette.spi.impl.subscriptions.Topic; import io.moquette.spi.impl.subscriptions.Topic;
import io.moquette.spi.security.IAuthenticator; 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.buffer.ByteBuf;
import io.netty.channel.Channel; import io.netty.channel.Channel;
import io.netty.handler.codec.mqtt.*; import io.netty.handler.codec.mqtt.*;
@@ -41,11 +41,9 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; 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.InternalRepublisher.createPublishForQos;
import static io.moquette.spi.impl.Utils.messageId; import static io.moquette.spi.impl.Utils.messageId;
import static io.moquette.spi.impl.Utils.readBytesAndRewind; import static io.moquette.spi.impl.Utils.readBytesAndRewind;
@@ -72,7 +70,7 @@ public class ProtocolProcessor {
private ConcurrentMap<RunningSubscription, SubscriptionState> subscriptionInCourse; private ConcurrentMap<RunningSubscription, SubscriptionState> subscriptionInCourse;
private ISubscriptionsDirectory subscriptions; private ISubscriptionsDirectory subscriptions;
private IAuthorizator m_authorizator; private IAuthorizatorPolicy m_authorizator;
private IMessagesStore m_messagesStore; private IMessagesStore m_messagesStore;
@@ -94,7 +92,7 @@ public class ProtocolProcessor {
} }
public void init(ISubscriptionsDirectory subscriptions, IMessagesStore storageService, ISessionsStore sessionsStore, 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, BrokerInterceptor interceptor, SessionsRepository sessionsRepository,
boolean reauthorizeSubscriptionsOnConnect) { boolean reauthorizeSubscriptionsOnConnect) {
init(subscriptions, storageService, sessionsStore, authenticator, allowAnonymous, false, init(subscriptions, storageService, sessionsStore, authenticator, allowAnonymous, false,
@@ -103,7 +101,7 @@ public class ProtocolProcessor {
public void init(ISubscriptionsDirectory subscriptions, IMessagesStore storageService, ISessionsStore sessionsStore, public void init(ISubscriptionsDirectory subscriptions, IMessagesStore storageService, ISessionsStore sessionsStore,
IAuthenticator authenticator, boolean allowAnonymous, boolean allowZeroByteClientId, IAuthenticator authenticator, boolean allowAnonymous, boolean allowZeroByteClientId,
IAuthorizator authorizator, BrokerInterceptor interceptor, SessionsRepository sessionsRepository, IAuthorizatorPolicy authorizator, BrokerInterceptor interceptor, SessionsRepository sessionsRepository,
boolean reauthorizeSubscriptionsOnConnect) { boolean reauthorizeSubscriptionsOnConnect) {
init(new ConnectionDescriptorStore(), subscriptions, storageService, sessionsStore, init(new ConnectionDescriptorStore(), subscriptions, storageService, sessionsStore,
authenticator, allowAnonymous, allowZeroByteClientId, authorizator, interceptor, sessionsRepository, authenticator, allowAnonymous, allowZeroByteClientId, authorizator, interceptor, sessionsRepository,
@@ -130,7 +128,7 @@ public class ProtocolProcessor {
*/ */
void init(IConnectionsManager connectionDescriptors, ISubscriptionsDirectory subscriptions, void init(IConnectionsManager connectionDescriptors, ISubscriptionsDirectory subscriptions,
IMessagesStore storageService, ISessionsStore sessionsStore, IAuthenticator authenticator, IMessagesStore storageService, ISessionsStore sessionsStore, IAuthenticator authenticator,
boolean allowAnonymous, boolean allowZeroByteClientId, IAuthorizator authorizator, boolean allowAnonymous, boolean allowZeroByteClientId, IAuthorizatorPolicy authorizator,
BrokerInterceptor interceptor, SessionsRepository sessionsRepository, BrokerInterceptor interceptor, SessionsRepository sessionsRepository,
boolean reauthorizeSubscriptionsOnConnect) { boolean reauthorizeSubscriptionsOnConnect) {
LOG.info("Initializing MQTT protocol processor.."); LOG.info("Initializing MQTT protocol processor..");
@@ -31,7 +31,7 @@ import io.moquette.spi.impl.subscriptions.CTrieSubscriptionDirectory;
import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory; import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory;
import io.moquette.spi.impl.subscriptions.Subscription; import io.moquette.spi.impl.subscriptions.Subscription;
import io.moquette.spi.security.IAuthenticator; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -82,7 +82,7 @@ public class ProtocolProcessorBootstrapper {
* @return the processor created for the broker. * @return the processor created for the broker.
*/ */
public ProtocolProcessor init(IConfig props, List<? extends InterceptHandler> embeddedObservers, public ProtocolProcessor init(IConfig props, List<? extends InterceptHandler> embeddedObservers,
IAuthenticator authenticator, IAuthorizator authorizator, Server server) { IAuthenticator authenticator, IAuthorizatorPolicy authorizator, Server server) {
IMessagesStore messagesStore; IMessagesStore messagesStore;
LOG.info("Initializing messages and sessions stores..."); LOG.info("Initializing messages and sessions stores...");
String storageClassName = props.getProperty(BrokerConstants.STORAGE_CLASS_NAME, INMEMDB_STORE_CLASS); String storageClassName = props.getProperty(BrokerConstants.STORAGE_CLASS_NAME, INMEMDB_STORE_CLASS);
@@ -145,13 +145,13 @@ public class ProtocolProcessorBootstrapper {
LOG.info("Configuring MQTT authorizator..."); LOG.info("Configuring MQTT authorizator...");
String authorizatorClassName = props.getProperty(BrokerConstants.AUTHORIZATOR_CLASS_NAME, ""); String authorizatorClassName = props.getProperty(BrokerConstants.AUTHORIZATOR_CLASS_NAME, "");
if (authorizator == null && !authorizatorClassName.isEmpty()) { if (authorizator == null && !authorizatorClassName.isEmpty()) {
authorizator = loadClass(authorizatorClassName, IAuthorizator.class, IConfig.class, props); authorizator = loadClass(authorizatorClassName, IAuthorizatorPolicy.class, IConfig.class, props);
} }
if (authorizator == null) { if (authorizator == null) {
String aclFilePath = props.getProperty(BrokerConstants.ACL_FILE_PROPERTY_NAME, ""); String aclFilePath = props.getProperty(BrokerConstants.ACL_FILE_PROPERTY_NAME, "");
if (aclFilePath != null && !aclFilePath.isEmpty()) { if (aclFilePath != null && !aclFilePath.isEmpty()) {
authorizator = new DenyAllAuthorizator(); authorizator = new DenyAllAuthorizatorPolicy();
try { try {
LOG.info("Parsing ACL file. Path = {}", aclFilePath); LOG.info("Parsing ACL file. Path = {}", aclFilePath);
authorizator = ACLFileParser.parse(resourceLoader.loadResource(aclFilePath)); authorizator = ACLFileParser.parse(resourceLoader.loadResource(aclFilePath));
@@ -159,7 +159,7 @@ public class ProtocolProcessorBootstrapper {
LOG.error("Unable to parse ACL file. path=" + aclFilePath, pex); LOG.error("Unable to parse ACL file. path=" + aclFilePath, pex);
} }
} else { } else {
authorizator = new PermitAllAuthorizator(); authorizator = new PermitAllAuthorizatorPolicy();
} }
LOG.info("An {} authorizator instance will be used", authorizator.getClass().getName()); LOG.info("An {} authorizator instance will be used", authorizator.getClass().getName());
} }
@@ -19,7 +19,7 @@ package io.moquette.spi.impl;
import io.moquette.server.netty.NettyUtils; import io.moquette.server.netty.NettyUtils;
import io.moquette.spi.IMessagesStore; import io.moquette.spi.IMessagesStore;
import io.moquette.spi.impl.subscriptions.Topic; 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.channel.Channel;
import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -34,7 +34,7 @@ class Qos0PublishHandler extends QosPublishHandler {
private final BrokerInterceptor m_interceptor; private final BrokerInterceptor m_interceptor;
private final MessagesPublisher publisher; private final MessagesPublisher publisher;
public Qos0PublishHandler(IAuthorizator authorizator, IMessagesStore messagesStore, BrokerInterceptor interceptor, public Qos0PublishHandler(IAuthorizatorPolicy authorizator, IMessagesStore messagesStore, BrokerInterceptor interceptor,
MessagesPublisher messagesPublisher) { MessagesPublisher messagesPublisher) {
super(authorizator); super(authorizator);
this.m_messagesStore = messagesStore; this.m_messagesStore = messagesStore;
@@ -18,11 +18,10 @@ package io.moquette.spi.impl;
import io.moquette.connections.IConnectionsManager; import io.moquette.connections.IConnectionsManager;
import io.moquette.server.ConnectionDescriptor; import io.moquette.server.ConnectionDescriptor;
import io.moquette.server.ConnectionDescriptorStore;
import io.moquette.server.netty.NettyUtils; import io.moquette.server.netty.NettyUtils;
import io.moquette.spi.IMessagesStore; import io.moquette.spi.IMessagesStore;
import io.moquette.spi.impl.subscriptions.Topic; 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.channel.Channel;
import io.netty.handler.codec.mqtt.MqttFixedHeader; import io.netty.handler.codec.mqtt.MqttFixedHeader;
import io.netty.handler.codec.mqtt.MqttMessageType; import io.netty.handler.codec.mqtt.MqttMessageType;
@@ -47,7 +46,7 @@ class Qos1PublishHandler extends QosPublishHandler {
private final IConnectionsManager connectionDescriptors; private final IConnectionsManager connectionDescriptors;
private final MessagesPublisher publisher; private final MessagesPublisher publisher;
Qos1PublishHandler(IAuthorizator authorizator, IMessagesStore messagesStore, BrokerInterceptor interceptor, Qos1PublishHandler(IAuthorizatorPolicy authorizator, IMessagesStore messagesStore, BrokerInterceptor interceptor,
IConnectionsManager connectionDescriptors, MessagesPublisher messagesPublisher) { IConnectionsManager connectionDescriptors, MessagesPublisher messagesPublisher) {
super(authorizator); super(authorizator);
this.m_messagesStore = messagesStore; this.m_messagesStore = messagesStore;
@@ -18,14 +18,12 @@ package io.moquette.spi.impl;
import io.moquette.connections.IConnectionsManager; import io.moquette.connections.IConnectionsManager;
import io.moquette.server.ConnectionDescriptor; import io.moquette.server.ConnectionDescriptor;
import io.moquette.server.ConnectionDescriptorStore;
import io.moquette.server.netty.NettyUtils; import io.moquette.server.netty.NettyUtils;
import io.moquette.spi.ClientSession; import io.moquette.spi.ClientSession;
import io.moquette.spi.IMessagesStore; import io.moquette.spi.IMessagesStore;
import io.moquette.spi.ISessionsStore;
import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory; import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory;
import io.moquette.spi.impl.subscriptions.Topic; 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.channel.Channel;
import io.netty.handler.codec.mqtt.*; import io.netty.handler.codec.mqtt.*;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -50,7 +48,7 @@ class Qos2PublishHandler extends QosPublishHandler {
private final MessagesPublisher publisher; private final MessagesPublisher publisher;
private final SessionsRepository sessionsRepository; private final SessionsRepository sessionsRepository;
Qos2PublishHandler(IAuthorizator authorizator, ISubscriptionsDirectory subscriptions, Qos2PublishHandler(IAuthorizatorPolicy authorizator, ISubscriptionsDirectory subscriptions,
IMessagesStore messagesStore, BrokerInterceptor interceptor, IMessagesStore messagesStore, BrokerInterceptor interceptor,
IConnectionsManager connectionDescriptors, IConnectionsManager connectionDescriptors,
MessagesPublisher messagesPublisher, SessionsRepository sessionsRepository) { MessagesPublisher messagesPublisher, SessionsRepository sessionsRepository) {
@@ -20,16 +20,16 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import io.moquette.server.netty.NettyUtils; import io.moquette.server.netty.NettyUtils;
import io.moquette.spi.impl.subscriptions.Topic; 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.channel.Channel;
abstract class QosPublishHandler { abstract class QosPublishHandler {
private static final Logger LOG = LoggerFactory.getLogger(QosPublishHandler.class); 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; this.m_authorizator = m_authorizator;
} }
@@ -17,7 +17,7 @@
package io.moquette.spi.impl.security; package io.moquette.spi.impl.security;
import io.moquette.spi.impl.subscriptions.Topic; 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.text.ParseException;
import java.util.*; import java.util.*;
@@ -27,7 +27,7 @@ import java.util.*;
* *
* Not thread safe. * Not thread safe.
*/ */
class AuthorizationsCollector implements IAuthorizator { class AuthorizationsCollector implements IAuthorizatorPolicy {
private List<Authorization> m_globalAuthorizations = new ArrayList<>(); private List<Authorization> m_globalAuthorizations = new ArrayList<>();
private List<Authorization> m_patternAuthorizations = new ArrayList<>(); private List<Authorization> m_patternAuthorizations = new ArrayList<>();
@@ -17,9 +17,9 @@
package io.moquette.spi.impl.security; package io.moquette.spi.impl.security;
import io.moquette.spi.impl.subscriptions.Topic; 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 @Override
public boolean canWrite(Topic topic, String user, String client) { public boolean canWrite(Topic topic, String user, String client) {
@@ -17,9 +17,9 @@
package io.moquette.spi.impl.security; package io.moquette.spi.impl.security;
import io.moquette.spi.impl.subscriptions.Topic; 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 @Override
public boolean canWrite(Topic topic, String user, String client) { public boolean canWrite(Topic topic, String user, String client) {
@@ -15,6 +15,8 @@
*/ */
package io.moquette.spi.impl.subscriptions; package io.moquette.spi.impl.subscriptions;
import io.netty.handler.codec.mqtt.MqttQoS;
import java.util.*; import java.util.*;
class CNode { class CNode {
@@ -79,8 +81,8 @@ class CNode {
this.children.remove(node); this.children.remove(node);
} }
CNode addSubscription(String clientId, Topic topic) { CNode addSubscription(Subscription newSubscription) {
this.subscriptions.add(new Subscription(clientId, topic)); this.subscriptions.add(new Subscription(newSubscription));
return this; return this;
} }
@@ -98,9 +98,45 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
* to use fo searching matching subscriptions. * to use fo searching matching subscriptions.
* @return the list of matching subscriptions, or empty if not matching. * @return the list of matching subscriptions, or empty if not matching.
*/ */
Set<Subscription> match(Topic topic) { @Override
final Set<Subscription> matchingSubs = recursiveMatch(topic, this.root); public Set<Subscription> matchWithoutQosSharpening(Topic topic) {
return recursiveMatch(topic, this.root);
}
@Override
public Set<Subscription> matchQosSharpening(Topic topic) {
final Set<Subscription> subscriptions = matchWithoutQosSharpening(topic);
Map<String, Subscription> 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<Subscription> match(Topic topic) {
final Set<Subscription> matchingSubs = matchWithoutQosSharpening(topic);
// remove the overlapping subscriptions, selecting ones with greatest qos // remove the overlapping subscriptions, selecting ones with greatest qos
Map<String, Subscription> subsForClient = loadSubscriptionsFromStorage(matchingSubs);
return new HashSet<>(subsForClient.values());
}
@Deprecated
private Map<String, Subscription> loadSubscriptionsFromStorage(Set<Subscription> matchingSubs) {
Map<String, Subscription> subsForClient = new HashMap<>(); Map<String, Subscription> subsForClient = new HashMap<>();
for (Subscription matchingSub : matchingSubs) { for (Subscription matchingSub : matchingSubs) {
Subscription existingSub = subsForClient.get(matchingSub.clientId); Subscription existingSub = subsForClient.get(matchingSub.clientId);
@@ -120,7 +156,7 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
subsForClient.put(sub.clientId, sub); subsForClient.put(sub.clientId, sub);
} }
} }
return new HashSet<>(subsForClient.values()); return subsForClient;
} }
Set<Subscription> recursiveMatch(Topic topic, INode inode) { Set<Subscription> recursiveMatch(Topic topic, INode inode) {
@@ -171,28 +207,28 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
public void add(Subscription newSubscription) { public void add(Subscription newSubscription) {
Action res; Action res;
do { do {
res = insert(newSubscription.clientId, newSubscription.topicFilter, this.root, newSubscription.topicFilter); res = insert(newSubscription.topicFilter, this.root, newSubscription);
} while (res == Action.REPEAT); } 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(); Token token = topic.headToken();
if (!topic.isEmpty() && inode.mainNode().anyChildrenMatch(token)) { if (!topic.isEmpty() && inode.mainNode().anyChildrenMatch(token)) {
Topic remainingTopic = topic.exceptHeadToken(); Topic remainingTopic = topic.exceptHeadToken();
INode nextInode = inode.mainNode().childOf(token); INode nextInode = inode.mainNode().childOf(token);
return insert(clientId, remainingTopic, nextInode, fullpath); return insert(remainingTopic, nextInode, newSubscription);
} else { } else {
if (topic.isEmpty()) { if (topic.isEmpty()) {
return insertSubscription(clientId, fullpath, inode); return insertSubscription(inode, newSubscription);
} else { } 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 cnode = inode.mainNode();
CNode updatedCnode = cnode.copy().addSubscription(clientId, topic); CNode updatedCnode = cnode.copy().addSubscription(newSubscription);
if (inode.compareAndSet(cnode, updatedCnode)) { if (inode.compareAndSet(cnode, updatedCnode)) {
return Action.OK; return Action.OK;
} else { } else {
@@ -200,8 +236,8 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
} }
} }
private Action createNodeAndInsertSubscription(String clientId, Topic topic, INode inode, Topic fullpath) { private Action createNodeAndInsertSubscription(Topic topic, INode inode, Subscription newSubscription) {
INode newInode = createPathRec(clientId, topic, fullpath); INode newInode = createPathRec(topic, newSubscription);
CNode cnode = inode.mainNode(); CNode cnode = inode.mainNode();
CNode updatedCnode = cnode.copy(); CNode updatedCnode = cnode.copy();
updatedCnode.add(newInode); updatedCnode.add(newInode);
@@ -209,24 +245,24 @@ public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
return inode.compareAndSet(cnode, updatedCnode) ? Action.OK : Action.REPEAT; 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(); CNode newLeafCnode = new CNode();
newLeafCnode.token = token; newLeafCnode.token = token;
newLeafCnode.addSubscription(clientId, fullpath); newLeafCnode.addSubscription(newSubscription);
return new INode(newLeafCnode); return new INode(newLeafCnode);
} }
private INode createPathRec(String clientId, Topic topic, Topic fullpath) { private INode createPathRec(Topic topic, Subscription newSubscription) {
Topic remainingTopic = topic.exceptHeadToken(); Topic remainingTopic = topic.exceptHeadToken();
if (!remainingTopic.isEmpty()) { if (!remainingTopic.isEmpty()) {
INode inode = createPathRec(clientId, remainingTopic, fullpath); INode inode = createPathRec(remainingTopic, newSubscription);
CNode cnode = new CNode(); CNode cnode = new CNode();
cnode.token = topic.headToken(); cnode.token = topic.headToken();
cnode.add(inode); cnode.add(inode);
return new INode(cnode); return new INode(cnode);
} else { } else {
return createLeafNodes(clientId, fullpath, topic.headToken()); return createLeafNodes(topic.headToken(), newSubscription);
} }
} }
@@ -18,11 +18,16 @@ package io.moquette.spi.impl.subscriptions;
import io.moquette.spi.impl.SessionsRepository; import io.moquette.spi.impl.SessionsRepository;
import java.util.List; import java.util.List;
import java.util.Set;
public interface ISubscriptionsDirectory { public interface ISubscriptionsDirectory {
void init(SessionsRepository sessionsRepository); void init(SessionsRepository sessionsRepository);
Set<Subscription> matchWithoutQosSharpening(Topic topic);
Set<Subscription> matchQosSharpening(Topic topic);
void add(Subscription newSubscription); void add(Subscription newSubscription);
void removeSubscription(Topic topic, String clientID); void removeSubscription(Topic topic, String clientID);
@@ -33,7 +33,7 @@ class TNode extends CNode {
} }
@Override @Override
CNode addSubscription(String clientId, Topic topic) { CNode addSubscription(Subscription newSubscription) {
throw new IllegalStateException("Can't be invoked on TNode"); throw new IllegalStateException("Can't be invoked on TNode");
} }
@@ -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 * 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. * 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. * Ask the implementation of the authorizator if the topic can be used in a publish.
@@ -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();
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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<Subscription> 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);
}
}
@@ -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<Subscription> 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<String> 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());
}
}
@@ -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<Subscription> 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<Subscription> 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<Integer> 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));
}
}
@@ -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<Subscription> 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);
}
}
@@ -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());
}
}
@@ -24,14 +24,14 @@ import io.moquette.persistence.MemoryMessagesStore;
import io.moquette.persistence.MemoryStorageService; import io.moquette.persistence.MemoryStorageService;
import io.moquette.persistence.MemorySessionStore; import io.moquette.persistence.MemorySessionStore;
import io.moquette.spi.security.IAuthenticator; 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.After;
import org.junit.Test; import org.junit.Test;
import java.io.IOException; import java.io.IOException;
import java.util.Properties; import java.util.Properties;
import static org.junit.Assert.*; import static org.junit.Assert.*;
public class ConfigurationClassLoaderTest implements IAuthenticator, IAuthorizator { public class ConfigurationClassLoaderTest implements IAuthenticator, IAuthorizatorPolicy {
Server m_server; Server m_server;
IConfig m_config; IConfig m_config;
@@ -21,7 +21,7 @@ import io.moquette.server.config.IConfig;
import io.moquette.server.config.MemoryConfig; import io.moquette.server.config.MemoryConfig;
import io.moquette.spi.impl.security.AcceptAllAuthenticator; import io.moquette.spi.impl.security.AcceptAllAuthenticator;
import io.moquette.spi.impl.subscriptions.Topic; 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.buffer.Unpooled;
import io.netty.handler.codec.mqtt.MqttMessageBuilders; import io.netty.handler.codec.mqtt.MqttMessageBuilders;
import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage;
@@ -71,7 +71,7 @@ public class ServerIntegrationPahoCanPublishOnReadBlockedTopicTest {
m_config = new MemoryConfig(configProps); m_config = new MemoryConfig(configProps);
canRead = true; canRead = true;
final IAuthorizator switchingAuthorizator = new IAuthorizator() { final IAuthorizatorPolicy switchingAuthorizator = new IAuthorizatorPolicy() {
// int callCount = 0; // int callCount = 0;
@Override @Override
public boolean canWrite(Topic topic, String user, String client) { public boolean canWrite(Topic topic, String user, String client) {
@@ -15,14 +15,12 @@
*/ */
package io.moquette.spi.impl; package io.moquette.spi.impl;
import io.moquette.interception.InterceptHandler;
import io.moquette.persistence.MemoryStorageService; import io.moquette.persistence.MemoryStorageService;
import io.moquette.server.netty.NettyUtils; import io.moquette.server.netty.NettyUtils;
import io.moquette.spi.IMessagesStore; import io.moquette.spi.IMessagesStore;
import io.moquette.spi.ISessionsStore; 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.impl.subscriptions.*;
import io.moquette.spi.security.IAuthorizator;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel; import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.mqtt.*; import io.netty.handler.codec.mqtt.*;
@@ -81,7 +79,7 @@ abstract class AbstractProtocolProcessorCommonUtils {
subscriptions.init(sessionsRepository); subscriptions.init(sessionsRepository);
m_processor = new ProtocolProcessor(); m_processor = new ProtocolProcessor();
m_processor.init(subscriptions, m_messagesStore, m_sessionStore, m_mockAuthenticator, true, 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() { void verifyNoPublishIsReceived() {
@@ -126,14 +124,14 @@ abstract class AbstractProtocolProcessorCommonUtils {
assertEquals(expectedSubscription, subscription); assertEquals(expectedSubscription, subscription);
} }
protected MqttSubAckMessage subscribeWithoutVerify(String topic, MqttQoS desiredQos) { // protected MqttSubAckMessage subscribeWithoutVerify(String topic, MqttQoS desiredQos) {
MqttSubscribeMessage subscribe = MqttMessageBuilders.subscribe() // MqttSubscribeMessage subscribe = MqttMessageBuilders.subscribe()
.addSubscription(desiredQos, topic) // .addSubscription(desiredQos, topic)
.messageId(1) // .messageId(1)
.build(); // .build();
this.m_processor.processSubscribe(m_channel, subscribe); // this.m_processor.processSubscribe(m_channel, subscribe);
return m_channel.readOutbound(); // return m_channel.readOutbound();
} // }
protected void subscribeAndNotReadResponse(String topic, MqttQoS desiredQos) { protected void subscribeAndNotReadResponse(String topic, MqttQoS desiredQos) {
MqttSubscribeMessage subscribe = MqttMessageBuilders.subscribe() MqttSubscribeMessage subscribe = MqttMessageBuilders.subscribe()
@@ -143,16 +141,6 @@ abstract class AbstractProtocolProcessorCommonUtils {
this.m_processor.processSubscribe(m_channel, subscribe); 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) { protected void unsubscribeAndVerifyAck(String topic) {
final int messageId = 1; final int messageId = 1;
MqttUnsubscribeMessage msg = MqttMessageBuilders.unsubscribe() MqttUnsubscribeMessage msg = MqttMessageBuilders.unsubscribe()
@@ -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());
}
}
@@ -25,12 +25,12 @@ import static java.nio.charset.StandardCharsets.UTF_8;
/** /**
* Test utility to implements authenticator instance. * Test utility to implements authenticator instance.
*/ */
class MockAuthenticator implements IAuthenticator { public class MockAuthenticator implements IAuthenticator {
private Set<String> m_clientIds; private Set<String> m_clientIds;
private Map<String, String> m_userPwds; private Map<String, String> m_userPwds;
MockAuthenticator(Set<String> clientIds, Map<String, String> userPwds) { public MockAuthenticator(Set<String> clientIds, Map<String, String> userPwds) {
m_clientIds = clientIds; m_clientIds = clientIds;
m_userPwds = userPwds; m_userPwds = userPwds;
} }
@@ -27,13 +27,13 @@ import static org.junit.Assert.assertTrue;
/** /**
* Some useful assertions used by Netty's EmbeddedChannel in tests. * 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); 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); assertTrue("connAck is not an instance of ConnAckMessage", connAck instanceof MqttConnAckMessage);
MqttConnAckMessage connAckMsg = (MqttConnAckMessage) connAck; MqttConnAckMessage connAckMsg = (MqttConnAckMessage) connAck;
@@ -43,12 +43,12 @@ final class NettyChannelAssertions {
assertEquals(msg, expectedCode, connAckMsg.variableHeader().connectReturnCode()); assertEquals(msg, expectedCode, connAckMsg.variableHeader().connectReturnCode());
} }
static void assertConnAckAccepted(EmbeddedChannel channel) { public static void assertConnAckAccepted(EmbeddedChannel channel) {
channel.flush(); channel.flush();
assertEqualsConnAck(CONNECTION_ACCEPTED, channel.readOutbound()); assertEqualsConnAck(CONNECTION_ACCEPTED, channel.readOutbound());
} }
static void assertEqualsSubAck(/* byte expectedCode, */ Object subAck) { public static void assertEqualsSubAck(/* byte expectedCode, */ Object subAck) {
assertTrue(subAck instanceof MqttSubAckMessage); assertTrue(subAck instanceof MqttSubAckMessage);
// SubAckMessage connAckMsg = (SubAckMessage) connAck; // SubAckMessage connAckMsg = (SubAckMessage) connAck;
// assertEquals(expectedCode, connAckMsg.getReturnCode()); // assertEquals(expectedCode, connAckMsg.getReturnCode());
@@ -16,17 +16,18 @@
package io.moquette.spi.impl; package io.moquette.spi.impl;
import io.moquette.broker.PostOfficePublishTest;
import io.moquette.interception.InterceptHandler; import io.moquette.interception.InterceptHandler;
import io.moquette.persistence.MemoryStorageService; import io.moquette.persistence.MemoryStorageService;
import io.moquette.server.netty.NettyUtils; import io.moquette.server.netty.NettyUtils;
import io.moquette.spi.IMessagesStore; import io.moquette.spi.IMessagesStore;
import io.moquette.spi.IMessagesStore.StoredMessage; 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.CTrieSubscriptionDirectory;
import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory; import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory;
import io.moquette.spi.impl.subscriptions.Subscription; import io.moquette.spi.impl.subscriptions.Subscription;
import io.moquette.spi.impl.subscriptions.Topic; 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.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel; import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.mqtt.*; import io.netty.handler.codec.mqtt.*;
@@ -69,201 +70,138 @@ public class ProtocolProcessorTest extends AbstractProtocolProcessorCommonUtils
initializeProcessorAndSubsystems(); initializeProcessorAndSubsystems();
} }
@Test //same test moved into PostOfficePublishTest
public void testPublishToItself() { // @Test
final Subscription subscription = new Subscription(FAKE_CLIENT_ID, new Topic(NEWS_TOPIC), AT_MOST_ONCE); // 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<Subscription> 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 //same test moved into PostOfficePublishTest
ISubscriptionsDirectory subs = new CTrieSubscriptionDirectory() { // @Test
// public void testPublishToMultipleSubscribers() {
@Override // final Subscription subscription = new Subscription(FAKE_CLIENT_ID, new Topic(NEWS_TOPIC), AT_MOST_ONCE);
public List<Subscription> matches(Topic topic) { // final Subscription subscriptionClient2 = new Subscription(
if (topic.toString().equals(NEWS_TOPIC)) { // FAKE_CLIENT_ID2,
return Collections.singletonList(subscription); // new Topic(NEWS_TOPIC),
} else { // AT_MOST_ONCE);
throw new IllegalArgumentException("Expected " + NEWS_TOPIC + " buf found " + topic); //
} // // subscriptions.matches(topic) redefine the method to return true
} // ISubscriptionsDirectory subs = new CTrieSubscriptionDirectory() {
}; //
// @Override
// simulate a connect that register a clientID to an IoSession // public List<Subscription> matches(Topic topic) {
MemoryStorageService storageService = new MemoryStorageService(null, null); // if (topic.toString().equals(NEWS_TOPIC)) {
SessionsRepository sessionsRepository = new SessionsRepository(storageService.sessionsStore(), null); // return Arrays.asList(subscription, subscriptionClient2);
subs.init(sessionsRepository); // } else {
m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizator(), // throw new IllegalArgumentException("Expected " + NEWS_TOPIC + " buf found " + topic);
NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false); // }
// }
connect_v3_1(); // };
//
// Exercise // // simulate a connect that register a clientID to an IoSession
publishToAs("FakeCLI", NEWS_TOPIC, AT_MOST_ONCE, false); // MemoryStorageService storageService = new MemoryStorageService(null, null);
// SessionsRepository sessionsRepository = new SessionsRepository(storageService.sessionsStore(), null);
// Verify // subs.init(sessionsRepository);
verifyPublishIsReceived(); // m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizatorPolicy(),
} // NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false);
//
@Test // EmbeddedChannel firstReceiverChannel = new EmbeddedChannel();
public void testPublishToMultipleSubscribers() { // MqttConnectMessage connectMessage = MqttMessageBuilders.connect().protocolVersion(MqttVersion.MQTT_3_1)
final Subscription subscription = new Subscription(FAKE_CLIENT_ID, new Topic(NEWS_TOPIC), AT_MOST_ONCE); // .clientId(FAKE_CLIENT_ID).cleanSession(true).build();
final Subscription subscriptionClient2 = new Subscription( // m_processor.processConnect(firstReceiverChannel, connectMessage);
FAKE_CLIENT_ID2, // assertConnAckAccepted(firstReceiverChannel);
new Topic(NEWS_TOPIC), //
AT_MOST_ONCE); // // connect the second fake subscriber
// EmbeddedChannel secondReceiverChannel = new EmbeddedChannel();
// subscriptions.matches(topic) redefine the method to return true // MqttConnectMessage connectMessage2 = MqttMessageBuilders.connect().protocolVersion(MqttVersion.MQTT_3_1)
ISubscriptionsDirectory subs = new CTrieSubscriptionDirectory() { // .clientId(FAKE_CLIENT_ID2).cleanSession(true).build();
// m_processor.processConnect(secondReceiverChannel, connectMessage2);
@Override // assertConnAckAccepted(secondReceiverChannel);
public List<Subscription> matches(Topic topic) { //
if (topic.toString().equals(NEWS_TOPIC)) { // // Exercise
return Arrays.asList(subscription, subscriptionClient2); // MqttPublishMessage msg = MqttMessageBuilders.publish().topicName(NEWS_TOPIC).qos(AT_MOST_ONCE)
} else { // .retained(false).payload(Unpooled.copiedBuffer("Hello".getBytes(UTF_8))).build();
throw new IllegalArgumentException("Expected " + NEWS_TOPIC + " buf found " + topic); // NettyUtils.userName(m_channel, "FakeCLI");
} // m_processor.processPublish(m_channel, msg);
} //
}; // // Verify
// firstReceiverChannel.flush();
// simulate a connect that register a clientID to an IoSession // MqttPublishMessage pub2FirstSubscriber = firstReceiverChannel.readOutbound();
MemoryStorageService storageService = new MemoryStorageService(null, null); // assertNotNull(pub2FirstSubscriber);
SessionsRepository sessionsRepository = new SessionsRepository(storageService.sessionsStore(), null); // String firstMessageContent = DebugUtils.payload2Str(pub2FirstSubscriber.payload());
subs.init(sessionsRepository); // assertEquals("Hello", firstMessageContent);
m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizator(), //
NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false); // secondReceiverChannel.flush();
// MqttPublishMessage pub2SecondSubscriber = secondReceiverChannel.readOutbound();
EmbeddedChannel firstReceiverChannel = new EmbeddedChannel(); // assertNotNull(pub2SecondSubscriber);
MqttConnectMessage connectMessage = MqttMessageBuilders.connect().protocolVersion(MqttVersion.MQTT_3_1) // String secondMessageContent = DebugUtils.payload2Str(pub2SecondSubscriber.payload());
.clientId(FAKE_CLIENT_ID).cleanSession(true).build(); // assertEquals("Hello", secondMessageContent);
m_processor.processConnect(firstReceiverChannel, connectMessage); // }
assertConnAckAccepted(firstReceiverChannel); //
// //TODO move this test on PostOfficeSubscribeTest
// connect the second fake subscriber // @Test
EmbeddedChannel secondReceiverChannel = new EmbeddedChannel(); // public void testPublishOfRetainedMessage_afterNewSubscription() throws Exception {
MqttConnectMessage connectMessage2 = MqttMessageBuilders.connect().protocolVersion(MqttVersion.MQTT_3_1) // // simulate a connect that register a clientID to an IoSession
.clientId(FAKE_CLIENT_ID2).cleanSession(true).build(); // final Subscription subscription =
m_processor.processConnect(secondReceiverChannel, connectMessage2); // new Subscription(PUBLISHER_ID, new Topic(NEWS_TOPIC), AT_MOST_ONCE);
assertConnAckAccepted(secondReceiverChannel); //
// // subscriptions.matches(topic) redefine the method to return true
// Exercise // ISubscriptionsDirectory subs = new CTrieSubscriptionDirectory() {
MqttPublishMessage msg = MqttMessageBuilders.publish().topicName(NEWS_TOPIC).qos(AT_MOST_ONCE) //
.retained(false).payload(Unpooled.copiedBuffer("Hello".getBytes(UTF_8))).build(); // @Override
NettyUtils.userName(m_channel, "FakeCLI"); // public List<Subscription> matches(Topic topic) {
m_processor.processPublish(m_channel, msg); // if (topic.toString().equals(NEWS_TOPIC)) {
// return Collections.singletonList(subscription);
// Verify // } else {
firstReceiverChannel.flush(); // throw new IllegalArgumentException("Expected " + NEWS_TOPIC + " buf found " + topic);
MqttPublishMessage pub2FirstSubscriber = firstReceiverChannel.readOutbound(); // }
assertNotNull(pub2FirstSubscriber); // }
String firstMessageContent = DebugUtils.payload2Str(pub2FirstSubscriber.payload()); // };
assertEquals("Hello", firstMessageContent); // MemoryStorageService storageService = new MemoryStorageService(null, null);
// SessionsRepository sessionsRepository = new SessionsRepository(storageService.sessionsStore(), null);
secondReceiverChannel.flush(); // subs.init(sessionsRepository);
MqttPublishMessage pub2SecondSubscriber = secondReceiverChannel.readOutbound(); //
assertNotNull(pub2SecondSubscriber); // // simulate a connect that register a clientID to an IoSession
String secondMessageContent = DebugUtils.payload2Str(pub2SecondSubscriber.payload()); // m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizatorPolicy(),
assertEquals("Hello", secondMessageContent); // NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false);
} // connect_v3_1_asClient(PUBLISHER_ID);
// publishToAs(PUBLISHER_ID, NEWS_TOPIC, AT_MOST_ONCE, true);
@Test // NettyUtils.cleanSession(m_channel, false);
public void testSubscribe() { //
connect(); // // Exercise
// subscribeAndNotReadResponse("#", AT_MOST_ONCE);
// Exercise & verify //
subscribe(NEWS_TOPIC, AT_MOST_ONCE); // // Verify
} // verifyPublishIsReceived();
// }
@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<Integer> 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<Subscription> 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();
}
@Test @Test
public void testRepublishAndConsumePersistedMessages_onReconnect() { public void testRepublishAndConsumePersistedMessages_onReconnect() {
@@ -276,7 +214,7 @@ public class ProtocolProcessorTest extends AbstractProtocolProcessorCommonUtils
retainedMessage.setClientID(PUBLISHER_ID); retainedMessage.setClientID(PUBLISHER_ID);
m_messagesStore.storeRetained(new Topic("/topic"), retainedMessage); 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); NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false);
connect_v3_1_asClient(PUBLISHER_ID); connect_v3_1_asClient(PUBLISHER_ID);
@@ -285,84 +223,85 @@ public class ProtocolProcessorTest extends AbstractProtocolProcessorCommonUtils
assertTrue(this.sessionsRepository.sessionForClient(PUBLISHER_ID).isEmptyQueue()); assertTrue(this.sessionsRepository.sessionForClient(PUBLISHER_ID).isEmptyQueue());
} }
@Test // Moved to PostOfficePublishTest
public void publishNoPublishToInactiveSession() { // @Test
// create an inactive session for Subscriber // public void publishNoPublishToInactiveSession() {
ISubscriptionsDirectory mockedSubscriptions = mock(ISubscriptionsDirectory.class); // // create an inactive session for Subscriber
Subscription inactiveSub = new Subscription("Subscriber", new Topic("/topic"), MqttQoS.AT_LEAST_ONCE); // ISubscriptionsDirectory mockedSubscriptions = mock(ISubscriptionsDirectory.class);
List<Subscription> inactiveSubscriptions = Collections.singletonList(inactiveSub); // Subscription inactiveSub = new Subscription("Subscriber", new Topic("/topic"), MqttQoS.AT_LEAST_ONCE);
when(mockedSubscriptions.matches(eq(new Topic("/topic")))).thenReturn(inactiveSubscriptions); // List<Subscription> inactiveSubscriptions = Collections.singletonList(inactiveSub);
m_processor = new ProtocolProcessor(); // when(mockedSubscriptions.matches(eq(new Topic("/topic")))).thenReturn(inactiveSubscriptions);
m_processor.init(mockedSubscriptions, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizator(), // m_processor = new ProtocolProcessor();
NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false); // m_processor.init(mockedSubscriptions, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizatorPolicy(),
// NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false);
m_processor.sessionsRepository.createNewSession("Subscriber", false); //
// m_processor.sessionsRepository.createNewSession("Subscriber", false);
// Exercise //
connectAsClient("Publisher"); // // Exercise
publishToAs("Publisher", "/topic", AT_MOST_ONCE, true); // connectAsClient("Publisher");
// publishToAs("Publisher", "/topic", AT_MOST_ONCE, true);
verifyNoPublishIsReceived(); //
} // verifyNoPublishIsReceived();
// }
/** //
* Verify that receiving a publish with retained message and with Q0S = 0 clean the existing // /**
* retained messages for that topic. // * Verify that receiving a publish with retained message and with Q0S = 0 clean the existing
*/ // * retained messages for that topic.
@Test // */
public void testCleanRetainedStoreAfterAQoS0AndRetainedTrue() { // @Test
// force a connect // public void testCleanRetainedStoreAfterAQoS0AndRetainedTrue() {
connect_v3_1_asClient("Publisher"); // // force a connect
// connect_v3_1_asClient("Publisher");
// prepare and existing retained store //
publishToAs("Publisher", NEWS_TOPIC, AT_LEAST_ONCE, 100, true); // // prepare and existing retained store
// publishToAs("Publisher", NEWS_TOPIC, AT_LEAST_ONCE, 100, true);
Collection<IMessagesStore.StoredMessage> messages = m_messagesStore //
.searchMatching(key -> key.match(new Topic(NEWS_TOPIC))); // Collection<IMessagesStore.StoredMessage> messages = m_messagesStore
assertFalse(messages.isEmpty()); // .searchMatching(key -> key.match(new Topic(NEWS_TOPIC)));
// assertFalse(messages.isEmpty());
// Exercise //
// send a message that clean the previous retained publish // // Exercise
publishToAs("Publisher", NEWS_TOPIC, AT_MOST_ONCE, true); // // 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))); // // Verify
assertTrue(messages.isEmpty()); // messages = m_messagesStore.searchMatching(key -> key.match(new Topic(NEWS_TOPIC)));
} // assertTrue(messages.isEmpty());
// }
@Test //
public void testLowerTheQosToTheRequestedBySubscription() { // @Test
Subscription subQos1 = new Subscription("Sub A", new Topic("a/b"), MqttQoS.AT_LEAST_ONCE); // public void testLowerTheQosToTheRequestedBySubscription() {
assertEquals(MqttQoS.AT_LEAST_ONCE, lowerQosToTheSubscriptionDesired(subQos1, EXACTLY_ONCE)); // 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)); // Subscription subQos2 = new Subscription("Sub B", new Topic("a/+"), EXACTLY_ONCE);
} // assertEquals(EXACTLY_ONCE, lowerQosToTheSubscriptionDesired(subQos2, EXACTLY_ONCE));
// }
@Test //
public void testSubscribeReceivePublishedMessageAtSubscriberQoSAndNotPublisherQoS() { // @Test
ISubscriptionsDirectory subs = new CTrieSubscriptionDirectory(); // public void testSubscribeReceivePublishedMessageAtSubscriberQoSAndNotPublisherQoS() {
MemoryStorageService storageService = new MemoryStorageService(null, null); // ISubscriptionsDirectory subs = new CTrieSubscriptionDirectory();
SessionsRepository sessionsRepository = new SessionsRepository(storageService.sessionsStore(), null); // MemoryStorageService storageService = new MemoryStorageService(null, null);
subs.init(sessionsRepository); // 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(), // // simulate a connect that register a clientID to an IoSession
NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false); // m_processor.init(subs, m_messagesStore, m_sessionStore, null, true, new PermitAllAuthorizatorPolicy(),
connect_v3_1_asClient(PUBLISHER_ID); // NO_OBSERVERS_INTERCEPTOR, this.sessionsRepository, false);
publishQoS2ToAs(this.m_channel, PUBLISHER_ID, NEWS_TOPIC, 1, true); // connect_v3_1_asClient(PUBLISHER_ID);
disconnect(); // publishQoS2ToAs(this.m_channel, PUBLISHER_ID, NEWS_TOPIC, 1, true);
// disconnect();
// connect from the subscriber //
EmbeddedChannel subscriberChannel = new EmbeddedChannel(); // // connect from the subscriber
NettyUtils.clientID(subscriberChannel, SUBSCRIBER_ID); // EmbeddedChannel subscriberChannel = new EmbeddedChannel();
NettyUtils.cleanSession(subscriberChannel, false); // NettyUtils.clientID(subscriberChannel, SUBSCRIBER_ID);
connectAsClient(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); // // 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); // // Verify the retained message arrives with QoS0 instead of QoS2
} // verifyPublishIsReceived(subscriberChannel, HELLO_WORLD_MQTT, MqttQoS.AT_MOST_ONCE);
// }
} }
@@ -20,7 +20,7 @@ import io.moquette.persistence.MemoryStorageService;
import io.moquette.server.netty.NettyUtils; import io.moquette.server.netty.NettyUtils;
import io.moquette.spi.IMessagesStore; import io.moquette.spi.IMessagesStore;
import io.moquette.spi.ISessionsStore; 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.CTrieSubscriptionDirectory;
import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory; import io.moquette.spi.impl.subscriptions.ISubscriptionsDirectory;
import io.moquette.spi.impl.subscriptions.Subscription; import io.moquette.spi.impl.subscriptions.Subscription;
@@ -60,7 +60,7 @@ public class ProtocolProcessor_CONNECT_Test {
subscriptions.init(sessionsRepository); subscriptions.init(sessionsRepository);
processor = new ProtocolProcessor(); processor = new ProtocolProcessor();
processor.init(subscriptions, m_messagesStore, sessionStore, mockAuthenticator, true, processor.init(subscriptions, m_messagesStore, sessionStore, mockAuthenticator, true,
new PermitAllAuthorizator(), NO_OBSERVERS_INTERCEPTOR, new PermitAllAuthorizatorPolicy(), NO_OBSERVERS_INTERCEPTOR,
new SessionsRepository(this.sessionStore, null), false); new SessionsRepository(this.sessionStore, null), false);
} }
@@ -16,9 +16,6 @@
package io.moquette.spi.impl; package io.moquette.spi.impl;
import io.moquette.server.netty.NettyUtils; 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.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.codec.mqtt.MqttQoS;
import org.junit.Before; import org.junit.Before;
@@ -31,331 +31,333 @@ public class ProtocolProcessor_VariousUseCases_Test extends AbstractProtocolProc
initializeProcessorAndSubsystems(); initializeProcessorAndSubsystems();
} }
@Test // @Test
public void testCleanSession_maintainClientSubscriptions() { // public void testCleanSession_maintainClientSubscriptions() {
connectAsClient(this.m_channel, "TestClient"); // connectAsClient(this.m_channel, "TestClient");
subscribe("/topic", AT_MOST_ONCE); // subscribe("/topic", AT_MOST_ONCE);
disconnect(); // disconnect();
//
// reconnect and publish // // reconnect and publish
EmbeddedChannel anotherChannel = new EmbeddedChannel(); // EmbeddedChannel anotherChannel = new EmbeddedChannel();
connectAsClient(anotherChannel, "TestClient"); // connectAsClient(anotherChannel, "TestClient");
//
publishToAs(anotherChannel, "TestClient", "/topic", AT_MOST_ONCE, false); // publishToAs(anotherChannel, "TestClient", "/topic", AT_MOST_ONCE, false);
//
verifyPublishIsReceived(anotherChannel); // verifyPublishIsReceived(anotherChannel);
} // }
//
@Ignore("At the end it's equal to testCleanSession_maintainClientSubscriptions") // @Ignore("At the end it's equal to testCleanSession_maintainClientSubscriptions")
@Test // @Test
public void testCleanSession_maintainClientSubscriptions_againstClientDestruction() { // public void testCleanSession_maintainClientSubscriptions_againstClientDestruction() {
connectAsClient("TestClient"); // connectAsClient("TestClient");
subscribe("/topic", AT_MOST_ONCE); // subscribe("/topic", AT_MOST_ONCE);
disconnect(); // disconnect();
//
this.m_channel = new EmbeddedChannel(); // this.m_channel = new EmbeddedChannel();
// reconnect and publish // // reconnect and publish
connectAsClient("TestClient"); // connectAsClient("TestClient");
//
publishToAs("TestClient", "/topic", AT_MOST_ONCE, false); // publishToAs("TestClient", "/topic", AT_MOST_ONCE, false);
//
verifyPublishIsReceived(); // verifyPublishIsReceived();
} // }
/** // /**
* Check that after a client has connected with clean session false, subscribed to some topic // * 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 // * and exited, if it reconnects with clean session true, the broker correctly cleanup every
* previous subscription // * previous subscription
*/ // */
@Test // @Test
public void testCleanSession_correctlyClientSubscriptions() { // public void testCleanSession_correctlyClientSubscriptions() {
connectAsClient("TestClient"); // connectAsClient("TestClient");
subscribe("/topic", AT_MOST_ONCE); // subscribe("/topic", AT_MOST_ONCE);
disconnect(); // disconnect();
//
this.m_channel = new EmbeddedChannel(); // this.m_channel = new EmbeddedChannel();
// reconnect and publish // // reconnect and publish
connectWithCleanSession("TestClient"); // connectWithCleanSession("TestClient");
//
publishToAs("TestClient", "/topic", AT_MOST_ONCE, false); // publishToAs("TestClient", "/topic", AT_MOST_ONCE, false);
//
verifyNoPublishIsReceived(); // verifyNoPublishIsReceived();
} // }
@Test // Duplicated, just verify retain works correctly
public void testRetain_maintainMessage_againstClientDestruction() { // @Test
connect(); // public void testRetain_maintainMessage_againstClientDestruction() {
publishToAs(FAKE_CLIENT_ID, "/topic", AT_LEAST_ONCE, 66, true); // connect();
disconnect(); // publishToAs(FAKE_CLIENT_ID, "/topic", AT_LEAST_ONCE, 66, true);
// disconnect();
// reconnect and publish //
this.m_channel = new EmbeddedChannel(); // // reconnect and publish
connect(); // this.m_channel = new EmbeddedChannel();
subscribe("/topic", AT_MOST_ONCE); // connect();
// subscribe("/topic", AT_MOST_ONCE);
verifyPublishIsReceived(); //
} // verifyPublishIsReceived();
// }
@Test
public void testUnsubscribe_do_not_notify_anymore_same_session() { // @Test
connect(); // public void testUnsubscribe_do_not_notify_anymore_same_session() {
subscribe("/topic", AT_MOST_ONCE); // connect();
publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, false); // subscribe("/topic", AT_MOST_ONCE);
verifyPublishIsReceived(); // publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, false);
// verifyPublishIsReceived();
unsubscribeAndVerifyAck("/topic"); //
publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, false); // unsubscribeAndVerifyAck("/topic");
verifyNoPublishIsReceived(); // publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, false);
} // verifyNoPublishIsReceived();
// }
@Test
public void testUnsubscribe_do_not_notify_anymore_new_session() { // @Test
connect(); // public void testUnsubscribe_do_not_notify_anymore_new_session() {
subscribe("/topic", AT_MOST_ONCE); // connect();
publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, false); // subscribe("/topic", AT_MOST_ONCE);
verifyPublishIsReceived(); // publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, false);
// verifyPublishIsReceived();
unsubscribeAndVerifyAck("/topic"); //
disconnect(); // unsubscribeAndVerifyAck("/topic");
// disconnect();
this.m_channel = new EmbeddedChannel(); //
connect(); // this.m_channel = new EmbeddedChannel();
publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, false); // connect();
verifyNoPublishIsReceived(); // publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, false);
} // verifyNoPublishIsReceived();
// }
@Test // Moved to PostOfficePublishTest
public void testPublishWithQoS1() { // @Test
connect(); // public void testPublishWithQoS1() {
subscribe("/topic", AT_LEAST_ONCE); // connect();
publishToAs(FAKE_CLIENT_ID, "/topic", AT_LEAST_ONCE, 66, false); // subscribe("/topic", AT_LEAST_ONCE);
disconnect(); // publishToAs(FAKE_CLIENT_ID, "/topic", AT_LEAST_ONCE, 66, false);
// disconnect();
verifyPublishIsReceived(AT_LEAST_ONCE); //
} // verifyPublishIsReceived(AT_LEAST_ONCE);
// }
@Test
public void testPublishWithQoS1_notCleanSession() { // Moved to PostOfficePublishTest.forwardQoS1PublishesWhenNotCleanSessionReconnects
connect(); // @Test
subscribe("/topic", AT_LEAST_ONCE); // public void testPublishWithQoS1_notCleanSession() {
disconnect(); // connect();
// subscribe("/topic", AT_LEAST_ONCE);
// publish a QoS 1 message another client publish a message on the topic // disconnect();
publishFromAnotherClient("/topic", AT_LEAST_ONCE); //
// // publish a QoS 1 message another client publish a message on the topic
this.m_channel = new EmbeddedChannel(); // publishFromAnotherClient("/topic", AT_LEAST_ONCE);
connect(); //
verifyPublishIsReceived(); // this.m_channel = new EmbeddedChannel();
} // connect();
// verifyPublishIsReceived();
private void publishFromAnotherClient(String topic, MqttQoS qos) { // }
EmbeddedChannel anotherChannel = new EmbeddedChannel();
connectAsClient(anotherChannel, "AnotherClient"); // private void publishFromAnotherClient(String topic, MqttQoS qos) {
publishToAs(anotherChannel, "AnotherClient", topic, qos, 67, false); // EmbeddedChannel anotherChannel = new EmbeddedChannel();
disconnect(anotherChannel); // 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"); // private void publishFromAnotherClient(String topic, String payload, MqttQoS qos) {
publishToAs(anotherChannel, "AnotherClient", topic, payload, qos, 68, false); // EmbeddedChannel anotherChannel = new EmbeddedChannel();
disconnect(anotherChannel); // 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"); // private void publishQoS2FromAnotherClient(String topic) {
publishQoS2ToAs(anotherChannel, "AnotherClient", topic, 67, false); // EmbeddedChannel anotherChannel = new EmbeddedChannel();
disconnect(anotherChannel); // connectAsClient(anotherChannel, "AnotherClient");
} // publishQoS2ToAs(anotherChannel, "AnotherClient", topic, 67, false);
// disconnect(anotherChannel);
@Test // }
public void checkReceivePublishedMessage_after_a_reconnect_with_notCleanSession() {
connect(); // @Test
subscribe("/topic", AT_LEAST_ONCE); // public void checkReceivePublishedMessage_after_a_reconnect_with_notCleanSession() {
disconnect(); // connect();
// subscribe("/topic", AT_LEAST_ONCE);
this.m_channel = new EmbeddedChannel(); // disconnect();
connect(); //
subscribe("/topic", AT_LEAST_ONCE); // this.m_channel = new EmbeddedChannel();
// connect();
// publish a QoS 1 message another client publish a message on the topic // subscribe("/topic", AT_LEAST_ONCE);
publishFromAnotherClient("/topic", AT_LEAST_ONCE); //
// // publish a QoS 1 message another client publish a message on the topic
// Verify that after a reconnection the client receive the message // publishFromAnotherClient("/topic", AT_LEAST_ONCE);
verifyPublishIsReceived(); //
} // // Verify that after a reconnection the client receive the message
// verifyPublishIsReceived();
@Test // }
public void testPublishWithQoS2() {
connect(); // @Test
subscribe("/topic", EXACTLY_ONCE); // public void testPublishWithQoS2() {
disconnect(); // connect();
// subscribe("/topic", EXACTLY_ONCE);
publishQoS2FromAnotherClient("/topic"); // disconnect();
//
this.m_channel = new EmbeddedChannel(); // publishQoS2FromAnotherClient("/topic");
connect(); //
// this.m_channel = new EmbeddedChannel();
verifyPublishIsReceived(EXACTLY_ONCE); // connect();
} //
// verifyPublishIsReceived(EXACTLY_ONCE);
@Test // }
public void avoidMultipleNotificationsAfterMultipleReconnection_cleanSessionFalseQoS1() { //
connect(); // @Test
subscribe("/topic", AT_LEAST_ONCE); // public void avoidMultipleNotificationsAfterMultipleReconnection_cleanSessionFalseQoS1() {
disconnect(); // connect();
// subscribe("/topic", AT_LEAST_ONCE);
publishFromAnotherClient("/topic", "Hello MQTT 1", AT_LEAST_ONCE); // disconnect();
//
this.m_channel = new EmbeddedChannel(); // publishFromAnotherClient("/topic", "Hello MQTT 1", AT_LEAST_ONCE);
connect(); //
verifyPublishIsReceived("Hello MQTT 1", AT_LEAST_ONCE); // this.m_channel = new EmbeddedChannel();
disconnect(); // connect();
// verifyPublishIsReceived("Hello MQTT 1", AT_LEAST_ONCE);
// publish other message // disconnect();
publishFromAnotherClient("/topic", "Hello MQTT 2", AT_LEAST_ONCE); //
// // publish other message
this.m_channel = new EmbeddedChannel(); // publishFromAnotherClient("/topic", "Hello MQTT 2", AT_LEAST_ONCE);
connect(); //
verifyPublishIsReceived("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(); // @Test
subscribe("/topic", AT_MOST_ONCE); // public void testConnectSubPub_cycle_getTimeout_on_second_disconnect_issue142() {
publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, true); // connect();
disconnect(); // subscribe("/topic", AT_MOST_ONCE);
// publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, true);
// second loop // disconnect();
this.m_channel = new EmbeddedChannel(); //
connect(); // // second loop
subscribe("/topic", AT_MOST_ONCE); // this.m_channel = new EmbeddedChannel();
publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, true); // connect();
disconnect(); // subscribe("/topic", AT_MOST_ONCE);
// publishToAs(FAKE_CLIENT_ID, "/topic", AT_MOST_ONCE, true);
assertFalse("after a disconnect the client should be disconnected", this.m_channel.isOpen()); // 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(); // @Test
connectAsClient(publisher, "Publisher"); // public void checkReplayofStoredPublishResumeAfter_a_disconnect_cleanSessionFalseQoS1() {
// EmbeddedChannel publisher = new EmbeddedChannel();
connect(); // connectAsClient(publisher, "Publisher");
subscribe("/topic", AT_LEAST_ONCE); //
// connect();
publishToAs(publisher, "Publisher", "/topic", "Hello world MQTT!!-1", AT_LEAST_ONCE, 99, false); // subscribe("/topic", AT_LEAST_ONCE);
verifyPublishIsReceived("Hello world MQTT!!-1", AT_LEAST_ONCE); //
disconnect(); // publishToAs(publisher, "Publisher", "/topic", "Hello world MQTT!!-1", AT_LEAST_ONCE, 99, false);
// verifyPublishIsReceived("Hello world MQTT!!-1", AT_LEAST_ONCE);
publishToAs(publisher, "Publisher", "/topic", "Hello world MQTT!!-2", AT_LEAST_ONCE, 100, false); // disconnect();
publishToAs(publisher, "Publisher", "/topic", "Hello world MQTT!!-3", AT_LEAST_ONCE, 101, false); //
// publishToAs(publisher, "Publisher", "/topic", "Hello world MQTT!!-2", AT_LEAST_ONCE, 100, false);
this.m_channel = new EmbeddedChannel(); // publishToAs(publisher, "Publisher", "/topic", "Hello world MQTT!!-3", AT_LEAST_ONCE, 101, false);
connect(); //
verifyPublishIsReceived("Hello world MQTT!!-2", AT_LEAST_ONCE); // this.m_channel = new EmbeddedChannel();
verifyPublishIsReceived("Hello world MQTT!!-3", AT_LEAST_ONCE); // 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(); // @Test
connectAsClient(publisher, "Publisher"); // public void checkReplayStoredPublish_forNoCleanSession_qos1() {
// EmbeddedChannel publisher = new EmbeddedChannel();
connect(); // connectAsClient(publisher, "Publisher");
subscribe("/topic", AT_LEAST_ONCE); //
disconnect(); // connect();
// subscribe("/topic", AT_LEAST_ONCE);
publishToAs(publisher, "Publisher", "/topic", "Hello world MQTT!!-1", AT_LEAST_ONCE, 99, false); // disconnect();
//
this.m_channel = new EmbeddedChannel(); // publishToAs(publisher, "Publisher", "/topic", "Hello world MQTT!!-1", AT_LEAST_ONCE, 99, false);
connect(); //
subscribeAndNotReadResponse("/topic", AT_LEAST_ONCE); // this.m_channel = new EmbeddedChannel();
verifyPublishIsReceived("Hello world MQTT!!-1", AT_LEAST_ONCE); // 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" // * subscriber connect and subscribe on "topic" subscriber disconnects publisher connects and
*/ // * send two message "hello1" "hello2" to "topic" subscriber connects again and receive "hello1"
@Test // * "hello2"
public void checkQoS2SubscriberDisconnectReceivePersistedPublishes() { // */
connect(); // @Test
subscribe("topic", EXACTLY_ONCE); // public void checkQoS2SubscriberDisconnectReceivePersistedPublishes() {
disconnect(); // connect();
// subscribe("topic", EXACTLY_ONCE);
EmbeddedChannel publisher = new EmbeddedChannel(); // disconnect();
connectAsClient(publisher, "Publisher"); //
publishQoS2ToAs(publisher, "Publisher", "topic", "Hello1", 101, true); // EmbeddedChannel publisher = new EmbeddedChannel();
publishQoS2ToAs(publisher, "Publisher", "topic", "Hello2", 102, true); // connectAsClient(publisher, "Publisher");
disconnect(publisher); // publishQoS2ToAs(publisher, "Publisher", "topic", "Hello1", 101, true);
// publishQoS2ToAs(publisher, "Publisher", "topic", "Hello2", 102, true);
this.m_channel = new EmbeddedChannel(); // disconnect(publisher);
connect(); //
verifyPublishIsReceived("Hello1", EXACTLY_ONCE); // this.m_channel = new EmbeddedChannel();
verifyPublishIsReceived("Hello2", EXACTLY_ONCE); // 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 // /**
*/ // * subscriber connect and subscribe on "a/b" QoS 1 and "a/+" QoS 2 publisher connects and send a
@Test // * message "hello" on "a/b" subscriber must receive only a single message not twice
public void checkSinglePublishOnOverlappingSubscriptions() { // */
EmbeddedChannel publisher = new EmbeddedChannel(); // @Test
connectAsClient(publisher, "Publisher"); // public void checkSinglePublishOnOverlappingSubscriptions() {
// EmbeddedChannel publisher = new EmbeddedChannel();
connect(); // connectAsClient(publisher, "Publisher");
subscribe("a/+", EXACTLY_ONCE); //
subscribe("a/b", AT_LEAST_ONCE); // connect();
// subscribe("a/+", EXACTLY_ONCE);
// force the publisher to send // subscribe("a/b", AT_LEAST_ONCE);
publishToAs(publisher, "Publisher", "a/b", "Hello world MQTT!!", AT_LEAST_ONCE, 60, false); //
// // force the publisher to send
verifyPublishIsReceived("Hello world MQTT!!", AT_LEAST_ONCE); // publishToAs(publisher, "Publisher", "a/b", "Hello world MQTT!!", AT_LEAST_ONCE, 60, false);
// try to listen a second publish //
verifyNoPublishIsReceived(); // verifyPublishIsReceived("Hello world MQTT!!", AT_LEAST_ONCE);
} // // try to listen a second publish
// verifyNoPublishIsReceived();
@Test // }
public void testForceClientDisconnection_issue116() {
EmbeddedChannel clientXA = new EmbeddedChannel(); // @Test
connectAsClient(clientXA, "subscriber"); // public void testForceClientDisconnection_issue116() {
subscribe(clientXA, "topic", AT_MOST_ONCE); // EmbeddedChannel clientXA = new EmbeddedChannel();
// connectAsClient(clientXA, "subscriber");
EmbeddedChannel clientXB = new EmbeddedChannel(); // subscribe(clientXA, "topic", AT_MOST_ONCE);
connectAsClient(clientXB, "publisher"); //
publishQoS2ToAs(clientXB, "publisher", "topic", "Hello", 20, false); // EmbeddedChannel clientXB = new EmbeddedChannel();
// connectAsClient(clientXB, "publisher");
EmbeddedChannel clientYA = new EmbeddedChannel(); // publishQoS2ToAs(clientXB, "publisher", "topic", "Hello", 20, false);
connectAsClient(clientYA, "subscriber"); //
subscribe(clientYA, "topic", AT_MOST_ONCE); // EmbeddedChannel clientYA = new EmbeddedChannel();
// connectAsClient(clientYA, "subscriber");
EmbeddedChannel clientYB = new EmbeddedChannel(); // subscribe(clientYA, "topic", AT_MOST_ONCE);
connectAsClient(clientYB, "publisher"); //
publishQoS2ToAs(clientYB, "publisher", "topic", "Hello 2", 20, true); // EmbeddedChannel clientYB = new EmbeddedChannel();
// connectAsClient(clientYB, "publisher");
assertFalse("First 'subscriber' channel MUST be closed by the broker", clientXA.isOpen()); // publishQoS2ToAs(clientYB, "publisher", "topic", "Hello 2", 20, true);
verifyPublishIsReceived(clientYA, "Hello 2", AT_MOST_ONCE); //
} // 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(); // @Test
connectAsClient(subscriber, "Subscriber"); // public void testPublishToEmptyTopic() {
// EmbeddedChannel subscriber = new EmbeddedChannel();
subscribe(subscriber, "#", AT_LEAST_ONCE); // connectAsClient(subscriber, "Subscriber");
//
// Exercise // subscribe(subscriber, "#", AT_LEAST_ONCE);
connect(); //
publishToAs(FAKE_CLIENT_ID, "", AT_LEAST_ONCE, 1, false); // // Exercise
// connect();
// Verify // publishToAs(FAKE_CLIENT_ID, "", AT_LEAST_ONCE, 1, false);
verifyNoPublishIsReceived(subscriber); //
assertFalse("On empty topic subscription the channel MUST be closed", m_channel.isOpen()); // // Verify
} // verifyNoPublishIsReceived(subscriber);
// assertFalse("On empty topic subscription the channel MUST be closed", m_channel.isOpen());
// }
} }
+6 -2
View File
@@ -1,7 +1,8 @@
# Set root logger level to DEBUG and its only appender to A1.
log4j.rootLogger=ERROR, stdout, file log4j.rootLogger=ERROR, stdout, file
log4j.logger.io.moquette=WARN 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 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.interception.HazelcastInterceptHandler=WARN
log4j.logger.io.moquette.server.HazelcastListener=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=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Threshold=INFO log4j.appender.stdout.Threshold=INFO
# only for test log4j.appender.stdout.Threshold=TRACE
# for debug trace # for debug trace
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
#log4j.appender.stdout.layout.ConversionPattern=%-4r [%t] %-5p %c{1} %x - %m%n #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 #file appender
log4j.appender.file=org.apache.log4j.RollingFileAppender log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.Threshold=INFO log4j.appender.file.Threshold=INFO
# only for tests log4j.appender.file.Threshold=TRACE
log4j.appender.file.File=moquette.log log4j.appender.file.File=moquette.log
log4j.appender.file.MaxFileSize=100MB log4j.appender.file.MaxFileSize=100MB
log4j.appender.file.MaxBackupIndex=1 log4j.appender.file.MaxBackupIndex=1
+8
View File
@@ -1,9 +1,17 @@
description = 'Moquette - performance tests' description = 'Moquette - performance tests'
repositories {
jcenter()
mavenCentral()
maven { url 'https://oss.jfrog.org/artifactory/oss-snapshot-local' }
}
dependencies { dependencies {
compile project(':moquette-broker') compile project(':moquette-broker')
compile group: 'io.netty', name: 'netty-codec-mqtt', version:"${nettyVersion}" compile group: 'io.netty', name: 'netty-codec-mqtt', version:"${nettyVersion}"
compile group: 'org.hdrhistogram', name: 'HdrHistogram', version:'2.1.2' 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.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.eclipse.jetty.toolchain', name: 'jetty-perf-helper', version:'1.0.5'
compile(group: 'org.mqttbee', name: 'mqtt-bee', version: '1.0.0-SNAPSHOT')
} }
@@ -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<Mqtt3ConnAck> 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<Mqtt3SubAck, Mqtt3Publish> 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<Mqtt3ConnAck> connectScenario = connAckSingle
// .doOnSuccess(connAck -> System.out.println("Connected with return code " + connAck.getReturnCode()))
// .doOnError(throwable -> System.out.println("Connection failed, " + throwable.getMessage()));
//
// final Flowable<Mqtt3Publish> 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<Mqtt3Publish> 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<Mqtt3Publish> 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<Mqtt3ConnAck> connectScenario = connAckSingle
.doOnSuccess(connAck -> System.out.println("Connected with return code " + connAck.getReturnCode()))
.doOnError(throwable -> System.out.println("Connection failed, " + throwable.getMessage()));
final Flowable<Mqtt3PublishResult> publishScenario = client.publish(messagesToPublish)
.doOnNext(publishResult -> System.out.println("Publish acknowledged: " + new String(publishResult.getPublish().getPayloadAsBytes())));
connectScenario.toCompletable().andThen(publishScenario).blockingSubscribe();
}
}
@@ -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 <host> <numMessages>");
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"}
}
}
@@ -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<Long, Long> 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.
}
}
+8 -4
View File
@@ -1,14 +1,16 @@
//@GrabResolver(name='Paho', root='https://repo.eclipse.org/content/repositories/paho-snapshots/') //@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') //@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/') @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.MqttAsyncClient
import org.eclipse.paho.client.mqttv3.MqttMessage 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.MqttCallback
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken
import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence
//import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import static groovy.json.JsonOutput.* import static groovy.json.JsonOutput.*
import java.nio.ByteBuffer import java.nio.ByteBuffer
@@ -71,7 +73,7 @@ def publishQos0(client, long numMessages) {
print "publishing.." print "publishing.."
long startTime = System.currentTimeMillis() long startTime = System.currentTimeMillis()
(1..numMessages).each { (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('topic', bytes, 0, false)
} }
client.publish('/exit', 'Exit'.bytes, 0, false) client.publish('/exit', 'Exit'.bytes, 0, false)
@@ -82,6 +84,7 @@ def publishQos0(client, long numMessages) {
if (args.size() < 2) { if (args.size() < 2) {
println "Usage benchmarker <host> <numMessages>" println "Usage benchmarker <host> <numMessages>"
println "groovy benchmarker.groovy localhost 1000 500 test1"
return return
} }
@@ -89,12 +92,13 @@ String host = args[0]
long numMessages = args[1] as long long numMessages = args[1] as long
String tmpDir = System.getProperty("java.io.tmpdir") String tmpDir = System.getProperty("java.io.tmpdir")
MqttDefaultFilePersistence dataStore = new MqttDefaultFilePersistence(tmpDir) MqttDefaultFilePersistence dataStore = new MqttDefaultFilePersistence(tmpDir)
//MemoryPersistence dataStore = new MemoryPersistence()
int rnd = (Math.random() * 100) as int int rnd = (Math.random() * 100) as int
MqttAsyncClient client = new MqttAsyncClient("tcp://${host}:1883", "SubscriberClient${rnd}", dataStore) MqttAsyncClient client = new MqttAsyncClient("tcp://${host}:1883", "SubscriberClient${rnd}", dataStore)
def callback = new SubscriberCallback() def callback = new SubscriberCallback()
client.callback = callback client.callback = callback
client.connect().waitForCompletion(1000); client.connect().waitForCompletion(1000)
client.subscribe("topic", 0) client.subscribe("topic", 0)
println "subscribed to topic" println "subscribed to topic"
client.subscribe("/exit", 0) client.subscribe("/exit", 0)
@@ -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: <host> <local client name>"
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)
@@ -13,7 +13,7 @@ import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence
* */ * */
if (args.size() != 3) { if (args.size() != 3) {
println "usage: <host> <num_serial_connctions> <local client name>" println "usage: <host> <num_serial_connections> <local client name>"
return return
} }
@@ -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"