ARTEMIS-3638 Support MQTT 5

MQTT 5 is an OASIS standard which debuted in March 2019. It boasts
numerous improvments over its predecessor (i.e. MQTT 3.1.1) which will
benefit users. These improvements are summarized in the specification
at:
https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901293

The specification describes all the behavior necessary for a client or
server to conform. The spec is highlighted with special "normative"
conformance statements which distill the descriptions into concise
terms. The specification provides a helpful summary of all these
statements. See:
https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901292

This commit implements all of the mandatory elements from the
specification and provides tests which are identified using the
corresponding normative conformance statement. All normative
conformance statements either have an explicit test or are noted in
comments with an explanation of why an explicit test doesn't exist. See
org.apache.activemq.artemis.tests.integration.mqtt5 for all those
details.

This commit also includes documentation about how to configure
everything related to the new MQTT 5 features.
This commit is contained in:
Justin Bertram 2022-01-11 18:58:28 -06:00 committed by clebertsuconic
parent 605079d4ba
commit 8063110644
107 changed files with 9493 additions and 612 deletions

View File

@ -344,6 +344,7 @@ public final class UTF8Util {
} }
} }
// TODO look at replacing this with io.netty.buffer.ByteBufUtil.utf8Bytes(java.lang.CharSequence)
public static int calculateUTFSize(final String str) { public static int calculateUTFSize(final String str) {
int calculatedLen = 0; int calculatedLen = 0;
for (int i = 0, stringLength = str.length(); i < stringLength; i++) { for (int i = 0, stringLength = str.length(); i < stringLength; i++) {

View File

@ -138,7 +138,7 @@ public class Wait {
assertTrue(failureMessage, condition, duration, SLEEP_MILLIS); assertTrue(failureMessage, condition, duration, SLEEP_MILLIS);
} }
public static void assertTrue(Condition condition, final long duration, final long sleep) throws Exception { public static void assertTrue(Condition condition, final long duration, final long sleep) {
assertTrue(DEFAULT_FAILURE_MESSAGE, condition, duration, sleep); assertTrue(DEFAULT_FAILURE_MESSAGE, condition, duration, sleep);
} }

View File

@ -644,6 +644,9 @@ public final class ActiveMQDefaultConfiguration {
// Whether or not to report Netty pool metrics // Whether or not to report Netty pool metrics
private static final boolean DEFAULT_NETTY_POOL_METRICS = false; private static final boolean DEFAULT_NETTY_POOL_METRICS = false;
// How often (in ms) to scan for expired MQTT sessions
private static long DEFAULT_MQTT_SESSION_SCAN_INTERVAL = 500;
/** /**
* If true then the ActiveMQ Artemis Server will make use of any Protocol Managers that are in available on the classpath. If false then only the core protocol will be available, unless in Embedded mode where users can inject their own Protocol Managers. * If true then the ActiveMQ Artemis Server will make use of any Protocol Managers that are in available on the classpath. If false then only the core protocol will be available, unless in Embedded mode where users can inject their own Protocol Managers.
*/ */
@ -1762,4 +1765,11 @@ public final class ActiveMQDefaultConfiguration {
public static Boolean getDefaultNettyPoolMetrics() { public static Boolean getDefaultNettyPoolMetrics() {
return DEFAULT_NETTY_POOL_METRICS; return DEFAULT_NETTY_POOL_METRICS;
} }
/**
* How often (in ms) to scan for expired MQTT sessions
*/
public static long getMqttSessionScanInterval() {
return DEFAULT_MQTT_SESSION_SCAN_INTERVAL;
}
} }

View File

@ -252,11 +252,6 @@ public class ProtonProtocolManager extends AbstractProtocolManager<AMQPMessage,
return entry; return entry;
} }
@Override
public void removeHandler(String name) {
}
@Override @Override
public void handleBuffer(RemotingConnection connection, ActiveMQBuffer buffer) { public void handleBuffer(RemotingConnection connection, ActiveMQBuffer buffer) {
ActiveMQProtonRemotingConnection protonConnection = (ActiveMQProtonRemotingConnection) connection; ActiveMQProtonRemotingConnection protonConnection = (ActiveMQProtonRemotingConnection) connection;

View File

@ -104,6 +104,11 @@
<artifactId>org.eclipse.paho.client.mqttv3</artifactId> <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.mqttv5.client</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>com.github.javafaker</groupId> <groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId> <artifactId>javafaker</artifactId>

View File

@ -52,8 +52,13 @@ public class MQTTConnection implements RemotingConnection {
private final List<FailureListener> failureListeners = new CopyOnWriteArrayList<>(); private final List<FailureListener> failureListeners = new CopyOnWriteArrayList<>();
private final List<CloseListener> closeListeners = new CopyOnWriteArrayList<>(); private final List<CloseListener> closeListeners = new CopyOnWriteArrayList<>();
private Subject subject; private Subject subject;
private int receiveMaximum = -1;
private String protocolVersion;
public MQTTConnection(Connection transportConnection) throws Exception { public MQTTConnection(Connection transportConnection) throws Exception {
this.transportConnection = transportConnection; this.transportConnection = transportConnection;
this.creationTime = System.currentTimeMillis(); this.creationTime = System.currentTimeMillis();
@ -189,7 +194,6 @@ public class MQTTConnection implements RemotingConnection {
@Override @Override
public void destroy() { public void destroy() {
//TODO(mtaylor) ensure this properly destroys this connection.
destroyed = true; destroyed = true;
disconnect(false); disconnect(false);
} }
@ -282,7 +286,7 @@ public class MQTTConnection implements RemotingConnection {
*/ */
@Override @Override
public String getProtocolName() { public String getProtocolName() {
return MQTTProtocolManagerFactory.MQTT_PROTOCOL_NAME; return MQTTProtocolManagerFactory.MQTT_PROTOCOL_NAME + (protocolVersion != null ? protocolVersion : "");
} }
/** /**
@ -310,4 +314,15 @@ public class MQTTConnection implements RemotingConnection {
return getTransportConnection().getLocalAddress(); return getTransportConnection().getLocalAddress();
} }
public int getReceiveMaximum() {
return receiveMaximum;
}
public void setReceiveMaximum(int maxReceive) {
this.receiveMaximum = maxReceive;
}
public void setProtocolVersion(String protocolVersion) {
this.protocolVersion = protocolVersion;
}
} }

View File

@ -19,15 +19,27 @@ package org.apache.activemq.artemis.core.protocol.mqtt;
import java.util.UUID; import java.util.UUID;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.codec.mqtt.MqttConnectReturnCode; import io.netty.handler.codec.mqtt.MqttConnectMessage;
import io.netty.handler.codec.mqtt.MqttProperties; import io.netty.handler.codec.mqtt.MqttProperties;
import io.netty.handler.codec.mqtt.MqttVersion;
import io.netty.util.CharsetUtil;
import org.apache.activemq.artemis.api.core.Pair;
import org.apache.activemq.artemis.api.core.client.ActiveMQClient; import org.apache.activemq.artemis.api.core.client.ActiveMQClient;
import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.core.server.ServerSession; import org.apache.activemq.artemis.core.server.ServerSession;
import org.apache.activemq.artemis.core.server.impl.ServerSessionImpl; import org.apache.activemq.artemis.core.server.impl.ServerSessionImpl;
import org.apache.activemq.artemis.utils.UUIDGenerator; import org.apache.activemq.artemis.utils.UUIDGenerator;
import org.jboss.logging.Logger;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.ASSIGNED_CLIENT_IDENTIFIER;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.AUTHENTICATION_METHOD;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.MAXIMUM_PACKET_SIZE;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.RECEIVE_MAXIMUM;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.SERVER_KEEP_ALIVE;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.SESSION_EXPIRY_INTERVAL;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.TOPIC_ALIAS_MAXIMUM;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.WILL_DELAY_INTERVAL;
/** /**
* MQTTConnectionManager is responsible for handle Connect and Disconnect packets and any resulting behaviour of these * MQTTConnectionManager is responsible for handle Connect and Disconnect packets and any resulting behaviour of these
@ -35,50 +47,57 @@ import org.apache.activemq.artemis.utils.UUIDGenerator;
*/ */
public class MQTTConnectionManager { public class MQTTConnectionManager {
private static final Logger logger = Logger.getLogger(MQTTConnectionManager.class);
private MQTTSession session; private MQTTSession session;
private MQTTLogger log = MQTTLogger.LOGGER;
private boolean isWill = false;
private ByteBuf willMessage;
private String willTopic;
private int willQoSLevel;
private boolean willRetain;
public MQTTConnectionManager(MQTTSession session) { public MQTTConnectionManager(MQTTSession session) {
this.session = session; this.session = session;
MQTTFailureListener failureListener = new MQTTFailureListener(this); MQTTFailureListener failureListener = new MQTTFailureListener(this);
session.getConnection().addFailureListener(failureListener); session.getConnection().addFailureListener(failureListener);
} }
/** void connect(MqttConnectMessage connect, String validatedUser) throws Exception {
* Handles the connect packet. See spec for details on each of parameters. int packetVersion = connect.variableHeader().version();
*/ if (packetVersion == MqttVersion.MQTT_5.protocolLevel()) {
void connect(String cId, session.set5(true);
String username, session.getConnection().setProtocolVersion(Byte.toString(MqttVersion.MQTT_5.protocolLevel()));
String password, String authenticationMethod = MQTTUtil.getProperty(String.class, connect.variableHeader().properties(), AUTHENTICATION_METHOD);
boolean will,
byte[] willMessage, if (authenticationMethod != null) {
String willTopic, session.getProtocolHandler().sendConnack(MQTTReasonCodes.BAD_AUTHENTICATION_METHOD);
boolean willRetain, disconnect(true);
int willQosLevel, return;
boolean cleanSession, String validatedUser) throws Exception { }
String clientId = validateClientId(cId, cleanSession); }
if (clientId == null) {
session.getProtocolHandler().sendConnack(MqttConnectReturnCode.CONNECTION_REFUSED_IDENTIFIER_REJECTED); String password = connect.payload().passwordInBytes() == null ? null : new String( connect.payload().passwordInBytes(), CharsetUtil.UTF_8);
session.getProtocolHandler().disconnect(true); String username = connect.payload().userName();
// the Netty codec uses "CleanSession" for both 3.1.1 "clean session" and 5 "clean start" which have slightly different semantics
boolean cleanStart = connect.variableHeader().isCleanSession();
Pair<String, Boolean> clientIdValidation = validateClientId(connect.payload().clientIdentifier(), cleanStart);
if (clientIdValidation == null) {
// this represents an invalid client ID for MQTT 5 clients
session.getProtocolHandler().sendConnack(MQTTReasonCodes.CLIENT_IDENTIFIER_NOT_VALID);
disconnect(true);
return;
} else if (clientIdValidation.getA() == null) {
// this represents an invalid client ID for MQTT 3.x clients
session.getProtocolHandler().sendConnack(MQTTReasonCodes.IDENTIFIER_REJECTED_3);
disconnect(true);
return; return;
} }
String clientId = clientIdValidation.getA();
boolean assignedClientId = clientIdValidation.getB();
boolean sessionPresent = session.getProtocolManager().getSessionStates().containsKey(clientId); boolean sessionPresent = session.getProtocolManager().getSessionStates().containsKey(clientId);
MQTTSessionState sessionState = getSessionState(clientId); MQTTSessionState sessionState = getSessionState(clientId);
synchronized (sessionState) { synchronized (sessionState) {
session.setSessionState(sessionState); session.setSessionState(sessionState);
session.getConnection().setClientID(clientId); session.getConnection().setClientID(clientId);
sessionState.setFailed(false);
ServerSessionImpl serverSession = createServerSession(username, password, validatedUser); ServerSessionImpl serverSession = createServerSession(username, password, validatedUser);
serverSession.start(); serverSession.start();
ServerSessionImpl internalServerSession = createServerSession(username, password, validatedUser); ServerSessionImpl internalServerSession = createServerSession(username, password, validatedUser);
@ -86,7 +105,7 @@ public class MQTTConnectionManager {
internalServerSession.start(); internalServerSession.start();
session.setServerSession(serverSession, internalServerSession); session.setServerSession(serverSession, internalServerSession);
if (cleanSession) { if (cleanStart) {
/* [MQTT-3.1.2-6] If CleanSession is set to 1, the Client and Server MUST discard any previous Session and /* [MQTT-3.1.2-6] If CleanSession is set to 1, the Client and Server MUST discard any previous Session and
* start a new one. This Session lasts as long as the Network Connection. State data associated with this Session * start a new one. This Session lasts as long as the Network Connection. State data associated with this Session
* MUST NOT be reused in any subsequent Session */ * MUST NOT be reused in any subsequent Session */
@ -94,30 +113,67 @@ public class MQTTConnectionManager {
session.setClean(true); session.setClean(true);
} }
if (will) { if (connect.variableHeader().isWillFlag()) {
isWill = true; session.getState().setWill(true);
this.willMessage = ByteBufAllocator.DEFAULT.buffer(willMessage.length); byte[] willMessage = connect.payload().willMessageInBytes();
this.willMessage.writeBytes(willMessage); session.getState().setWillMessage(ByteBufAllocator.DEFAULT.buffer(willMessage.length).writeBytes(willMessage));
this.willQoSLevel = willQosLevel; session.getState().setWillQoSLevel(connect.variableHeader().willQos());
this.willRetain = willRetain; session.getState().setWillRetain(connect.variableHeader().isWillRetain());
this.willTopic = willTopic; session.getState().setWillTopic(connect.payload().willTopic());
if (session.is5()) {
MqttProperties willProperties = connect.payload().willProperties();
if (willProperties != null) {
MqttProperties.MqttProperty willDelayInterval = willProperties.getProperty(WILL_DELAY_INTERVAL.value());
if (willDelayInterval != null) {
session.getState().setWillDelayInterval(( int) willDelayInterval.value());
}
}
}
}
MqttProperties connackProperties;
if (session.is5()) {
session.getConnection().setReceiveMaximum(MQTTUtil.getProperty(Integer.class, connect.variableHeader().properties(), RECEIVE_MAXIMUM, -1));
sessionState.setClientSessionExpiryInterval(MQTTUtil.getProperty(Integer.class, connect.variableHeader().properties(), SESSION_EXPIRY_INTERVAL, 0));
sessionState.setClientMaxPacketSize(MQTTUtil.getProperty(Integer.class, connect.variableHeader().properties(), MAXIMUM_PACKET_SIZE, 0));
sessionState.setClientTopicAliasMaximum(MQTTUtil.getProperty(Integer.class, connect.variableHeader().properties(), TOPIC_ALIAS_MAXIMUM));
connackProperties = getConnackProperties(clientId, assignedClientId);
} else {
connackProperties = MqttProperties.NO_PROPERTIES;
} }
session.getConnection().setConnected(true); session.getConnection().setConnected(true);
session.getProtocolHandler().sendConnack(MqttConnectReturnCode.CONNECTION_ACCEPTED, sessionPresent && !cleanSession, MqttProperties.NO_PROPERTIES); session.getProtocolHandler().sendConnack(MQTTReasonCodes.SUCCESS, sessionPresent && !cleanStart, connackProperties);
// ensure we don't publish before the CONNACK // ensure we don't publish before the CONNACK
session.start(); session.start();
} }
} }
/** private MqttProperties getConnackProperties(String clientId, boolean assignedClientId) {
* Creates an internal Server Session. MqttProperties connackProperties = new MqttProperties();
*
* @param username if (assignedClientId) {
* @param password connackProperties.add(new MqttProperties.StringProperty(ASSIGNED_CLIENT_IDENTIFIER.value(), clientId));
* @return }
* @throws Exception
*/ if (this.session.getProtocolManager().getTopicAliasMaximum() != -1) {
connackProperties.add(new MqttProperties.IntegerProperty(TOPIC_ALIAS_MAXIMUM.value(), this.session.getProtocolManager().getTopicAliasMaximum()));
}
if (this.session.isUsingServerKeepAlive()) {
connackProperties.add(new MqttProperties.IntegerProperty(SERVER_KEEP_ALIVE.value(), this.session.getProtocolManager().getServerKeepAlive()));
}
if (this.session.getProtocolManager().getMaximumPacketSize() != -1) {
connackProperties.add(new MqttProperties.IntegerProperty(MAXIMUM_PACKET_SIZE.value(), this.session.getProtocolManager().getMaximumPacketSize()));
}
return connackProperties;
}
ServerSessionImpl createServerSession(String username, String password, String validatedUser) throws Exception { ServerSessionImpl createServerSession(String username, String password, String validatedUser) throws Exception {
String id = UUIDGenerator.getInstance().generateStringUUID(); String id = UUIDGenerator.getInstance().generateStringUUID();
ActiveMQServer server = session.getServer(); ActiveMQServer server = session.getServer();
@ -144,19 +200,15 @@ public class MQTTConnectionManager {
return; return;
} }
synchronized (session.getSessionState()) { synchronized (session.getState()) {
try { try {
if (isWill && failure) { session.stop(failure);
session.getMqttPublishManager().sendInternal(0, willTopic, willQoSLevel, willMessage, willRetain, true);
}
session.stop();
session.getConnection().destroy(); session.getConnection().destroy();
} catch (Exception e) { } catch (Exception e) {
log.error("Error disconnecting client: " + e.getMessage()); MQTTLogger.LOGGER.errorDisconnectingClient(e);
} finally { } finally {
if (session.getSessionState() != null) { if (session.getState() != null) {
session.getSessionState().setAttached(false); String clientId = session.getState().getClientId();
String clientId = session.getSessionState().getClientId();
/** /**
* ensure that the connection for the client ID matches *this* connection otherwise we could remove the * ensure that the connection for the client ID matches *this* connection otherwise we could remove the
* entry for the client who "stole" this client ID via [MQTT-3.1.4-2] * entry for the client who "stole" this client ID via [MQTT-3.1.4-2]
@ -173,10 +225,12 @@ public class MQTTConnectionManager {
return session.getProtocolManager().getSessionState(clientId); return session.getProtocolManager().getSessionState(clientId);
} }
private String validateClientId(String clientId, boolean cleanSession) { private Pair<String, Boolean> validateClientId(String clientId, boolean cleanSession) {
Boolean assigned = Boolean.FALSE;
if (clientId == null || clientId.isEmpty()) { if (clientId == null || clientId.isEmpty()) {
// [MQTT-3.1.3-7] [MQTT-3.1.3-6] If client does not specify a client ID and clean session is set to 1 create it. // [MQTT-3.1.3-7] [MQTT-3.1.3-6] If client does not specify a client ID and clean session is set to 1 create it.
if (cleanSession) { if (cleanSession) {
assigned = Boolean.TRUE;
clientId = UUID.randomUUID().toString(); clientId = UUID.randomUUID().toString();
} else { } else {
// [MQTT-3.1.3-8] Return ID rejected and disconnect if clean session = false and client id is null // [MQTT-3.1.3-8] Return ID rejected and disconnect if clean session = false and client id is null
@ -186,10 +240,14 @@ public class MQTTConnectionManager {
MQTTConnection connection = session.getProtocolManager().addConnectedClient(clientId, session.getConnection()); MQTTConnection connection = session.getProtocolManager().addConnectedClient(clientId, session.getConnection());
if (connection != null) { if (connection != null) {
MQTTSession existingSession = session.getProtocolManager().getSessionState(clientId).getSession();
if (session.is5()) {
existingSession.getProtocolHandler().sendDisconnect(MQTTReasonCodes.SESSION_TAKEN_OVER);
}
// [MQTT-3.1.4-2] If the client ID represents a client already connected to the server then the server MUST disconnect the existing client // [MQTT-3.1.4-2] If the client ID represents a client already connected to the server then the server MUST disconnect the existing client
connection.disconnect(false); existingSession.getConnectionManager().disconnect(false);
} }
} }
return clientId; return new Pair<>(clientId, assigned);
} }
} }

View File

@ -34,7 +34,7 @@ public class MQTTFailureListener implements FailureListener {
@Override @Override
public void connectionFailed(ActiveMQException exception, boolean failedOver) { public void connectionFailed(ActiveMQException exception, boolean failedOver) {
connectionManager.disconnect(true); connectionFailed(exception, failedOver, null);
} }
@Override @Override

View File

@ -17,8 +17,12 @@
package org.apache.activemq.artemis.core.protocol.mqtt; package org.apache.activemq.artemis.core.protocol.mqtt;
import org.apache.activemq.artemis.core.server.MessageReference;
import org.jboss.logging.BasicLogger; import org.jboss.logging.BasicLogger;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.logging.annotations.Cause;
import org.jboss.logging.annotations.LogMessage;
import org.jboss.logging.annotations.Message;
import org.jboss.logging.annotations.MessageLogger; import org.jboss.logging.annotations.MessageLogger;
/** /**
@ -33,11 +37,47 @@ import org.jboss.logging.annotations.MessageLogger;
* TRACE 5 * TRACE 5
* FATAL 6 * FATAL 6
* *
* so an INFO message would be 101000 to 101999 * so an INFO message would be 831000 to 831999
*/ */
@MessageLogger(projectCode = "AMQ") @MessageLogger(projectCode = "AMQ")
public interface MQTTLogger extends BasicLogger { public interface MQTTLogger extends BasicLogger {
MQTTLogger LOGGER = Logger.getMessageLogger(MQTTLogger.class, MQTTLogger.class.getPackage().getName()); MQTTLogger LOGGER = Logger.getMessageLogger(MQTTLogger.class, MQTTLogger.class.getPackage().getName());
}
@LogMessage(level = Logger.Level.WARN)
@Message(id = 832000, value = "Unable to send message: {0}", format = Message.Format.MESSAGE_FORMAT)
void unableToSendMessage(MessageReference message, @Cause Exception e);
@LogMessage(level = Logger.Level.WARN)
@Message(id = 832001, value = "MQTT client({0}) attempted to ack already ack'd message: ", format = Message.Format.MESSAGE_FORMAT)
void failedToAckMessage(String clientId, @Cause Exception e);
@LogMessage(level = Logger.Level.ERROR)
@Message(id = 834000, value = "Error removing subscription.", format = Message.Format.MESSAGE_FORMAT)
void errorRemovingSubscription(@Cause Exception e);
@LogMessage(level = Logger.Level.ERROR)
@Message(id = 834001, value = "Error disconnecting client.", format = Message.Format.MESSAGE_FORMAT)
void errorDisconnectingClient(@Cause Exception e);
@LogMessage(level = Logger.Level.ERROR)
@Message(id = 834002, value = "Error processing control packet: {0}", format = Message.Format.MESSAGE_FORMAT)
void errorProcessingControlPacket(String packet, @Cause Exception e);
@LogMessage(level = Logger.Level.ERROR)
@Message(id = 834003, value = "Error sending will message.", format = Message.Format.MESSAGE_FORMAT)
void errorSendingWillMessage(@Cause Exception e);
@LogMessage(level = Logger.Level.ERROR)
@Message(id = 834004, value = "Error disconnecting consumer.", format = Message.Format.MESSAGE_FORMAT)
void errorDisconnectingConsumer(@Cause Exception e);
@LogMessage(level = Logger.Level.ERROR)
@Message(id = 834005, value = "Failed to cast property {0}.", format = Message.Format.MESSAGE_FORMAT)
void failedToCastProperty(String property);
@LogMessage(level = Logger.Level.ERROR)
@Message(id = 834006, value = "Failed to publish MQTT message: {0}.", format = Message.Format.MESSAGE_FORMAT)
void failedToPublishMqttMessage(String exceptionMessage, @Cause Throwable t);
}

View File

@ -17,33 +17,41 @@
package org.apache.activemq.artemis.core.protocol.mqtt; package org.apache.activemq.artemis.core.protocol.mqtt;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.mqtt.MqttConnAckMessage;
import io.netty.handler.codec.mqtt.MqttConnAckVariableHeader;
import io.netty.handler.codec.mqtt.MqttConnectMessage; import io.netty.handler.codec.mqtt.MqttConnectMessage;
import io.netty.handler.codec.mqtt.MqttConnectReturnCode; import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
import io.netty.handler.codec.mqtt.MqttFixedHeader; import io.netty.handler.codec.mqtt.MqttFixedHeader;
import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttMessage;
import io.netty.handler.codec.mqtt.MqttMessageBuilders;
import io.netty.handler.codec.mqtt.MqttMessageIdAndPropertiesVariableHeader;
import io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader; import io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader;
import io.netty.handler.codec.mqtt.MqttMessageType; import io.netty.handler.codec.mqtt.MqttMessageType;
import io.netty.handler.codec.mqtt.MqttProperties; import io.netty.handler.codec.mqtt.MqttProperties;
import io.netty.handler.codec.mqtt.MqttPubAckMessage; import io.netty.handler.codec.mqtt.MqttPubAckMessage;
import io.netty.handler.codec.mqtt.MqttPubReplyMessageVariableHeader;
import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage;
import io.netty.handler.codec.mqtt.MqttPublishVariableHeader;
import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.codec.mqtt.MqttQoS;
import io.netty.handler.codec.mqtt.MqttReasonCodeAndPropertiesVariableHeader;
import io.netty.handler.codec.mqtt.MqttSubAckMessage; import io.netty.handler.codec.mqtt.MqttSubAckMessage;
import io.netty.handler.codec.mqtt.MqttSubAckPayload; import io.netty.handler.codec.mqtt.MqttSubAckPayload;
import io.netty.handler.codec.mqtt.MqttSubscribeMessage; import io.netty.handler.codec.mqtt.MqttSubscribeMessage;
import io.netty.handler.codec.mqtt.MqttUnsubAckMessage; import io.netty.handler.codec.mqtt.MqttUnsubAckMessage;
import io.netty.handler.codec.mqtt.MqttUnsubAckPayload;
import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage; import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage;
import io.netty.handler.codec.mqtt.MqttVersion;
import io.netty.util.CharsetUtil; import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCountUtil;
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException;
import org.apache.activemq.artemis.core.protocol.mqtt.exceptions.DisconnectException;
import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.logs.AuditLogger; import org.apache.activemq.artemis.logs.AuditLogger;
import org.apache.activemq.artemis.spi.core.protocol.ConnectionEntry; import org.apache.activemq.artemis.spi.core.protocol.ConnectionEntry;
import org.apache.activemq.artemis.utils.actors.Actor; import org.apache.activemq.artemis.utils.actors.Actor;
import org.jboss.logging.Logger;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.AUTHENTICATION_DATA;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.AUTHENTICATION_METHOD;
/** /**
* This class is responsible for receiving and sending MQTT packets, delegating behaviour to one of the * This class is responsible for receiving and sending MQTT packets, delegating behaviour to one of the
@ -51,6 +59,8 @@ import org.apache.activemq.artemis.utils.actors.Actor;
*/ */
public class MQTTProtocolHandler extends ChannelInboundHandlerAdapter { public class MQTTProtocolHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = Logger.getLogger(MQTTProtocolHandler.class);
private ConnectionEntry connectionEntry; private ConnectionEntry connectionEntry;
private MQTTConnection connection; private MQTTConnection connection;
@ -64,8 +74,6 @@ public class MQTTProtocolHandler extends ChannelInboundHandlerAdapter {
// This Channel Handler is not sharable, therefore it can only ever be associated with a single ctx. // This Channel Handler is not sharable, therefore it can only ever be associated with a single ctx.
private ChannelHandlerContext ctx; private ChannelHandlerContext ctx;
private final MQTTLogger log = MQTTLogger.LOGGER;
private boolean stopped = false; private boolean stopped = false;
private final Actor<MqttMessage> mqttMessageActor; private final Actor<MqttMessage> mqttMessageActor;
@ -88,21 +96,46 @@ public class MQTTProtocolHandler extends ChannelInboundHandlerAdapter {
@Override @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) { public void channelRead(ChannelHandlerContext ctx, Object msg) {
connection.dataReceived();
MqttMessage message = (MqttMessage) msg; MqttMessage message = (MqttMessage) msg;
// Disconnect if Netty codec failed to decode the stream. if (stopped) {
if (message.decoderResult().isFailure()) { if (session.is5()) {
log.debug("Bad Message Disconnecting Client."); sendDisconnect(MQTTReasonCodes.IMPLEMENTATION_SPECIFIC_ERROR);
}
disconnect(true); disconnect(true);
return; return;
} }
// Disconnect if Netty codec failed to decode the stream.
if (message.decoderResult().isFailure()) {
logger.debugf(message.decoderResult().cause(), "Disconnecting client due to message decoding failure.");
if (session.is5()) {
sendDisconnect(MQTTReasonCodes.MALFORMED_PACKET);
}
disconnect(true);
return;
}
String interceptResult = this.protocolManager.invokeIncoming(message, this.connection);
if (interceptResult != null) {
logger.debugf("Interceptor %s rejected MQTT control packet: %s", interceptResult, message);
disconnect(true);
return;
}
connection.dataReceived();
if (AuditLogger.isAnyLoggingEnabled()) {
AuditLogger.setRemoteAddress(connection.getRemoteAddress());
}
MQTTUtil.logMessage(session.getState(), message, true);
if (this.ctx == null) { if (this.ctx == null) {
this.ctx = ctx; this.ctx = ctx;
} }
// let netty handle keepalive response // let Netty handle client pings (i.e. connection keep-alive)
if (MqttMessageType.PINGREQ == message.fixedHeader().messageType()) { if (MqttMessageType.PINGREQ == message.fixedHeader().messageType()) {
handlePingreq(); handlePingreq();
} else { } else {
@ -112,24 +145,10 @@ public class MQTTProtocolHandler extends ChannelInboundHandlerAdapter {
public void act(MqttMessage message) { public void act(MqttMessage message) {
try { try {
if (stopped) {
disconnect(true);
return;
}
if (AuditLogger.isAnyLoggingEnabled()) {
AuditLogger.setRemoteAddress(connection.getRemoteAddress());
}
MQTTUtil.logMessage(session.getState(), message, true);
if (this.protocolManager.invokeIncoming(message, this.connection) != null) {
log.debugf("Interceptor rejected MQTT message: %s", message);
disconnect(true);
return;
}
switch (message.fixedHeader().messageType()) { switch (message.fixedHeader().messageType()) {
case AUTH:
handleAuth(message);
break;
case CONNECT: case CONNECT:
handleConnect((MqttConnectMessage) message); handleConnect((MqttConnectMessage) message);
break; break;
@ -159,34 +178,103 @@ public class MQTTProtocolHandler extends ChannelInboundHandlerAdapter {
break; break;
case UNSUBACK: case UNSUBACK:
case SUBACK: case SUBACK:
case PINGREQ: // These are actually handled by the Netty thread directly so this packet should never make it here
case PINGRESP: case PINGRESP:
case CONNACK: // The server does not instantiate connections therefore any CONNACK received over a connection is an invalid control message. case CONNACK: // The server does not instantiate connections therefore any CONNACK received over a connection is an invalid control message.
default: default:
disconnect(true); disconnect(true);
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("Error processing Control Packet, Disconnecting Client", e); MQTTLogger.LOGGER.errorProcessingControlPacket(message.toString(), e);
if (session.is5()) {
sendDisconnect(MQTTReasonCodes.IMPLEMENTATION_SPECIFIC_ERROR);
}
disconnect(true); disconnect(true);
} finally { } finally {
ReferenceCountUtil.release(message); ReferenceCountUtil.release(message);
} }
} }
/** /*
* Called during connection. * Scaffolding for "enhanced authentication" implementation.
* *
* @param connect * See:
* https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901217
* https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901256
*
* Tests for this are in:
* org.apache.activemq.artemis.tests.integration.mqtt5.spec.controlpackets.AuthTests
* org.apache.activemq.artemis.tests.integration.mqtt5.spec.EnhancedAuthenticationTests
*
* This should integrate somehow with our existing SASL implementation for challenge/response conversations.
*/ */
void handleConnect(MqttConnectMessage connect) throws Exception { void handleAuth(MqttMessage auth) throws Exception {
final String username = connect.payload().userName(); byte[] authenticationData = MQTTUtil.getProperty(byte[].class, ((MqttReasonCodeAndPropertiesVariableHeader)auth.variableHeader()).properties(), AUTHENTICATION_DATA);
final String password = connect.payload().passwordInBytes() == null ? null : new String( connect.payload().passwordInBytes(), CharsetUtil.UTF_8); String authenticationMethod = MQTTUtil.getProperty(String.class, ((MqttReasonCodeAndPropertiesVariableHeader)auth.variableHeader()).properties(), AUTHENTICATION_METHOD);
final String validatedUser = server.validateUser(username, password, session.getConnection(), session.getProtocolManager().getSecurityDomain());
if (connection.getTransportConnection().getRedirectTo() == null ||
!protocolManager.getRedirectHandler().redirect(connection, session, connect)) {
connectionEntry.ttl = connect.variableHeader().keepAliveTimeSeconds() * 1500L;
String clientId = connect.payload().clientIdentifier(); MqttReasonCodeAndPropertiesVariableHeader header = (MqttReasonCodeAndPropertiesVariableHeader) auth.variableHeader();
session.getConnectionManager().connect(clientId, username, password, connect.variableHeader().isWillFlag(), connect.payload().willMessageInBytes(), connect.payload().willTopic(), connect.variableHeader().isWillRetain(), connect.variableHeader().willQos(), connect.variableHeader().isCleanSession(), validatedUser); if (header.reasonCode() == MQTTReasonCodes.RE_AUTHENTICATE) {
} else if (header.reasonCode() == MQTTReasonCodes.CONTINUE_AUTHENTICATION) {
} else if (header.reasonCode() == MQTTReasonCodes.SUCCESS) {
}
}
void handleConnect(MqttConnectMessage connect) throws Exception {
/*
* Perform authentication *before* attempting redirection because redirection may be based on the user's role.
*/
String password = connect.payload().passwordInBytes() == null ? null : new String(connect.payload().passwordInBytes(), CharsetUtil.UTF_8);
String username = connect.payload().userName();
String validatedUser;
try {
validatedUser = session.getServer().validateUser(username, password, session.getConnection(), session.getProtocolManager().getSecurityDomain());
} catch (ActiveMQSecurityException e) {
if (session.is5()) {
session.getProtocolHandler().sendConnack(MQTTReasonCodes.BAD_USER_NAME_OR_PASSWORD);
}
disconnect(true);
return;
}
if (connection.getTransportConnection().getRedirectTo() == null || !protocolManager.getRedirectHandler().redirect(connection, session, connect)) {
/* [MQTT-3.1.2-2] Reject unsupported clients. */
int packetVersion = connect.variableHeader().version();
if (packetVersion != MqttVersion.MQTT_3_1.protocolLevel() &&
packetVersion != MqttVersion.MQTT_3_1_1.protocolLevel() &&
packetVersion != MqttVersion.MQTT_5.protocolLevel()) {
if (packetVersion <= MqttVersion.MQTT_3_1_1.protocolLevel()) {
// See MQTT-3.1.2-2 at http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718030
sendConnack(MQTTReasonCodes.UNACCEPTABLE_PROTOCOL_VERSION_3);
} else {
// See MQTT-3.1.2-2 at https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901037
sendConnack(MQTTReasonCodes.UNSUPPORTED_PROTOCOL_VERSION);
}
disconnect(true);
return;
}
/*
* If the server's keep-alive has been disabled (-1) or if the client is using a lower value than the server
* then we use the client's keep-alive.
*
* We must adjust the keep-alive because MQTT communicates keep-alive values in *seconds*, but the broker uses
* *milliseconds*. Also, the connection keep-alive is effectively "one and a half times" the configured
* keep-alive value. See [MQTT-3.1.2-22].
*/
int serverKeepAlive = session.getProtocolManager().getServerKeepAlive();
int clientKeepAlive = connect.variableHeader().keepAliveTimeSeconds();
if (serverKeepAlive == -1 || (clientKeepAlive <= serverKeepAlive && clientKeepAlive != 0)) {
connectionEntry.ttl = clientKeepAlive * MQTTUtil.KEEP_ALIVE_ADJUSTMENT;
} else {
session.setUsingServerKeepAlive(true);
}
session.getConnectionManager().connect(connect, validatedUser);
} }
} }
@ -194,39 +282,60 @@ public class MQTTProtocolHandler extends ChannelInboundHandlerAdapter {
session.getConnectionManager().disconnect(error); session.getConnectionManager().disconnect(error);
} }
void sendConnack(MqttConnectReturnCode returnCode) { void sendConnack(byte returnCode) {
sendConnack(returnCode, MqttProperties.NO_PROPERTIES); sendConnack(returnCode, MqttProperties.NO_PROPERTIES);
} }
void sendConnack(MqttConnectReturnCode returnCode, MqttProperties properties) { void sendConnack(byte returnCode, MqttProperties properties) {
sendConnack(returnCode, true, properties); sendConnack(returnCode, true, properties);
} }
void sendConnack(MqttConnectReturnCode returnCode, boolean sessionPresent, MqttProperties properties) { void sendConnack(byte returnCode, boolean sessionPresent, MqttProperties properties) {
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0); // 3.1.1 - [MQTT-3.2.2-4] If a server sends a CONNACK packet containing a non-zero return code it MUST set Session Present to 0.
// [MQTT-3.2.2-4] If a server sends a CONNACK packet containing a non-zero return code it MUST set Session Present to 0. // 5 - [MQTT-3.2.2-6] If a Server sends a CONNACK packet containing a non-zero Reason Code it MUST set Session Present to 0.
if (returnCode.byteValue() != (byte) 0x00) { if (returnCode != MQTTReasonCodes.SUCCESS) {
sessionPresent = false; sessionPresent = false;
} }
MqttConnAckVariableHeader varHeader = new MqttConnAckVariableHeader(returnCode, sessionPresent, properties); sendToClient(MqttMessageBuilders
MqttConnAckMessage message = new MqttConnAckMessage(fixedHeader, varHeader); .connAck()
sendToClient(message); .returnCode(MqttConnectReturnCode.valueOf(returnCode))
.properties(properties)
.sessionPresent(sessionPresent)
.build());
}
void sendDisconnect(byte reasonCode) {
sendToClient(MqttMessageBuilders
.disconnect()
.reasonCode(reasonCode)
.build());
} }
void handlePublish(MqttPublishMessage message) throws Exception { void handlePublish(MqttPublishMessage message) throws Exception {
session.getMqttPublishManager().handleMessage(message.variableHeader().packetId(), message.variableHeader().topicName(), message.fixedHeader().qosLevel().value(), message.payload(), message.fixedHeader().isRetain()); if (session.is5() && session.getProtocolManager().getMaximumPacketSize() != -1 && MQTTUtil.calculateMessageSize(message) > session.getProtocolManager().getMaximumPacketSize()) {
sendDisconnect(MQTTReasonCodes.PACKET_TOO_LARGE);
disconnect(true);
return;
}
try {
session.getMqttPublishManager().sendToQueue(message, false);
} catch (DisconnectException e) {
sendDisconnect(e.getCode());
disconnect(true);
}
} }
void sendPubAck(int messageId) { void sendPubAck(int messageId, byte reasonCode) {
sendPublishProtocolControlMessage(messageId, MqttMessageType.PUBACK); sendPublishProtocolControlMessage(messageId, MqttMessageType.PUBACK, reasonCode);
} }
void sendPubRel(int messageId) { void sendPubRel(int messageId) {
sendPublishProtocolControlMessage(messageId, MqttMessageType.PUBREL); sendPublishProtocolControlMessage(messageId, MqttMessageType.PUBREL);
} }
void sendPubRec(int messageId) { void sendPubRec(int messageId, byte reasonCode) {
sendPublishProtocolControlMessage(messageId, MqttMessageType.PUBREC); sendPublishProtocolControlMessage(messageId, MqttMessageType.PUBREC, reasonCode);
} }
void sendPubComp(int messageId) { void sendPubComp(int messageId) {
@ -234,14 +343,25 @@ public class MQTTProtocolHandler extends ChannelInboundHandlerAdapter {
} }
void sendPublishProtocolControlMessage(int messageId, MqttMessageType messageType) { void sendPublishProtocolControlMessage(int messageId, MqttMessageType messageType) {
MqttQoS qos = (messageType == MqttMessageType.PUBREL) ? MqttQoS.AT_LEAST_ONCE : MqttQoS.AT_MOST_ONCE; sendPublishProtocolControlMessage(messageId, messageType, MQTTReasonCodes.SUCCESS);
MqttFixedHeader fixedHeader = new MqttFixedHeader(messageType, false, qos, // Spec requires 01 in header for rel }
false, 0);
MqttPubAckMessage rel = new MqttPubAckMessage(fixedHeader, MqttMessageIdVariableHeader.from(messageId)); void sendPublishProtocolControlMessage(int messageId, MqttMessageType messageType, byte reasonCode) {
sendToClient(rel); // [MQTT-3.6.1-1] Spec requires 01 in header for rel
MqttFixedHeader fixedHeader = new MqttFixedHeader(messageType, false, (messageType == MqttMessageType.PUBREL) ? MqttQoS.AT_LEAST_ONCE : MqttQoS.AT_MOST_ONCE, false, 0);
MqttMessageIdVariableHeader variableHeader;
if (session.is5()) {
variableHeader = new MqttPubReplyMessageVariableHeader(messageId, reasonCode, MqttProperties.NO_PROPERTIES);
} else {
variableHeader = MqttMessageIdVariableHeader.from(messageId);
}
MqttPubAckMessage pubAck = new MqttPubAckMessage(fixedHeader, variableHeader);
sendToClient(pubAck);
} }
void handlePuback(MqttPubAckMessage message) throws Exception { void handlePuback(MqttPubAckMessage message) throws Exception {
// ((MqttPubReplyMessageVariableHeader)message.variableHeader()).reasonCode();
session.getMqttPublishManager().handlePubAck(getMessageId(message)); session.getMqttPublishManager().handlePubAck(getMessageId(message));
} }
@ -258,19 +378,23 @@ public class MQTTProtocolHandler extends ChannelInboundHandlerAdapter {
} }
void handleSubscribe(MqttSubscribeMessage message) throws Exception { void handleSubscribe(MqttSubscribeMessage message) throws Exception {
MQTTSubscriptionManager subscriptionManager = session.getSubscriptionManager(); int[] qos = session.getSubscriptionManager().addSubscriptions(message.payload().topicSubscriptions(), message.idAndPropertiesVariableHeader().properties());
int[] qos = subscriptionManager.addSubscriptions(message.payload().topicSubscriptions());
MqttFixedHeader header = new MqttFixedHeader(MqttMessageType.SUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0); MqttFixedHeader header = new MqttFixedHeader(MqttMessageType.SUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
MqttSubAckMessage ack = new MqttSubAckMessage(header, message.variableHeader(), new MqttSubAckPayload(qos)); MqttMessageIdAndPropertiesVariableHeader variableHeader = new MqttMessageIdAndPropertiesVariableHeader(message.variableHeader().messageId(), MqttProperties.NO_PROPERTIES);
sendToClient(ack); MqttSubAckMessage subAck = new MqttSubAckMessage(header, variableHeader, new MqttSubAckPayload(qos));
sendToClient(subAck);
} }
void handleUnsubscribe(MqttUnsubscribeMessage message) throws Exception { void handleUnsubscribe(MqttUnsubscribeMessage message) throws Exception {
session.getSubscriptionManager().removeSubscriptions(message.payload().topics()); short[] reasonCodes = session.getSubscriptionManager().removeSubscriptions(message.payload().topics());
MqttFixedHeader header = new MqttFixedHeader(MqttMessageType.UNSUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0); MqttFixedHeader header = new MqttFixedHeader(MqttMessageType.UNSUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
MqttUnsubAckMessage m = new MqttUnsubAckMessage(header, message.variableHeader()); MqttUnsubAckMessage unsubAck;
sendToClient(m); if (session.is5()) {
unsubAck = new MqttUnsubAckMessage(header, message.variableHeader(), new MqttUnsubAckPayload(reasonCodes));
} else {
unsubAck = new MqttUnsubAckMessage(header, message.variableHeader());
}
sendToClient(unsubAck);
} }
void handlePingreq() { void handlePingreq() {
@ -278,19 +402,11 @@ public class MQTTProtocolHandler extends ChannelInboundHandlerAdapter {
sendToClient(pingResp); sendToClient(pingResp);
} }
protected void send(int messageId, String topicName, int qosLevel, boolean isRetain, ByteBuf payload, int deliveryCount) { protected void sendToClient(MqttMessage message) {
boolean redelivery = qosLevel == 0 ? false : (deliveryCount > 0);
MqttFixedHeader header = new MqttFixedHeader(MqttMessageType.PUBLISH, redelivery, MqttQoS.valueOf(qosLevel), isRetain, 0);
MqttPublishVariableHeader varHeader = new MqttPublishVariableHeader(topicName, messageId);
MqttMessage publish = new MqttPublishMessage(header, varHeader, payload);
sendToClient(publish);
}
private void sendToClient(MqttMessage message) {
if (this.protocolManager.invokeOutgoing(message, connection) != null) { if (this.protocolManager.invokeOutgoing(message, connection) != null) {
return; return;
} }
MQTTUtil.logMessage(session.getSessionState(), message, false); MQTTUtil.logMessage(session.getState(), message, false);
ctx.writeAndFlush(message, ctx.voidPromise()); ctx.writeAndFlush(message, ctx.voidPromise());
} }

View File

@ -45,12 +45,12 @@ import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
import org.apache.activemq.artemis.spi.core.remoting.Acceptor; import org.apache.activemq.artemis.spi.core.remoting.Acceptor;
import org.apache.activemq.artemis.spi.core.remoting.Connection; import org.apache.activemq.artemis.spi.core.remoting.Connection;
import org.apache.activemq.artemis.utils.collections.TypedProperties; import org.apache.activemq.artemis.utils.collections.TypedProperties;
import org.jboss.logging.Logger;
/**
* MQTTProtocolManager
*/
public class MQTTProtocolManager extends AbstractProtocolManager<MqttMessage, MQTTInterceptor, MQTTConnection, MQTTRedirectHandler> implements NotificationListener { public class MQTTProtocolManager extends AbstractProtocolManager<MqttMessage, MQTTInterceptor, MQTTConnection, MQTTRedirectHandler> implements NotificationListener {
private static final Logger logger = Logger.getLogger(MQTTProtocolManager.class);
private static final List<String> websocketRegistryNames = Arrays.asList("mqtt", "mqttv3.1"); private static final List<String> websocketRegistryNames = Arrays.asList("mqtt", "mqttv3.1");
private ActiveMQServer server; private ActiveMQServer server;
@ -64,6 +64,14 @@ public class MQTTProtocolManager extends AbstractProtocolManager<MqttMessage, MQ
private int defaultMqttSessionExpiryInterval = -1; private int defaultMqttSessionExpiryInterval = -1;
private int topicAliasMaximum = MQTTUtil.DEFAULT_TOPIC_ALIAS_MAX;
private int receiveMaximum = MQTTUtil.DEFAULT_RECEIVE_MAXIMUM;
private int serverKeepAlive = MQTTUtil.DEFAULT_SERVER_KEEP_ALIVE;
private int maximumPacketSize = MQTTUtil.DEFAULT_MAXIMUM_PACKET_SIZE;
private final MQTTRedirectHandler redirectHandler; private final MQTTRedirectHandler redirectHandler;
MQTTProtocolManager(ActiveMQServer server, MQTTProtocolManager(ActiveMQServer server,
@ -84,6 +92,42 @@ public class MQTTProtocolManager extends AbstractProtocolManager<MqttMessage, MQ
return this; return this;
} }
public int getTopicAliasMaximum() {
return topicAliasMaximum;
}
public MQTTProtocolManager setTopicAliasMaximum(int topicAliasMaximum) {
this.topicAliasMaximum = topicAliasMaximum;
return this;
}
public int getReceiveMaximum() {
return receiveMaximum;
}
public MQTTProtocolManager setReceiveMaximum(int receiveMaximum) {
this.receiveMaximum = receiveMaximum;
return this;
}
public int getMaximumPacketSize() {
return maximumPacketSize;
}
public MQTTProtocolManager setMaximumPacketSize(int maximumPacketSize) {
this.maximumPacketSize = maximumPacketSize;
return this;
}
public int getServerKeepAlive() {
return serverKeepAlive;
}
public MQTTProtocolManager setServerKeepAlive(int serverKeepAlive) {
this.serverKeepAlive = serverKeepAlive;
return this;
}
@Override @Override
public void onNotification(Notification notification) { public void onNotification(Notification notification) {
if (!(notification.getType() instanceof CoreNotificationType)) if (!(notification.getType() instanceof CoreNotificationType))
@ -98,7 +142,7 @@ public class MQTTProtocolManager extends AbstractProtocolManager<MqttMessage, MQ
SimpleString protocolName = props.getSimpleStringProperty(ManagementHelper.HDR_PROTOCOL_NAME); SimpleString protocolName = props.getSimpleStringProperty(ManagementHelper.HDR_PROTOCOL_NAME);
//Only process SESSION_CREATED notifications for the MQTT protocol //Only process SESSION_CREATED notifications for the MQTT protocol
if (protocolName == null || !protocolName.toString().equals(MQTTProtocolManagerFactory.MQTT_PROTOCOL_NAME)) if (protocolName == null || !protocolName.toString().startsWith(MQTTProtocolManagerFactory.MQTT_PROTOCOL_NAME))
return; return;
int distance = props.getIntProperty(ManagementHelper.HDR_DISTANCE); int distance = props.getIntProperty(ManagementHelper.HDR_DISTANCE);
@ -133,29 +177,48 @@ public class MQTTProtocolManager extends AbstractProtocolManager<MqttMessage, MQ
} }
public void scanSessions() { public void scanSessions() {
if (defaultMqttSessionExpiryInterval == -1) { List<String> toRemove = new ArrayList();
log.debug("sessionExpiryInterval is -1 so skipping check"); for (Map.Entry<String, MQTTSessionState> entry : sessionStates.entrySet()) {
} else { MQTTSessionState state = entry.getValue();
for (Map.Entry<String, MQTTSessionState> entry : sessionStates.entrySet()) { logger.debugf("Inspecting session: %s", state);
MQTTSessionState state = entry.getValue(); int sessionExpiryInterval = getSessionExpiryInterval(state);
if (log.isDebugEnabled()) { if (!state.isAttached() && sessionExpiryInterval > 0 && state.getDisconnectedTime() + (sessionExpiryInterval * 1000) < System.currentTimeMillis()) {
log.debug("Inspecting session state: " + state); toRemove.add(entry.getKey());
} }
if (!state.getAttached() && state.getDisconnectedTime() + (defaultMqttSessionExpiryInterval * 1000) < System.currentTimeMillis()) { if (!state.isAttached() && state.isFailed() && !state.isWillSent() && state.getWillDelayInterval() > 0 && state.getDisconnectedTime() + (state.getWillDelayInterval() * 1000) < System.currentTimeMillis()) {
if (log.isDebugEnabled()) { state.getSession().sendWillMessage();
log.debug("Removing expired session state: " + state);
}
sessionStates.remove(entry.getKey());
}
} }
} }
for (String key : toRemove) {
logger.debugf("Removing state for session: %s", key);
MQTTSessionState state = sessionStates.remove(key);
if (state != null && !state.isAttached() && state.isFailed() && !state.isWillSent()) {
state.getSession().sendWillMessage();
}
}
}
private int getSessionExpiryInterval(MQTTSessionState state) {
int sessionExpiryInterval;
if (state.getClientSessionExpiryInterval() == 0) {
sessionExpiryInterval = getDefaultMqttSessionExpiryInterval();
} else {
sessionExpiryInterval = state.getClientSessionExpiryInterval();
}
return sessionExpiryInterval;
} }
@Override @Override
public ConnectionEntry createConnectionEntry(Acceptor acceptorUsed, Connection connection) { public ConnectionEntry createConnectionEntry(Acceptor acceptorUsed, Connection connection) {
try { try {
MQTTConnection mqttConnection = new MQTTConnection(connection); MQTTConnection mqttConnection = new MQTTConnection(connection);
ConnectionEntry entry = new ConnectionEntry(mqttConnection, null, System.currentTimeMillis(), MQTTUtil.DEFAULT_KEEP_ALIVE_FREQUENCY); /*
* We must adjust the keep-alive because MQTT communicates keep-alive values in *seconds*, but the broker uses
* *milliseconds*. Also, the connection keep-alive is effectively "one and a half times" the configured
* keep-alive value. See [MQTT-3.1.2-22].
*/
ConnectionEntry entry = new ConnectionEntry(mqttConnection, null, System.currentTimeMillis(), getServerKeepAlive() == -1 || getServerKeepAlive() == 0 ? -1 : getServerKeepAlive() * MQTTUtil.KEEP_ALIVE_ADJUSTMENT);
NettyServerConnection nettyConnection = ((NettyServerConnection) connection); NettyServerConnection nettyConnection = ((NettyServerConnection) connection);
MQTTProtocolHandler protocolHandler = nettyConnection.getChannel().pipeline().get(MQTTProtocolHandler.class); MQTTProtocolHandler protocolHandler = nettyConnection.getChannel().pipeline().get(MQTTProtocolHandler.class);
@ -172,11 +235,6 @@ public class MQTTProtocolManager extends AbstractProtocolManager<MqttMessage, MQ
return false; return false;
} }
@Override
public void removeHandler(String name) {
// TODO add support for handlers
}
@Override @Override
public void handleBuffer(RemotingConnection connection, ActiveMQBuffer buffer) { public void handleBuffer(RemotingConnection connection, ActiveMQBuffer buffer) {
connection.bufferReceived(connection.getID(), buffer); connection.bufferReceived(connection.getID(), buffer);
@ -185,45 +243,63 @@ public class MQTTProtocolManager extends AbstractProtocolManager<MqttMessage, MQ
@Override @Override
public void addChannelHandlers(ChannelPipeline pipeline) { public void addChannelHandlers(ChannelPipeline pipeline) {
pipeline.addLast(MqttEncoder.INSTANCE); pipeline.addLast(MqttEncoder.INSTANCE);
pipeline.addLast(new MqttDecoder(MQTTUtil.MAX_MESSAGE_SIZE)); /*
* If we use the value from getMaximumPacketSize() here anytime a client sends a packet that's too large it
* will receive a DISCONNECT with a reason code of 0x81 instead of 0x95 like it should according to the spec.
* See https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901086:
*
* If a Server receives a packet whose size exceeds this limit, this is a Protocol Error, the Server uses
* DISCONNECT with Reason Code 0x95 (Packet too large)...
*
* Therefore we check manually in org.apache.activemq.artemis.core.protocol.mqtt.MQTTProtocolHandler.handlePublish
*/
pipeline.addLast(new MqttDecoder(MQTTUtil.MAX_PACKET_SIZE));
pipeline.addLast(new MQTTProtocolHandler(server, this)); pipeline.addLast(new MQTTProtocolHandler(server, this));
} }
/** /**
* The protocol handler passes us an 8 byte long array from the transport. We sniff these first 8 bytes to see * Relevant portions of the specs we support:
* if they match the first 8 bytes from MQTT Connect packet. In many other protocols the protocol name is the first * MQTT 3.1 - https://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html#connect
* thing sent on the wire. However, in MQTT the protocol name doesn't come until later on in the CONNECT packet. * MQTT 3.1.1 - http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718028
* * MQTT 5 - https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901033
* In order to fully identify MQTT protocol via protocol name, we need up to 12 bytes. However, we can use other
* information from the connect packet to infer that the MQTT protocol is being used. This is enough to identify MQTT
* and add the Netty codec in the pipeline. The Netty codec takes care of things from here.
*
* MQTT CONNECT PACKET: See MQTT 3.1.1 Spec for more info.
*
* Byte 1: Fixed Header Packet Type. 0b0001000 (16) = MQTT Connect
* Byte 2-[N]: Remaining length of the Connect Packet (encoded with 1-4 bytes).
*
* The next set of bytes represents the UTF8 encoded string MQTT (MQTT 3.1.1) or MQIsdp (MQTT 3.1)
* Byte N: UTF8 MSB must be 0
* Byte N+1: UTF8 LSB must be (4(MQTT) or 6(MQIsdp))
* Byte N+1: M (first char from the protocol name).
*
* Max no bytes used in the sequence = 8.
*/ */
@Override @Override
public boolean isProtocol(byte[] array) { public boolean isProtocol(byte[] array) {
ByteBuf buf = Unpooled.wrappedBuffer(array); ByteBuf buf = Unpooled.wrappedBuffer(array);
if (!(buf.readByte() == 16 && validateRemainingLength(buf) && buf.readByte() == (byte) 0)) return false; // Parse "fixed header"
if (!(readByte(buf) == 16 && validateRemainingLength(buf) && readByte(buf) == (byte) 0)) {
return false;
}
/*
* Parse the protocol name by looking at the length LSB and the next 2 bytes (which should be "MQ").
* This should be 4 for MQTT 5 & 3.1.1 because they both use "MQTT"
* This should be or 6 for MQTT 3.1 because it uses "MQIsdp"
*/
byte b = readByte(buf);
if ((b == 4 || b == 6) &&
(readByte(buf) != 77 || // M
readByte(buf) != 81)) { // Q
return false;
}
return true;
}
byte readByte(ByteBuf buf) {
byte b = buf.readByte(); byte b = buf.readByte();
return ((b == 4 || b == 6) && (buf.readByte() == 77)); if (log.isTraceEnabled()) {
log.trace(String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0'));
}
return b;
} }
private boolean validateRemainingLength(ByteBuf buffer) { private boolean validateRemainingLength(ByteBuf buffer) {
byte msb = (byte) 0b10000000; byte msb = (byte) 0b10000000;
for (byte i = 0; i < 4; i++) { for (byte i = 0; i < 4; i++) {
if ((buffer.readByte() & msb) != msb) if ((readByte(buffer) & msb) != msb)
return true; return true;
} }
return false; return false;
@ -281,6 +357,9 @@ public class MQTTProtocolManager extends AbstractProtocolManager<MqttMessage, MQ
} }
public MQTTSessionState removeSessionState(String clientId) { public MQTTSessionState removeSessionState(String clientId) {
if (clientId == null) {
return null;
}
return sessionStates.remove(clientId); return sessionStates.remove(clientId);
} }

View File

@ -67,7 +67,6 @@ public class MQTTProtocolManagerFactory extends AbstractProtocolManagerFactory<M
return MODULE_NAME; return MODULE_NAME;
} }
@Override @Override
public void loadProtocolServices(ActiveMQServer server, List<ActiveMQComponent> services) { public void loadProtocolServices(ActiveMQServer server, List<ActiveMQComponent> services) {
services.add(new MQTTPeriodicTasks(server, server.getScheduledPool())); services.add(new MQTTPeriodicTasks(server, server.getScheduledPool()));
@ -76,7 +75,7 @@ public class MQTTProtocolManagerFactory extends AbstractProtocolManagerFactory<M
public class MQTTPeriodicTasks extends ActiveMQScheduledComponent { public class MQTTPeriodicTasks extends ActiveMQScheduledComponent {
final ActiveMQServer server; final ActiveMQServer server;
public MQTTPeriodicTasks(ActiveMQServer server, ScheduledExecutorService scheduledExecutorService) { public MQTTPeriodicTasks(ActiveMQServer server, ScheduledExecutorService scheduledExecutorService) {
super(scheduledExecutorService, null, 5, TimeUnit.SECONDS, false); super(scheduledExecutorService, null, server.getConfiguration().getMqttSessionScanInterval(), TimeUnit.MILLISECONDS, false);
this.server = server; this.server = server;
} }
@Override @Override

View File

@ -17,12 +17,22 @@
package org.apache.activemq.artemis.core.protocol.mqtt; package org.apache.activemq.artemis.core.protocol.mqtt;
import java.util.Arrays;
import java.util.List;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufUtil; import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.EmptyByteBuf; import io.netty.buffer.EmptyByteBuf;
import io.netty.handler.codec.mqtt.MqttFixedHeader;
import io.netty.handler.codec.mqtt.MqttMessageType;
import io.netty.handler.codec.mqtt.MqttProperties;
import io.netty.handler.codec.mqtt.MqttPublishMessage;
import io.netty.handler.codec.mqtt.MqttPublishVariableHeader;
import io.netty.handler.codec.mqtt.MqttQoS;
import org.apache.activemq.artemis.api.core.ActiveMQBuffer; import org.apache.activemq.artemis.api.core.ActiveMQBuffer;
import org.apache.activemq.artemis.api.core.ActiveMQIllegalStateException; import org.apache.activemq.artemis.api.core.ActiveMQIllegalStateException;
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException;
import org.apache.activemq.artemis.api.core.ICoreMessage; import org.apache.activemq.artemis.api.core.ICoreMessage;
import org.apache.activemq.artemis.api.core.Message; import org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.api.core.Pair; import org.apache.activemq.artemis.api.core.Pair;
@ -30,13 +40,28 @@ import org.apache.activemq.artemis.api.core.QueueConfiguration;
import org.apache.activemq.artemis.api.core.RoutingType; import org.apache.activemq.artemis.api.core.RoutingType;
import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.core.io.IOCallback; import org.apache.activemq.artemis.core.io.IOCallback;
import org.apache.activemq.artemis.core.protocol.mqtt.exceptions.DisconnectException;
import org.apache.activemq.artemis.core.server.Queue; import org.apache.activemq.artemis.core.server.Queue;
import org.apache.activemq.artemis.core.server.ServerConsumer; import org.apache.activemq.artemis.core.server.ServerConsumer;
import org.apache.activemq.artemis.core.server.impl.ServerSessionImpl; import org.apache.activemq.artemis.core.server.impl.ServerSessionImpl;
import org.apache.activemq.artemis.core.transaction.Transaction; import org.apache.activemq.artemis.core.transaction.Transaction;
import org.apache.activemq.artemis.reader.MessageUtil;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.CONTENT_TYPE;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.CORRELATION_DATA;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.PAYLOAD_FORMAT_INDICATOR;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.PUBLICATION_EXPIRY_INTERVAL;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.RESPONSE_TOPIC;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.SUBSCRIPTION_IDENTIFIER;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.TOPIC_ALIAS;
import static org.apache.activemq.artemis.core.protocol.mqtt.MQTTUtil.MQTT_CONTENT_TYPE_KEY;
import static org.apache.activemq.artemis.core.protocol.mqtt.MQTTUtil.MQTT_CORRELATION_DATA_KEY;
import static org.apache.activemq.artemis.core.protocol.mqtt.MQTTUtil.MQTT_MESSAGE_RETAIN_KEY; import static org.apache.activemq.artemis.core.protocol.mqtt.MQTTUtil.MQTT_MESSAGE_RETAIN_KEY;
import static org.apache.activemq.artemis.core.protocol.mqtt.MQTTUtil.MQTT_PAYLOAD_FORMAT_INDICATOR_KEY;
import static org.apache.activemq.artemis.core.protocol.mqtt.MQTTUtil.MQTT_RESPONSE_TOPIC_KEY;
import static org.apache.activemq.artemis.core.protocol.mqtt.MQTTUtil.MQTT_USER_PROPERTY_EXISTS_KEY;
import static org.apache.activemq.artemis.core.protocol.mqtt.MQTTUtil.MQTT_USER_PROPERTY_KEY_PREFIX_SIMPLE;
/** /**
* Handles MQTT Exactly Once (QoS level 2) Protocol. * Handles MQTT Exactly Once (QoS level 2) Protocol.
@ -63,8 +88,8 @@ public class MQTTPublishManager {
this.session = session; this.session = session;
} }
synchronized void start() throws Exception { synchronized void start() {
this.state = session.getSessionState(); this.state = session.getState();
this.outboundStore = state.getOutboundStore(); this.outboundStore = state.getOutboundStore();
} }
@ -95,7 +120,7 @@ public class MQTTPublishManager {
} }
private SimpleString createManagementAddress() { private SimpleString createManagementAddress() {
return new SimpleString(MQTTUtil.MANAGEMENT_QUEUE_PREFIX + session.getSessionState().getClientId()); return new SimpleString(MQTTUtil.MANAGEMENT_QUEUE_PREFIX + session.getState().getClientId());
} }
private void createManagementQueue() throws Exception { private void createManagementQueue() throws Exception {
@ -112,11 +137,11 @@ public class MQTTPublishManager {
} }
/** /**
* Since MQTT Subscriptions can over lap; a client may receive the same message twice. When this happens the client * Since MQTT Subscriptions can overlap, a client may receive the same message twice. When this happens the client
* returns a PubRec or PubAck with ID. But we need to know which consumer to ack, since we only have the ID to go on we * returns a PubRec or PubAck with ID. But we need to know which consumer to ack, since we only have the ID to go on
* are not able to decide which consumer to ack. Instead we send MQTT messages with different IDs and store a reference * we are not able to decide which consumer to ack. Instead we send MQTT messages with different IDs and store a
* to original ID and consumer in the Session state. This way we can look up the consumer Id and the message Id from * reference to original ID and consumer in the Session state. This way we can look up the consumer Id and the
* the PubAck or PubRec message id. * * message Id from the PubAck or PubRec message id.
*/ */
protected void sendMessage(ICoreMessage message, ServerConsumer consumer, int deliveryCount) throws Exception { protected void sendMessage(ICoreMessage message, ServerConsumer consumer, int deliveryCount) throws Exception {
// This is to allow retries of PubRel. // This is to allow retries of PubRel.
@ -125,12 +150,13 @@ public class MQTTPublishManager {
} else { } else {
int qos = decideQoS(message, consumer); int qos = decideQoS(message, consumer);
if (qos == 0) { if (qos == 0) {
sendServerMessage((int) message.getMessageID(), message, deliveryCount, qos); if (publishToClient((int) message.getMessageID(), message, deliveryCount, qos, consumer.getID())) {
session.getServerSession().individualAcknowledge(consumer.getID(), message.getMessageID()); session.getServerSession().individualAcknowledge(consumer.getID(), message.getMessageID());
}
} else if (qos == 1 || qos == 2) { } else if (qos == 1 || qos == 2) {
int mqttid = outboundStore.generateMqttId(message.getMessageID(), consumer.getID()); int mqttid = outboundStore.generateMqttId(message.getMessageID(), consumer.getID());
outboundStore.publish(mqttid, message.getMessageID(), consumer.getID()); outboundStore.publish(mqttid, message.getMessageID(), consumer.getID());
sendServerMessage(mqttid, message, deliveryCount, qos); publishToClient(mqttid, message, deliveryCount, qos, consumer.getID());
} else { } else {
// Client must have disconnected and it's Subscription QoS cleared // Client must have disconnected and it's Subscription QoS cleared
consumer.individualCancel(message.getMessageID(), false); consumer.individualCancel(message.getMessageID(), false);
@ -138,29 +164,44 @@ public class MQTTPublishManager {
} }
} }
// INBOUND
void handleMessage(int messageId, String topic, int qos, ByteBuf payload, boolean retain) throws Exception {
sendInternal(messageId, topic, qos, payload, retain, false);
}
/** /**
* Sends a message either on behalf of the client or on behalf of the broker (Will Messages) * Sends a message either on behalf of the client or on behalf of the broker (Will Messages)
* @param messageId *
* @param topic
* @param qos
* @param payload
* @param retain
* @param internal if true means on behalf of the broker (skips authorisation) and does not return ack. * @param internal if true means on behalf of the broker (skips authorisation) and does not return ack.
* @throws Exception * @throws Exception
*/ */
void sendInternal(int messageId, String topic, int qos, ByteBuf payload, boolean retain, boolean internal) throws Exception { void sendToQueue(MqttPublishMessage message, boolean internal) throws Exception {
synchronized (lock) { synchronized (lock) {
Message serverMessage = MQTTUtil.createServerMessageFromByteBuf(session, topic, retain, qos, payload); String topic = message.variableHeader().topicName();
if (session.is5()) {
Integer alias = MQTTUtil.getProperty(Integer.class, message.variableHeader().properties(), TOPIC_ALIAS);
Integer topicAliasMax = session.getProtocolManager().getTopicAliasMaximum();
if (alias != null) {
if (alias == 0) {
// [MQTT-3.3.2-8]
throw new DisconnectException(MQTTReasonCodes.TOPIC_ALIAS_INVALID);
} else if (topicAliasMax != null && alias > topicAliasMax) {
// [MQTT-3.3.2-9]
throw new DisconnectException(MQTTReasonCodes.TOPIC_ALIAS_INVALID);
} else {
topic = session.getState().getClientTopicAlias(alias);
if (topic == null) {
topic = message.variableHeader().topicName();
session.getState().addClientTopicAlias(alias, topic);
}
}
} else {
topic = message.variableHeader().topicName();
}
}
Message serverMessage = MQTTUtil.createServerMessageFromByteBuf(session, topic, message);
int qos = message.fixedHeader().qosLevel().value();
if (qos > 0) { if (qos > 0) {
serverMessage.setDurable(MQTTUtil.DURABLE_MESSAGES); serverMessage.setDurable(MQTTUtil.DURABLE_MESSAGES);
} }
int messageId = message.variableHeader().packetId();
if (qos < 2 || !state.getPubRec().contains(messageId)) { if (qos < 2 || !state.getPubRec().contains(messageId)) {
if (qos == 2 && !internal) if (qos == 2 && !internal)
state.getPubRec().add(messageId); state.getPubRec().add(messageId);
@ -173,13 +214,29 @@ public class MQTTPublishManager {
session.getServerSession().send(tx, serverMessage, true, false); session.getServerSession().send(tx, serverMessage, true, false);
} }
if (retain) { if (message.fixedHeader().isRetain()) {
ByteBuf payload = message.payload();
boolean reset = payload instanceof EmptyByteBuf || payload.capacity() == 0; boolean reset = payload instanceof EmptyByteBuf || payload.capacity() == 0;
session.getRetainMessageManager().handleRetainedMessage(serverMessage, topic, reset, tx); session.getRetainMessageManager().handleRetainedMessage(serverMessage, topic, reset, tx);
} }
tx.commit(); tx.commit();
} catch (ActiveMQSecurityException e) {
tx.rollback();
if (session.is5()) {
sendMessageAck(internal, qos, messageId, MQTTReasonCodes.NOT_AUTHORIZED);
return;
} else {
/*
* For MQTT 3.x clients:
*
* [MQTT-3.3.5-2] If a Server implementation does not authorize a PUBLISH to be performed by a Client;
* it has no way of informing that Client. It MUST either make a positive acknowledgement, according
* to the normal QoS rules, or close the Network Connection
*/
throw e;
}
} catch (Throwable t) { } catch (Throwable t) {
logger.warn(t.getMessage(), t); MQTTLogger.LOGGER.failedToPublishMqttMessage(t.getMessage(), t);
tx.rollback(); tx.rollback();
throw t; throw t;
} }
@ -188,9 +245,19 @@ public class MQTTPublishManager {
} }
} }
private void sendMessageAck(boolean internal, int qos, int messageId, byte reasonCode) {
if (!internal) {
if (qos == 1) {
session.getProtocolHandler().sendPubAck(messageId, reasonCode);
} else if (qos == 2) {
session.getProtocolHandler().sendPubRec(messageId, reasonCode);
}
}
}
void sendPubRelMessage(Message message) { void sendPubRelMessage(Message message) {
int messageId = message.getIntProperty(MQTTUtil.MQTT_MESSAGE_ID_KEY); int messageId = message.getIntProperty(MQTTUtil.MQTT_MESSAGE_ID_KEY);
session.getSessionState().getOutboundStore().publishReleasedSent(messageId, message.getMessageID()); session.getState().getOutboundStore().publishReleasedSent(messageId, message.getMessageID());
session.getProtocolHandler().sendPubRel(messageId); session.getProtocolHandler().sendPubRel(messageId);
} }
@ -211,18 +278,31 @@ public class MQTTPublishManager {
//send the management message via the internal server session to bypass security. //send the management message via the internal server session to bypass security.
session.getInternalServerSession().send(m, true); session.getInternalServerSession().send(m, true);
session.getServerSession().individualAcknowledge(ref.getB(), ref.getA()); session.getServerSession().individualAcknowledge(ref.getB(), ref.getA());
releaseFlowControl(ref.getB());
} else { } else {
session.getProtocolHandler().sendPubRel(messageId); session.getProtocolHandler().sendPubRel(messageId);
} }
} catch (ActiveMQIllegalStateException e) { } catch (ActiveMQIllegalStateException e) {
log.warn("MQTT Client(" + session.getSessionState().getClientId() + ") attempted to Ack already Ack'd message"); MQTTLogger.LOGGER.failedToAckMessage(session.getState().getClientId(), e);
}
}
/**
* Once we get an acknowledgement for a QoS 1 or 2 message we allow messages to flow
*
* @param consumerId
*/
private void releaseFlowControl(Long consumerId) {
ServerConsumer consumer = session.getServerSession().locateConsumer(consumerId);
if (consumer != null) {
consumer.promptDelivery();
} }
} }
void handlePubComp(int messageId) throws Exception { void handlePubComp(int messageId) throws Exception {
Pair<Long, Long> ref = session.getState().getOutboundStore().publishComplete(messageId); Pair<Long, Long> ref = session.getState().getOutboundStore().publishComplete(messageId);
if (ref != null) { if (ref != null) {
//ack the message via the internal server session to bypass security. // ack the message via the internal server session to bypass security.
session.getInternalServerSession().individualAcknowledge(managementConsumer.getID(), ref.getA()); session.getInternalServerSession().individualAcknowledge(managementConsumer.getID(), ref.getA());
} }
} }
@ -231,13 +311,7 @@ public class MQTTPublishManager {
session.getServer().getStorageManager().afterCompleteOperations(new IOCallback() { session.getServer().getStorageManager().afterCompleteOperations(new IOCallback() {
@Override @Override
public void done() { public void done() {
if (!internal) { sendMessageAck(internal, qos, messageId, MQTTReasonCodes.SUCCESS);
if (qos == 1) {
session.getProtocolHandler().sendPubAck(messageId);
} else if (qos == 2) {
session.getProtocolHandler().sendPubRec(messageId);
}
}
} }
@Override @Override
@ -259,15 +333,15 @@ public class MQTTPublishManager {
Pair<Long, Long> ref = outboundStore.publishAckd(messageId); Pair<Long, Long> ref = outboundStore.publishAckd(messageId);
if (ref != null) { if (ref != null) {
session.getServerSession().individualAcknowledge(ref.getB(), ref.getA()); session.getServerSession().individualAcknowledge(ref.getB(), ref.getA());
releaseFlowControl(ref.getB());
} }
} catch (ActiveMQIllegalStateException e) { } catch (ActiveMQIllegalStateException e) {
log.warn("MQTT Client(" + session.getSessionState().getClientId() + ") attempted to Ack already Ack'd message"); log.warn("MQTT Client(" + session.getState().getClientId() + ") attempted to Ack already Ack'd message");
} }
} }
private void sendServerMessage(int messageId, ICoreMessage message, int deliveryCount, int qos) { private boolean publishToClient(int messageId, ICoreMessage message, int deliveryCount, int qos, long consumerId) throws Exception {
String address = MQTTUtil.convertCoreAddressFilterToMQTT(message.getAddress() == null ? "" : message.getAddress(), session.getWildcardConfiguration()); String address = MQTTUtil.convertCoreAddressToMqttTopicFilter(message.getAddress() == null ? "" : message.getAddress(), session.getWildcardConfiguration());
boolean isRetain = message.getBooleanProperty(MQTT_MESSAGE_RETAIN_KEY);
ByteBuf payload; ByteBuf payload;
switch (message.getType()) { switch (message.getType()) {
@ -284,11 +358,110 @@ public class MQTTPublishManager {
payload.writeBytes(bodyBuffer.byteBuf()); payload.writeBytes(bodyBuffer.byteBuf());
break; break;
} }
session.getProtocolHandler().send(messageId, address, qos, isRetain, payload, deliveryCount);
// [MQTT-3.3.1-2] The DUP flag MUST be set to 0 for all QoS 0 messages.
boolean redelivery = qos == 0 ? false : (deliveryCount > 1);
boolean isRetain = message.getBooleanProperty(MQTT_MESSAGE_RETAIN_KEY);
MqttProperties mqttProperties = getPublishProperties(message);
if (session.is5()) {
if (session.getState().getSubscription(message.getAddress()) != null && !session.getState().getSubscription(message.getAddress()).option().isRetainAsPublished()) {
isRetain = false;
}
// [MQTT-3.8.3-3] remove property used for no-local implementation
message.removeProperty(MessageUtil.CONNECTION_ID_PROPERTY_NAME);
if (session.getState().getClientTopicAliasMaximum() != null) {
Integer alias = session.getState().getServerTopicAlias(address);
if (alias == null) {
alias = session.getState().addServerTopicAlias(address);
if (alias != null) {
mqttProperties.add(new MqttProperties.IntegerProperty(TOPIC_ALIAS.value(), alias));
}
} else {
mqttProperties.add(new MqttProperties.IntegerProperty(TOPIC_ALIAS.value(), alias));
address = "";
}
}
}
int remainingLength = MQTTUtil.calculateRemainingLength(address, mqttProperties, payload);
MqttFixedHeader header = new MqttFixedHeader(MqttMessageType.PUBLISH, redelivery, MqttQoS.valueOf(qos), isRetain, remainingLength);
MqttPublishVariableHeader varHeader = new MqttPublishVariableHeader(address, messageId, mqttProperties);
MqttPublishMessage publish = new MqttPublishMessage(header, varHeader, payload);
int maxSize = session.getState().getClientMaxPacketSize();
if (session.is5() && maxSize != 0) {
int size = MQTTUtil.calculateMessageSize(publish);
if (size > maxSize) {
/*
* [MQTT-3.1.2-25] Where a Packet is too large to send, the Server MUST discard it without sending it and then
* behave as if it had completed sending that Application Message
*/
logger.debugf("Not sending message %s to client as its size (%d) exceeds the max (%d)", message, size, maxSize);
session.getServerSession().individualAcknowledge(consumerId, message.getMessageID());
return false;
}
}
session.getProtocolHandler().sendToClient(publish);
return true;
}
private MqttProperties getPublishProperties(ICoreMessage message) {
MqttProperties props = new MqttProperties();
if (message.containsProperty(MQTT_PAYLOAD_FORMAT_INDICATOR_KEY)) {
props.add(new MqttProperties.IntegerProperty(PAYLOAD_FORMAT_INDICATOR.value(), message.getIntProperty(MQTT_PAYLOAD_FORMAT_INDICATOR_KEY)));
}
if (message.containsProperty(MQTT_RESPONSE_TOPIC_KEY)) {
props.add(new MqttProperties.StringProperty(RESPONSE_TOPIC.value(), message.getStringProperty(MQTT_RESPONSE_TOPIC_KEY)));
}
if (message.containsProperty(MQTT_CORRELATION_DATA_KEY)) {
props.add(new MqttProperties.BinaryProperty(CORRELATION_DATA.value(), message.getBytesProperty(MQTT_CORRELATION_DATA_KEY)));
}
if (message.containsProperty(MQTT_USER_PROPERTY_EXISTS_KEY)) {
MqttProperties.StringPair[] orderedProperties = new MqttProperties.StringPair[message.getIntProperty(MQTT_USER_PROPERTY_EXISTS_KEY)];
for (SimpleString propertyName : message.getPropertyNames()) {
if (propertyName.startsWith(MQTT_USER_PROPERTY_KEY_PREFIX_SIMPLE)) {
SimpleString[] split = propertyName.split('.');
int position = Integer.valueOf(split[4].toString());
String key = propertyName.subSeq(MQTT_USER_PROPERTY_KEY_PREFIX_SIMPLE.length() + split[4].length() + 1, propertyName.length()).toString();
orderedProperties[position] = new MqttProperties.StringPair(key, message.getStringProperty(propertyName));
}
}
props.add(new MqttProperties.UserProperties(Arrays.asList(orderedProperties)));
}
if (message.containsProperty(MQTT_CONTENT_TYPE_KEY)) {
props.add(new MqttProperties.StringProperty(CONTENT_TYPE.value(), message.getStringProperty(MQTT_CONTENT_TYPE_KEY)));
}
List<Integer> subscriptionIdentifiers = session.getState().getMatchingSubscriptionIdentifiers(message.getAddress());
if (subscriptionIdentifiers != null) {
for (Integer id : subscriptionIdentifiers) {
props.add(new MqttProperties.IntegerProperty(SUBSCRIPTION_IDENTIFIER.value(), id));
}
}
if (message.getExpiration() != 0) {
/*
* [MQTT-3.3.2-6] The PUBLISH packet sent to a Client by the Server MUST contain a Message Expiry Interval set
* to the received value minus the time that the Application Message has been waiting in the Server.
*
* Therefore, calculate how much time is left until the message expires rounded to the nearest *second*.
*/
int messageExpiryInterval = (int) Math.round(((message.getExpiration() - System.currentTimeMillis()) / 1_000_000.0000) * 1000);
props.add(new MqttProperties.IntegerProperty(PUBLICATION_EXPIRY_INTERVAL.value(), messageExpiryInterval));
}
return props;
} }
private int decideQoS(Message message, ServerConsumer consumer) { private int decideQoS(Message message, ServerConsumer consumer) {
int subscriptionQoS = -1; int subscriptionQoS = -1;
try { try {
subscriptionQoS = session.getSubscriptionManager().getConsumerQoSLevels().get(consumer.getID()); subscriptionQoS = session.getSubscriptionManager().getConsumerQoSLevels().get(consumer.getID());
@ -302,8 +475,10 @@ public class MQTTPublishManager {
qos = message.getIntProperty(MQTTUtil.MQTT_QOS_LEVEL_KEY); qos = message.getIntProperty(MQTTUtil.MQTT_QOS_LEVEL_KEY);
} }
/* Subscription QoS is the maximum QoS the client is willing to receive for this subscription. If the message QoS /*
is less than the subscription QoS then use it, otherwise use the subscription qos). */ * Subscription QoS is the maximum QoS the client is willing to receive for this subscription. If the message QoS
* is less than the subscription QoS then use it, otherwise use the subscription qos).
*/
return subscriptionQoS < qos ? subscriptionQoS : qos; return subscriptionQoS < qos ? subscriptionQoS : qos;
} }
} }

View File

@ -0,0 +1,77 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.core.protocol.mqtt;
/**
* Taken mainly from https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901031
*/
public class MQTTReasonCodes {
// codes specific to MQTT 3.x
public static final byte UNACCEPTABLE_PROTOCOL_VERSION_3 = (byte) 0X01;
public static final byte IDENTIFIER_REJECTED_3 = (byte) 0x02;
public static final byte SERVER_UNAVAILABLE_3 = (byte) 0x03;
public static final byte BAD_USER_NAME_OR_PASSWORD_3 = (byte) 0x04;
public static final byte NOT_AUTHORIZED_3 = (byte) 0x05;
// codes specific to MQTT 5
public static final byte SUCCESS = (byte) 0x00;
public static final byte NORMAL_DISCONNECTION = (byte) 0x00;
public static final byte GRANTED_QOS_0 = (byte) 0x00;
public static final byte GRANTED_QOS_1 = (byte) 0x01;
public static final byte GRANTED_QOS_2 = (byte) 0x02;
public static final byte DISCONNECT_WITH_WILL_MESSAGE = (byte) 0x04;
public static final byte NO_MATCHING_SUBSCRIBERS = (byte) 0x10;
public static final byte NO_SUBSCRIPTION_EXISTED = (byte) 0x11;
public static final byte CONTINUE_AUTHENTICATION = (byte) 0x18;
public static final byte RE_AUTHENTICATE = (byte) 0x19;
public static final byte UNSPECIFIED_ERROR = (byte) 0x80;
public static final byte MALFORMED_PACKET = (byte) 0x81;
public static final byte PROTOCOL_ERROR = (byte) 0x82;
public static final byte IMPLEMENTATION_SPECIFIC_ERROR = (byte) 0x83;
public static final byte UNSUPPORTED_PROTOCOL_VERSION = (byte) 0x84;
public static final byte CLIENT_IDENTIFIER_NOT_VALID = (byte) 0x85;
public static final byte BAD_USER_NAME_OR_PASSWORD = (byte) 0x86;
public static final byte NOT_AUTHORIZED = (byte) 0x87;
public static final byte SERVER_UNAVAILABLE = (byte) 0x88;
public static final byte SERVER_BUSY = (byte) 0x89;
public static final byte BANNED = (byte) 0x8A;
public static final byte SERVER_SHUTTING_DOWN = (byte) 0x8B;
public static final byte BAD_AUTHENTICATION_METHOD = (byte) 0x8C;
public static final byte KEEP_ALIVE_TIMEOUT = (byte) 0x8D;
public static final byte SESSION_TAKEN_OVER = (byte) 0x8E;
public static final byte TOPIC_FILTER_INVALID = (byte) 0x8F;
public static final byte TOPIC_NAME_INVALID = (byte) 0x90;
public static final byte PACKET_IDENTIFIER_IN_USE = (byte) 0x91;
public static final byte PACKET_IDENTIFIER_NOT_FOUND = (byte) 0x92;
public static final byte RECEIVE_MAXIMUM_EXCEEDED = (byte) 0x93;
public static final byte TOPIC_ALIAS_INVALID = (byte) 0x94;
public static final byte PACKET_TOO_LARGE = (byte) 0x95;
public static final byte MESSAGE_RATE_TOO_HIGH = (byte) 0x96;
public static final byte QUOTA_EXCEEDED = (byte) 0x97;
public static final byte ADMINISTRATIVE_ACTION = (byte) 0x98;
public static final byte PAYLOAD_FORMAT_INVALID = (byte) 0x99;
public static final byte RETAIN_NOT_SUPPORTED = (byte) 0x9A;
public static final byte QOS_NOT_SUPPORTED = (byte) 0x9B;
public static final byte USE_ANOTHER_SERVER = (byte) 0x9C;
public static final byte SERVER_MOVED = (byte) 0x9D;
public static final byte SHARED_SUBSCRIPTIONS_NOT_SUPPORTED = (byte) 0x9E;
public static final byte CONNECTION_RATE_EXCEEDED = (byte) 0x9F;
public static final byte MAXIMUM_CONNECT_TIME = (byte) 0xA0;
public static final byte SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED = (byte) 0xA1;
public static final byte WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED = (byte) 0xA2;
}

View File

@ -18,13 +18,14 @@
package org.apache.activemq.artemis.core.protocol.mqtt; package org.apache.activemq.artemis.core.protocol.mqtt;
import io.netty.handler.codec.mqtt.MqttConnectMessage; import io.netty.handler.codec.mqtt.MqttConnectMessage;
import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
import io.netty.handler.codec.mqtt.MqttProperties; import io.netty.handler.codec.mqtt.MqttProperties;
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants;
import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.core.server.balancing.RedirectHandler; import org.apache.activemq.artemis.core.server.balancing.RedirectHandler;
import org.apache.activemq.artemis.utils.ConfigurationHelper; import org.apache.activemq.artemis.utils.ConfigurationHelper;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.SERVER_REFERENCE;
public class MQTTRedirectHandler extends RedirectHandler<MQTTRedirectContext> { public class MQTTRedirectHandler extends RedirectHandler<MQTTRedirectContext> {
protected MQTTRedirectHandler(ActiveMQServer server) { protected MQTTRedirectHandler(ActiveMQServer server) {
@ -39,10 +40,10 @@ public class MQTTRedirectHandler extends RedirectHandler<MQTTRedirectContext> {
protected void cannotRedirect(MQTTRedirectContext context) { protected void cannotRedirect(MQTTRedirectContext context) {
switch (context.getResult().getStatus()) { switch (context.getResult().getStatus()) {
case REFUSED_USE_ANOTHER: case REFUSED_USE_ANOTHER:
context.getMQTTSession().getProtocolHandler().sendConnack(MqttConnectReturnCode.CONNECTION_REFUSED_USE_ANOTHER_SERVER); context.getMQTTSession().getProtocolHandler().sendConnack(MQTTReasonCodes.USE_ANOTHER_SERVER);
break; break;
case REFUSED_UNAVAILABLE: case REFUSED_UNAVAILABLE:
context.getMQTTSession().getProtocolHandler().sendConnack(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE); context.getMQTTSession().getProtocolHandler().sendConnack(MQTTReasonCodes.SERVER_UNAVAILABLE);
break; break;
} }
context.getMQTTSession().getProtocolHandler().disconnect(true); context.getMQTTSession().getProtocolHandler().disconnect(true);
@ -54,9 +55,9 @@ public class MQTTRedirectHandler extends RedirectHandler<MQTTRedirectContext> {
int port = ConfigurationHelper.getIntProperty(TransportConstants.PORT_PROP_NAME, TransportConstants.DEFAULT_PORT, context.getTarget().getConnector().getParams()); int port = ConfigurationHelper.getIntProperty(TransportConstants.PORT_PROP_NAME, TransportConstants.DEFAULT_PORT, context.getTarget().getConnector().getParams());
MqttProperties mqttProperties = new MqttProperties(); MqttProperties mqttProperties = new MqttProperties();
mqttProperties.add(new MqttProperties.StringProperty(MqttProperties.MqttPropertyType.SERVER_REFERENCE.value(), String.format("%s:%d", host, port))); mqttProperties.add(new MqttProperties.StringProperty(SERVER_REFERENCE.value(), String.format("%s:%d", host, port)));
context.getMQTTSession().getProtocolHandler().sendConnack(MqttConnectReturnCode.CONNECTION_REFUSED_USE_ANOTHER_SERVER, mqttProperties); context.getMQTTSession().getProtocolHandler().sendConnack(MQTTReasonCodes.USE_ANOTHER_SERVER, mqttProperties);
context.getMQTTSession().getProtocolHandler().disconnect(true); context.getMQTTSession().getProtocolHandler().disconnect(true);
} }
} }

View File

@ -28,10 +28,12 @@ import org.apache.activemq.artemis.core.server.RoutingContext;
import org.apache.activemq.artemis.core.server.impl.RoutingContextImpl; import org.apache.activemq.artemis.core.server.impl.RoutingContextImpl;
import org.apache.activemq.artemis.core.transaction.Transaction; import org.apache.activemq.artemis.core.transaction.Transaction;
import org.apache.activemq.artemis.utils.collections.LinkedListIterator; import org.apache.activemq.artemis.utils.collections.LinkedListIterator;
import org.jboss.logging.Logger;
public class MQTTRetainMessageManager { public class MQTTRetainMessageManager {
private MQTTSession session; private MQTTSession session;
private static final Logger logger = Logger.getLogger(MQTTRetainMessageManager.class);
public MQTTRetainMessageManager(MQTTSession session) { public MQTTRetainMessageManager(MQTTSession session) {
this.session = session; this.session = session;
@ -47,7 +49,7 @@ public class MQTTRetainMessageManager {
* the retained queue and the previous retain message consumed to remove it from the queue. * the retained queue and the previous retain message consumed to remove it from the queue.
*/ */
void handleRetainedMessage(Message messageParameter, String address, boolean reset, Transaction tx) throws Exception { void handleRetainedMessage(Message messageParameter, String address, boolean reset, Transaction tx) throws Exception {
SimpleString retainAddress = new SimpleString(MQTTUtil.convertMQTTAddressFilterToCoreRetain(address, session.getWildcardConfiguration())); SimpleString retainAddress = new SimpleString(MQTTUtil.convertMqttTopicFilterToCoreAddress(MQTTUtil.MQTT_RETAIN_ADDRESS_PREFIX, address, session.getWildcardConfiguration()));
Queue queue = session.getServer().locateQueue(retainAddress); Queue queue = session.getServer().locateQueue(retainAddress);
if (queue == null) { if (queue == null) {
@ -63,10 +65,9 @@ public class MQTTRetainMessageManager {
} }
// SEND to Queue.
void addRetainedMessagesToQueue(Queue queue, String address) throws Exception { void addRetainedMessagesToQueue(Queue queue, String address) throws Exception {
// The address filter that matches all retained message queues. // The address filter that matches all retained message queues.
String retainAddress = MQTTUtil.convertMQTTAddressFilterToCoreRetain(address, session.getWildcardConfiguration()); String retainAddress = MQTTUtil.convertMqttTopicFilterToCoreAddress(MQTTUtil.MQTT_RETAIN_ADDRESS_PREFIX, address, session.getWildcardConfiguration());
BindingQueryResult bindingQueryResult = session.getServerSession().executeBindingQuery(new SimpleString(retainAddress)); BindingQueryResult bindingQueryResult = session.getServerSession().executeBindingQuery(new SimpleString(retainAddress));
// Iterate over all matching retain queues and add the queue // Iterate over all matching retain queues and add the queue

View File

@ -19,14 +19,20 @@ package org.apache.activemq.artemis.core.protocol.mqtt;
import java.util.UUID; import java.util.UUID;
import io.netty.handler.codec.mqtt.MqttMessageBuilders;
import io.netty.handler.codec.mqtt.MqttPublishMessage;
import io.netty.handler.codec.mqtt.MqttQoS;
import org.apache.activemq.artemis.core.config.WildcardConfiguration; import org.apache.activemq.artemis.core.config.WildcardConfiguration;
import org.apache.activemq.artemis.core.persistence.CoreMessageObjectPools; import org.apache.activemq.artemis.core.persistence.CoreMessageObjectPools;
import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.core.server.impl.ServerSessionImpl; import org.apache.activemq.artemis.core.server.impl.ServerSessionImpl;
import org.apache.activemq.artemis.spi.core.protocol.SessionCallback; import org.apache.activemq.artemis.spi.core.protocol.SessionCallback;
import org.jboss.logging.Logger;
public class MQTTSession { public class MQTTSession {
private static final Logger logger = Logger.getLogger(MQTTSession.class);
private final String id = UUID.randomUUID().toString(); private final String id = UUID.randomUUID().toString();
private MQTTProtocolHandler protocolHandler; private MQTTProtocolHandler protocolHandler;
@ -51,8 +57,6 @@ public class MQTTSession {
private boolean stopped = false; private boolean stopped = false;
private MQTTLogger log = MQTTLogger.LOGGER;
private MQTTProtocolManager protocolManager; private MQTTProtocolManager protocolManager;
private boolean clean; private boolean clean;
@ -61,6 +65,10 @@ public class MQTTSession {
private CoreMessageObjectPools coreMessageObjectPools = new CoreMessageObjectPools(); private CoreMessageObjectPools coreMessageObjectPools = new CoreMessageObjectPools();
private boolean five = false;
private boolean usingServerKeepAlive = false;
public MQTTSession(MQTTProtocolHandler protocolHandler, public MQTTSession(MQTTProtocolHandler protocolHandler,
MQTTConnection connection, MQTTConnection connection,
MQTTProtocolManager protocolManager, MQTTProtocolManager protocolManager,
@ -79,7 +87,7 @@ public class MQTTSession {
state = MQTTSessionState.DEFAULT; state = MQTTSessionState.DEFAULT;
log.debug("SESSION CREATED: " + id); logger.debugf("MQTT session created: %s", id);
} }
// Called after the client has Connected. // Called after the client has Connected.
@ -89,8 +97,9 @@ public class MQTTSession {
stopped = false; stopped = false;
} }
// TODO ensure resources are cleaned up for GC. synchronized void stop(boolean failure) throws Exception {
synchronized void stop() throws Exception { state.setFailed(failure);
if (!stopped) { if (!stopped) {
protocolHandler.stop(); protocolHandler.stop();
subscriptionManager.stop(); subscriptionManager.stop();
@ -111,9 +120,25 @@ public class MQTTSession {
state.setDisconnectedTime(System.currentTimeMillis()); state.setDisconnectedTime(System.currentTimeMillis());
} }
if (isClean()) { if (is5()) {
clean(); if (state.getClientSessionExpiryInterval() == 0) {
protocolManager.removeSessionState(connection.getClientID()); if (state.isWill() && failure) {
// If the session expires the will message must be sent no matter the will delay
sendWillMessage();
}
clean();
protocolManager.removeSessionState(connection.getClientID());
} else {
state.setDisconnectedTime(System.currentTimeMillis());
}
} else {
if (state.isWill() && failure) {
sendWillMessage();
}
if (isClean()) {
clean();
protocolManager.removeSessionState(connection.getClientID());
}
} }
} }
stopped = true; stopped = true;
@ -143,10 +168,6 @@ public class MQTTSession {
return mqttConnectionManager; return mqttConnectionManager;
} }
MQTTSessionState getSessionState() {
return state;
}
ServerSessionImpl getServerSession() { ServerSessionImpl getServerSession() {
return serverSession; return serverSession;
} }
@ -178,8 +199,9 @@ public class MQTTSession {
void setSessionState(MQTTSessionState state) { void setSessionState(MQTTSessionState state) {
this.state = state; this.state = state;
state.setAttached(true); this.state.setAttached(true);
this.state.setDisconnectedTime(0); this.state.setDisconnectedTime(0);
this.state.setSession(this);
} }
MQTTRetainMessageManager getRetainMessageManager() { MQTTRetainMessageManager getRetainMessageManager() {
@ -212,4 +234,42 @@ public class MQTTSession {
return coreMessageObjectPools; return coreMessageObjectPools;
} }
public boolean is5() {
return five;
}
public void set5(boolean five) {
this.five = five;
}
public boolean isUsingServerKeepAlive() {
return usingServerKeepAlive;
}
public void setUsingServerKeepAlive(boolean usingServerKeepAlive) {
this.usingServerKeepAlive = usingServerKeepAlive;
}
public void sendWillMessage() {
try {
MqttPublishMessage publishMessage = MqttMessageBuilders.publish()
.messageId(0)
.qos(MqttQoS.valueOf(state.getWillQoSLevel()))
.retained(state.isWillRetain())
.topicName(state.getWillTopic())
.payload(state.getWillMessage())
.build();
logger.debugf("%s sending will message: %s", this, publishMessage);
getMqttPublishManager().sendToQueue(publishMessage, true);
state.setWillSent(true);
state.setWillMessage(null);
} catch (Exception e) {
MQTTLogger.LOGGER.errorSendingWillMessage(e);
}
}
@Override
public String toString() {
return "MQTTSession[coreSessionId: " + (serverSession != null ? serverSession.getName() : "null") + "]";
}
} }

View File

@ -23,13 +23,14 @@ import org.apache.activemq.artemis.core.server.MessageReference;
import org.apache.activemq.artemis.core.server.ServerConsumer; import org.apache.activemq.artemis.core.server.ServerConsumer;
import org.apache.activemq.artemis.spi.core.protocol.SessionCallback; import org.apache.activemq.artemis.spi.core.protocol.SessionCallback;
import org.apache.activemq.artemis.spi.core.remoting.ReadyListener; import org.apache.activemq.artemis.spi.core.remoting.ReadyListener;
import org.jboss.logging.Logger;
public class MQTTSessionCallback implements SessionCallback { public class MQTTSessionCallback implements SessionCallback {
private final MQTTSession session; private final MQTTSession session;
private final MQTTConnection connection; private final MQTTConnection connection;
private MQTTLogger log = MQTTLogger.LOGGER; private static final Logger logger = Logger.getLogger(MQTTSessionCallback.class);
public MQTTSessionCallback(MQTTSession session, MQTTConnection connection) throws Exception { public MQTTSessionCallback(MQTTSession session, MQTTConnection connection) throws Exception {
this.session = session; this.session = session;
@ -54,7 +55,7 @@ public class MQTTSessionCallback implements SessionCallback {
try { try {
session.getMqttPublishManager().sendMessage(message.toCore(), consumer, deliveryCount); session.getMqttPublishManager().sendMessage(message.toCore(), consumer, deliveryCount);
} catch (Exception e) { } catch (Exception e) {
log.warn("Unable to send message: " + message.getMessageID() + " Cause: " + e.getMessage(), e); MQTTLogger.LOGGER.unableToSendMessage(reference, e);
} }
return 1; return 1;
} }
@ -86,7 +87,7 @@ public class MQTTSessionCallback implements SessionCallback {
try { try {
consumer.removeItself(); consumer.removeItself();
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage()); MQTTLogger.LOGGER.errorDisconnectingConsumer(e);
} }
} }
@ -102,7 +103,22 @@ public class MQTTSessionCallback implements SessionCallback {
@Override @Override
public boolean hasCredits(ServerConsumer consumerID) { public boolean hasCredits(ServerConsumer consumerID) {
return true; return hasCredits(consumerID, null);
}
@Override
public boolean hasCredits(ServerConsumer consumerID, MessageReference ref) {
/*
* [MQTT-3.3.4-9] The Server MUST NOT send more than Receive Maximum QoS 1 and QoS 2 PUBLISH packets for which it
* has not received PUBACK, PUBCOMP, or PUBREC with a Reason Code of 128 or greater from the Client.
*
* Therefore, enforce flow-control based on the number of pending QoS 1 & 2 messages
*/
if (ref != null && ref.isDurable() == true && connection.getReceiveMaximum() != -1 && session.getState().getOutboundStore().getPendingMessages() >= connection.getReceiveMaximum()) {
return false;
} else {
return true;
}
} }
@Override @Override

View File

@ -17,26 +17,36 @@
package org.apache.activemq.artemis.core.protocol.mqtt; package org.apache.activemq.artemis.core.protocol.mqtt;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.mqtt.MqttTopicSubscription; import io.netty.handler.codec.mqtt.MqttTopicSubscription;
import org.apache.activemq.artemis.api.core.Pair; import org.apache.activemq.artemis.api.core.Pair;
import org.apache.activemq.artemis.core.config.WildcardConfiguration; import org.apache.activemq.artemis.core.config.WildcardConfiguration;
import org.apache.activemq.artemis.core.settings.impl.Match;
import org.jboss.logging.Logger;
public class MQTTSessionState { public class MQTTSessionState {
private static final Logger logger = Logger.getLogger(MQTTSessionState.class);
public static final MQTTSessionState DEFAULT = new MQTTSessionState(null); public static final MQTTSessionState DEFAULT = new MQTTSessionState(null);
private MQTTSession session;
private String clientId; private String clientId;
private final ConcurrentMap<String, MqttTopicSubscription> subscriptions = new ConcurrentHashMap<>(); private final ConcurrentMap<String, Pair<MqttTopicSubscription, Integer>> subscriptions = new ConcurrentHashMap<>();
// Used to store Packet ID of Publish QoS1 and QoS2 message. See spec: 4.3.3 QoS 2: Exactly once delivery. Method B. // Used to store Packet ID of Publish QoS1 and QoS2 message. See spec: 4.3.3 QoS 2: Exactly once delivery. Method B.
private final Map<Integer, MQTTMessageInfo> messageRefStore = new ConcurrentHashMap<>(); private final Map<Integer, MQTTMessageInfo> messageRefStore = new ConcurrentHashMap<>();
@ -51,10 +61,44 @@ public class MQTTSessionState {
private final OutboundStore outboundStore = new OutboundStore(); private final OutboundStore outboundStore = new OutboundStore();
private int clientSessionExpiryInterval;
private boolean isWill = false;
private ByteBuf willMessage;
private String willTopic;
private int willQoSLevel;
private boolean willRetain = false;
private long willDelayInterval = 0;
private boolean willSent = false;
private boolean failed = false;
private int clientMaxPacketSize = 0;
private Map<Integer, String> clientTopicAliases;
private Integer clientTopicAliasMaximum;
private Map<String, Integer> serverTopicAliases;
public MQTTSessionState(String clientId) { public MQTTSessionState(String clientId) {
this.clientId = clientId; this.clientId = clientId;
} }
public MQTTSession getSession() {
return session;
}
public void setSession(MQTTSession session) {
this.session = session;
}
public synchronized void clear() { public synchronized void clear() {
subscriptions.clear(); subscriptions.clear();
messageRefStore.clear(); messageRefStore.clear();
@ -62,48 +106,76 @@ public class MQTTSessionState {
pubRec.clear(); pubRec.clear();
outboundStore.clear(); outboundStore.clear();
disconnectedTime = 0; disconnectedTime = 0;
if (willMessage != null) {
willMessage.clear();
willMessage = null;
}
willSent = false;
failed = false;
willDelayInterval = 0;
willRetain = false;
willTopic = null;
clientMaxPacketSize = 0;
if (clientTopicAliases != null) {
clientTopicAliases.clear();
clientTopicAliases = null;
}
if (serverTopicAliases != null) {
serverTopicAliases.clear();
serverTopicAliases = null;
}
clientTopicAliasMaximum = 0;
} }
OutboundStore getOutboundStore() { public OutboundStore getOutboundStore() {
return outboundStore; return outboundStore;
} }
Set<Integer> getPubRec() { public Set<Integer> getPubRec() {
return pubRec; return pubRec;
} }
boolean getAttached() { public boolean isAttached() {
return attached; return attached;
} }
void setAttached(boolean attached) { public void setAttached(boolean attached) {
this.attached = attached; this.attached = attached;
} }
Collection<MqttTopicSubscription> getSubscriptions() { public Collection<MqttTopicSubscription> getSubscriptions() {
return subscriptions.values(); Collection<MqttTopicSubscription> result = new HashSet<>();
for (Pair<MqttTopicSubscription, Integer> pair : subscriptions.values()) {
result.add(pair.getA());
}
return result;
} }
boolean addSubscription(MqttTopicSubscription subscription, WildcardConfiguration wildcardConfiguration) { public boolean addSubscription(MqttTopicSubscription subscription, WildcardConfiguration wildcardConfiguration, Integer subscriptionIdentifier) {
// synchronized to prevent race with removeSubscription // synchronized to prevent race with removeSubscription
synchronized (subscriptions) { synchronized (subscriptions) {
addressMessageMap.putIfAbsent(MQTTUtil.convertMQTTAddressFilterToCore(subscription.topicName(), wildcardConfiguration), new ConcurrentHashMap<Long, Integer>()); addressMessageMap.putIfAbsent(MQTTUtil.convertMqttTopicFilterToCoreAddress(subscription.topicName(), wildcardConfiguration), new ConcurrentHashMap<>());
MqttTopicSubscription existingSubscription = subscriptions.get(subscription.topicName()); Pair<MqttTopicSubscription, Integer> existingSubscription = subscriptions.get(subscription.topicName());
if (existingSubscription != null) { if (existingSubscription != null) {
if (subscription.qualityOfService().value() > existingSubscription.qualityOfService().value()) { boolean updated = false;
subscriptions.put(subscription.topicName(), subscription); if (subscription.qualityOfService().value() > existingSubscription.getA().qualityOfService().value()) {
return true; existingSubscription.setA(subscription);
updated = true;
} }
if (subscriptionIdentifier != null && !subscriptionIdentifier.equals(existingSubscription.getB())) {
existingSubscription.setB(subscriptionIdentifier);
updated = true;
}
return updated;
} else { } else {
subscriptions.put(subscription.topicName(), subscription); subscriptions.put(subscription.topicName(), new Pair<>(subscription, subscriptionIdentifier));
return true; return true;
} }
} }
return false;
} }
void removeSubscription(String address) { public void removeSubscription(String address) {
// synchronized to prevent race with addSubscription // synchronized to prevent race with addSubscription
synchronized (subscriptions) { synchronized (subscriptions) {
subscriptions.remove(address); subscriptions.remove(address);
@ -111,26 +183,169 @@ public class MQTTSessionState {
} }
} }
MqttTopicSubscription getSubscription(String address) { public MqttTopicSubscription getSubscription(String address) {
return subscriptions.get(address); return subscriptions.get(address) != null ? subscriptions.get(address).getA() : null;
} }
String getClientId() { public List<Integer> getMatchingSubscriptionIdentifiers(String address) {
address = MQTTUtil.convertCoreAddressToMqttTopicFilter(address, session.getServer().getConfiguration().getWildcardConfiguration());
List<Integer> result = null;
for (Pair<MqttTopicSubscription, Integer> pair : subscriptions.values()) {
Pattern pattern = Match.createPattern(pair.getA().topicName(), MQTTUtil.MQTT_WILDCARD, true);
boolean matches = pattern.matcher(address).matches();
logger.debugf("Matching %s with %s: %s", address, pattern, matches);
if (matches) {
if (result == null) {
result = new ArrayList<>();
}
if (pair.getB() != null) {
result.add(pair.getB());
}
}
}
return result;
}
public String getClientId() {
return clientId; return clientId;
} }
void setClientId(String clientId) { public void setClientId(String clientId) {
this.clientId = clientId; this.clientId = clientId;
} }
long getDisconnectedTime() { public long getDisconnectedTime() {
return disconnectedTime; return disconnectedTime;
} }
void setDisconnectedTime(long disconnectedTime) { public void setDisconnectedTime(long disconnectedTime) {
this.disconnectedTime = disconnectedTime; this.disconnectedTime = disconnectedTime;
} }
public int getClientSessionExpiryInterval() {
return clientSessionExpiryInterval;
}
public void setClientSessionExpiryInterval(int sessionExpiryInterval) {
this.clientSessionExpiryInterval = sessionExpiryInterval;
}
public boolean isWill() {
return isWill;
}
public void setWill(boolean will) {
isWill = will;
}
public ByteBuf getWillMessage() {
return willMessage;
}
public void setWillMessage(ByteBuf willMessage) {
this.willMessage = willMessage;
}
public String getWillTopic() {
return willTopic;
}
public void setWillTopic(String willTopic) {
this.willTopic = willTopic;
}
public int getWillQoSLevel() {
return willQoSLevel;
}
public void setWillQoSLevel(int willQoSLevel) {
this.willQoSLevel = willQoSLevel;
}
public boolean isWillRetain() {
return willRetain;
}
public void setWillRetain(boolean willRetain) {
this.willRetain = willRetain;
}
public long getWillDelayInterval() {
return willDelayInterval;
}
public void setWillDelayInterval(long willDelayInterval) {
this.willDelayInterval = willDelayInterval;
}
public boolean isWillSent() {
return willSent;
}
public void setWillSent(boolean willSent) {
this.willSent = willSent;
}
public boolean isFailed() {
return failed;
}
public void setFailed(boolean failed) {
this.failed = failed;
}
public int getClientMaxPacketSize() {
return clientMaxPacketSize;
}
public void setClientMaxPacketSize(int clientMaxPacketSize) {
this.clientMaxPacketSize = clientMaxPacketSize;
}
public void addClientTopicAlias(Integer alias, String topicName) {
if (clientTopicAliases == null) {
clientTopicAliases = new HashMap<>();
}
clientTopicAliases.put(alias, topicName);
}
public String getClientTopicAlias(Integer alias) {
String result;
if (clientTopicAliases == null) {
result = null;
} else {
result = clientTopicAliases.get(alias);
}
return result;
}
public Integer getClientTopicAliasMaximum() {
return clientTopicAliasMaximum;
}
public void setClientTopicAliasMaximum(Integer clientTopicAliasMaximum) {
this.clientTopicAliasMaximum = clientTopicAliasMaximum;
}
public Integer addServerTopicAlias(String topicName) {
if (serverTopicAliases == null) {
serverTopicAliases = new ConcurrentHashMap<>();
}
Integer alias = serverTopicAliases.size() + 1;
if (alias <= clientTopicAliasMaximum) {
serverTopicAliases.put(topicName, alias);
return alias;
} else {
return null;
}
}
public Integer getServerTopicAlias(String topicName) {
return serverTopicAliases == null ? null : serverTopicAliases.get(topicName);
}
void removeMessageRef(Integer mqttId) { void removeMessageRef(Integer mqttId) {
MQTTMessageInfo info = messageRefStore.remove(mqttId); MQTTMessageInfo info = messageRefStore.remove(mqttId);
if (info != null) { if (info != null) {
@ -199,6 +414,12 @@ public class MQTTSessionState {
return publishAckd(mqtt); return publishAckd(mqtt);
} }
public int getPendingMessages() {
synchronized (dataStoreLock) {
return mqttToServerIds.size();
}
}
public void clear() { public void clear() {
synchronized (dataStoreLock) { synchronized (dataStoreLock) {
artemisToMqttMessageMap.clear(); artemisToMqttMessageMap.clear();
@ -207,4 +428,9 @@ public class MQTTSessionState {
} }
} }
} }
@Override
public String toString() {
return "MQTTSessionState[" + "session=" + session + ", clientId='" + clientId + "', subscriptions=" + subscriptions + ", messageRefStore=" + messageRefStore + ", addressMessageMap=" + addressMessageMap + ", pubRec=" + pubRec + ", attached=" + attached + ", outboundStore=" + outboundStore + ", disconnectedTime=" + disconnectedTime + ", sessionExpiryInterval=" + clientSessionExpiryInterval + ", isWill=" + isWill + ", willMessage=" + willMessage + ", willTopic='" + willTopic + "', willQoSLevel=" + willQoSLevel + ", willRetain=" + willRetain + ", willDelayInterval=" + willDelayInterval + ", failed=" + failed + ", maxPacketSize=" + clientMaxPacketSize + ']';
}
} }

View File

@ -17,14 +17,18 @@
package org.apache.activemq.artemis.core.protocol.mqtt; package org.apache.activemq.artemis.core.protocol.mqtt;
import java.util.HashSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import io.netty.handler.codec.mqtt.MqttProperties;
import io.netty.handler.codec.mqtt.MqttSubscriptionOption;
import io.netty.handler.codec.mqtt.MqttTopicSubscription;
import org.apache.activemq.artemis.api.core.ActiveMQQueueExistsException; import org.apache.activemq.artemis.api.core.ActiveMQQueueExistsException;
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException;
import org.apache.activemq.artemis.api.core.FilterConstants; import org.apache.activemq.artemis.api.core.FilterConstants;
import org.apache.activemq.artemis.api.core.QueueConfiguration; import org.apache.activemq.artemis.api.core.QueueConfiguration;
import org.apache.activemq.artemis.api.core.RoutingType; import org.apache.activemq.artemis.api.core.RoutingType;
@ -36,19 +40,27 @@ import org.apache.activemq.artemis.core.server.Queue;
import org.apache.activemq.artemis.core.server.ServerConsumer; import org.apache.activemq.artemis.core.server.ServerConsumer;
import org.apache.activemq.artemis.core.server.impl.AddressInfo; import org.apache.activemq.artemis.core.server.impl.AddressInfo;
import org.apache.activemq.artemis.utils.CompositeAddress; import org.apache.activemq.artemis.utils.CompositeAddress;
import org.jboss.logging.Logger;
import io.netty.handler.codec.mqtt.MqttTopicSubscription; import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.SUBSCRIPTION_IDENTIFIER;
import static org.apache.activemq.artemis.reader.MessageUtil.CONNECTION_ID_PROPERTY_NAME_STRING;
public class MQTTSubscriptionManager { public class MQTTSubscriptionManager {
private static final Logger logger = Logger.getLogger(MQTTSubscriptionManager.class);
private final MQTTSession session; private final MQTTSession session;
private final ConcurrentMap<Long, Integer> consumerQoSLevels; private final ConcurrentMap<Long, Integer> consumerQoSLevels;
private final ConcurrentMap<String, ServerConsumer> consumers; private final ConcurrentMap<String, ServerConsumer> consumers;
// We filter out Artemis management messages and notifications /*
private final SimpleString managementFilter; * We filter out certain messages (e.g. management messages, notifications, and messages from any address starting
* with '$'). This is because MQTT clients can do silly things like subscribe to '#' which matches ever address
* on the broker.
*/
private final SimpleString messageFilter;
public MQTTSubscriptionManager(MQTTSession session) { public MQTTSubscriptionManager(MQTTSession session) {
this.session = session; this.session = session;
@ -56,7 +68,7 @@ public class MQTTSubscriptionManager {
consumers = new ConcurrentHashMap<>(); consumers = new ConcurrentHashMap<>();
consumerQoSLevels = new ConcurrentHashMap<>(); consumerQoSLevels = new ConcurrentHashMap<>();
// Create filter string to ignore management messages // Create filter string to ignore certain messages
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.append("NOT (("); builder.append("NOT ((");
builder.append(FilterConstants.ACTIVEMQ_ADDRESS); builder.append(FilterConstants.ACTIVEMQ_ADDRESS);
@ -66,15 +78,53 @@ public class MQTTSubscriptionManager {
builder.append(FilterConstants.ACTIVEMQ_ADDRESS); builder.append(FilterConstants.ACTIVEMQ_ADDRESS);
builder.append(" = '"); builder.append(" = '");
builder.append(session.getServer().getConfiguration().getManagementNotificationAddress()); builder.append(session.getServer().getConfiguration().getManagementNotificationAddress());
builder.append("'))"); builder.append("') OR (");
managementFilter = new SimpleString(builder.toString()); builder.append(FilterConstants.ACTIVEMQ_ADDRESS);
builder.append(" LIKE '$%'))"); // [MQTT-4.7.2-1]
messageFilter = new SimpleString(builder.toString());
} }
synchronized void start() throws Exception { synchronized void start() throws Exception {
for (MqttTopicSubscription subscription : session.getSessionState().getSubscriptions()) { for (MqttTopicSubscription subscription : session.getState().getSubscriptions()) {
String coreAddress = MQTTUtil.convertMQTTAddressFilterToCore(subscription.topicName(), session.getWildcardConfiguration()); addSubscription(subscription, null, true);
Queue q = createQueueForSubscription(coreAddress, subscription.qualityOfService().value()); }
createConsumerForSubscriptionQueue(q, subscription.topicName(), subscription.qualityOfService().value()); }
private void addSubscription(MqttTopicSubscription subscription, Integer subscriptionIdentifier, boolean initialStart) throws Exception {
String topicName = CompositeAddress.extractAddressName(subscription.topicName());
String sharedSubscriptionName = null;
// if using a shared subscription then parse the subscription name and topic
if (topicName.startsWith(MQTTUtil.SHARED_SUBSCRIPTION_PREFIX)) {
int slashIndex = topicName.indexOf("/") + 1;
sharedSubscriptionName = topicName.substring(slashIndex, topicName.indexOf("/", slashIndex));
topicName = topicName.substring(topicName.indexOf("/", slashIndex) + 1);
}
int qos = subscription.qualityOfService().value();
String coreAddress = MQTTUtil.convertMqttTopicFilterToCoreAddress(topicName, session.getWildcardConfiguration());
Queue q = createQueueForSubscription(coreAddress, qos, sharedSubscriptionName);
if (initialStart) {
createConsumerForSubscriptionQueue(q, topicName, qos, subscription.option().isNoLocal(), null);
} else {
MqttTopicSubscription existingSubscription = session.getState().getSubscription(topicName);
if (existingSubscription == null) {
createConsumerForSubscriptionQueue(q, topicName, qos, subscription.option().isNoLocal(), null);
} else {
Long existingConsumerId = consumers.get(topicName).getID();
consumerQoSLevels.put(existingConsumerId, qos);
if (existingSubscription.option().isNoLocal() != subscription.option().isNoLocal()) {
createConsumerForSubscriptionQueue(q, topicName, qos, subscription.option().isNoLocal(), existingConsumerId);
}
}
if (subscription.option().retainHandling() == MqttSubscriptionOption.RetainedHandlingPolicy.SEND_AT_SUBSCRIBE ||
(subscription.option().retainHandling() == MqttSubscriptionOption.RetainedHandlingPolicy.SEND_AT_SUBSCRIBE_IF_NOT_YET_EXISTS && existingSubscription == null)) {
session.getRetainMessageManager().addRetainedMessagesToQueue(q, topicName);
}
session.getState().addSubscription(subscription, session.getWildcardConfiguration(), subscriptionIdentifier);
} }
} }
@ -87,12 +137,16 @@ public class MQTTSubscriptionManager {
} }
} }
/** private Queue createQueueForSubscription(String address, int qos, String sharedSubscriptionName) throws Exception {
* Creates a Queue if it doesn't already exist, based on a topic and address. Returning the queue name. // determine the proper queue name
*/ SimpleString queue;
private Queue createQueueForSubscription(String address, int qos) throws Exception { if (sharedSubscriptionName != null) {
// Check to see if a subscription queue already exists. queue = SimpleString.toSimpleString(sharedSubscriptionName);
SimpleString queue = getQueueNameForTopic(address); } else {
queue = getQueueNameForTopic(address);
}
// check to see if a subscription queue already exists.
Queue q = session.getServer().locateQueue(queue); Queue q = session.getServer().locateQueue(queue);
// The queue does not exist so we need to create it. // The queue does not exist so we need to create it.
@ -105,7 +159,7 @@ public class MQTTSubscriptionManager {
throw ActiveMQMessageBundle.BUNDLE.noSuchQueue(sAddress); throw ActiveMQMessageBundle.BUNDLE.noSuchQueue(sAddress);
} }
// Check that the address exists, if not we try to auto create it. // check that the address exists, if not we try to auto create it (if allowed).
AddressInfo addressInfo = session.getServerSession().getAddress(sAddress); AddressInfo addressInfo = session.getServerSession().getAddress(sAddress);
if (addressInfo == null) { if (addressInfo == null) {
if (!bindingQueryResult.isAutoCreateAddresses()) { if (!bindingQueryResult.isAutoCreateAddresses()) {
@ -120,9 +174,8 @@ public class MQTTSubscriptionManager {
} }
private Queue findOrCreateQueue(BindingQueryResult bindingQueryResult, AddressInfo addressInfo, SimpleString queue, int qos) throws Exception { private Queue findOrCreateQueue(BindingQueryResult bindingQueryResult, AddressInfo addressInfo, SimpleString queue, int qos) throws Exception {
if (addressInfo.getRoutingTypes().contains(RoutingType.MULTICAST)) { if (addressInfo.getRoutingTypes().contains(RoutingType.MULTICAST)) {
return session.getServerSession().createQueue(new QueueConfiguration(queue).setAddress(addressInfo.getName()).setFilterString(managementFilter).setDurable(MQTTUtil.DURABLE_MESSAGES && qos >= 0)); return session.getServerSession().createQueue(new QueueConfiguration(queue).setAddress(addressInfo.getName()).setFilterString(messageFilter).setDurable(MQTTUtil.DURABLE_MESSAGES && qos >= 0));
} }
if (addressInfo.getRoutingTypes().contains(RoutingType.ANYCAST)) { if (addressInfo.getRoutingTypes().contains(RoutingType.ANYCAST)) {
@ -138,100 +191,100 @@ public class MQTTSubscriptionManager {
return session.getServer().locateQueue(name); return session.getServer().locateQueue(name);
} else { } else {
try { try {
return session.getServerSession().createQueue(new QueueConfiguration(addressInfo.getName()).setRoutingType(RoutingType.ANYCAST).setFilterString(managementFilter).setDurable(MQTTUtil.DURABLE_MESSAGES && qos >= 0)); return session.getServerSession().createQueue(new QueueConfiguration(addressInfo.getName()).setRoutingType(RoutingType.ANYCAST).setFilterString(messageFilter).setDurable(MQTTUtil.DURABLE_MESSAGES && qos >= 0));
} catch (ActiveMQQueueExistsException e) { } catch (ActiveMQQueueExistsException e) {
return session.getServer().locateQueue(addressInfo.getName()); return session.getServer().locateQueue(addressInfo.getName());
} }
} }
} }
Set<RoutingType> routingTypeSet = new HashSet(); throw ActiveMQMessageBundle.BUNDLE.invalidRoutingTypeForAddress(addressInfo.getRoutingType(), addressInfo.getName().toString(), EnumSet.allOf(RoutingType.class));
routingTypeSet.add(RoutingType.MULTICAST);
routingTypeSet.add(RoutingType.ANYCAST);
throw ActiveMQMessageBundle.BUNDLE.invalidRoutingTypeForAddress(addressInfo.getRoutingType(), addressInfo.getName().toString(), routingTypeSet);
} }
/** private void createConsumerForSubscriptionQueue(Queue queue, String topic, int qos, boolean noLocal, Long existingConsumerId) throws Exception {
* Creates a new consumer for the queue associated with a subscription long cid = existingConsumerId != null ? existingConsumerId : session.getServer().getStorageManager().generateID();
*/
private void createConsumerForSubscriptionQueue(Queue queue, String topic, int qos) throws Exception { // for noLocal support we use the MQTT *client id* rather than the connection ID, but we still use the existing property name
long cid = session.getServer().getStorageManager().generateID(); ServerConsumer consumer = session.getServerSession().createConsumer(cid, queue.getName(), noLocal ? SimpleString.toSimpleString(CONNECTION_ID_PROPERTY_NAME_STRING + " <> '" + session.getState().getClientId() + "'") : null, false, false, -1);
ServerConsumer consumer = session.getServerSession().createConsumer(cid, queue.getName(), null, false, false, -1);
ServerConsumer existingConsumer = consumers.put(topic, consumer);
if (existingConsumer != null) {
existingConsumer.setStarted(false);
existingConsumer.close(false);
}
consumer.setStarted(true); consumer.setStarted(true);
consumers.put(topic, consumer);
consumerQoSLevels.put(cid, qos); consumerQoSLevels.put(cid, qos);
} }
private void addSubscription(MqttTopicSubscription subscription) throws Exception { short[] removeSubscriptions(List<String> topics) throws Exception {
String topicName = CompositeAddress.extractAddressName(subscription.topicName()); short[] reasonCodes;
MqttTopicSubscription s = session.getSessionState().getSubscription(topicName);
int qos = subscription.qualityOfService().value(); synchronized (session.getState()) {
reasonCodes = new short[topics.size()];
String coreAddress = MQTTUtil.convertMQTTAddressFilterToCore(topicName, session.getWildcardConfiguration()); for (int i = 0; i < topics.size(); i++) {
reasonCodes[i] = removeSubscription(topics.get(i));
session.getSessionState().addSubscription(subscription, session.getWildcardConfiguration());
Queue q = createQueueForSubscription(coreAddress, qos);
if (s == null) {
createConsumerForSubscriptionQueue(q, topicName, qos);
} else {
consumerQoSLevels.put(consumers.get(topicName).getID(), qos);
}
session.getRetainMessageManager().addRetainedMessagesToQueue(q, topicName);
}
void removeSubscriptions(List<String> topics) throws Exception {
synchronized (session.getSessionState()) {
for (String topic : topics) {
removeSubscription(topic);
} }
} }
return reasonCodes;
} }
private void removeSubscription(String address) throws Exception { private short removeSubscription(String address) {
String internalAddress = MQTTUtil.convertMQTTAddressFilterToCore(address, session.getWildcardConfiguration()); if (session.getState().getSubscription(address) == null) {
SimpleString internalQueueName = getQueueNameForTopic(internalAddress); return MQTTReasonCodes.NO_SUBSCRIPTION_EXISTED;
session.getSessionState().removeSubscription(address); }
Queue queue = session.getServer().locateQueue(internalQueueName); short reasonCode = MQTTReasonCodes.SUCCESS;
SimpleString sAddress = SimpleString.toSimpleString(internalAddress);
AddressInfo addressInfo = session.getServerSession().getAddress(sAddress); try {
if (addressInfo != null && addressInfo.getRoutingTypes().contains(RoutingType.ANYCAST)) { String internalAddress = MQTTUtil.convertMqttTopicFilterToCoreAddress(address, session.getWildcardConfiguration());
ServerConsumer consumer = consumers.get(address); SimpleString internalQueueName = getQueueNameForTopic(internalAddress);
consumers.remove(address); session.getState().removeSubscription(address);
if (consumer != null) {
consumer.close(false); Queue queue = session.getServer().locateQueue(internalQueueName);
consumerQoSLevels.remove(consumer.getID()); SimpleString sAddress = SimpleString.toSimpleString(internalAddress);
} AddressInfo addressInfo = session.getServerSession().getAddress(sAddress);
} else { if (addressInfo != null && addressInfo.getRoutingTypes().contains(RoutingType.ANYCAST)) {
consumers.remove(address); ServerConsumer consumer = consumers.get(address);
Set<Consumer> queueConsumers; consumers.remove(address);
if (queue != null && (queueConsumers = (Set<Consumer>) queue.getConsumers()) != null) { if (consumer != null) {
for (Consumer consumer : queueConsumers) { consumer.close(false);
if (consumer instanceof ServerConsumer) { consumerQoSLevels.remove(consumer.getID());
((ServerConsumer) consumer).close(false); }
consumerQoSLevels.remove(((ServerConsumer) consumer).getID()); } else {
consumers.remove(address);
Set<Consumer> queueConsumers;
if (queue != null && (queueConsumers = (Set<Consumer>) queue.getConsumers()) != null) {
for (Consumer consumer : queueConsumers) {
if (consumer instanceof ServerConsumer) {
((ServerConsumer) consumer).close(false);
consumerQoSLevels.remove(((ServerConsumer) consumer).getID());
}
} }
} }
} }
}
if (queue != null) { if (queue != null) {
assert session.getServerSession().executeQueueQuery(internalQueueName).isExists(); assert session.getServerSession().executeQueueQuery(internalQueueName).isExists();
if (queue.isConfigurationManaged()) { if (queue.isConfigurationManaged()) {
queue.deleteAllReferences(); queue.deleteAllReferences();
} else { } else {
session.getServerSession().deleteQueue(internalQueueName); session.getServerSession().deleteQueue(internalQueueName);
}
} }
} catch (Exception e) {
MQTTLogger.LOGGER.errorRemovingSubscription(e);
reasonCode = MQTTReasonCodes.UNSPECIFIED_ERROR;
} }
return reasonCode;
} }
private SimpleString getQueueNameForTopic(String topic) { private SimpleString getQueueNameForTopic(String topic) {
return new SimpleString(session.getSessionState().getClientId() + "." + topic); return new SimpleString(session.getState().getClientId() + "." + topic);
} }
/** /**
@ -241,13 +294,28 @@ public class MQTTSubscriptionManager {
* @return An array of integers representing the list of accepted QoS for each topic. * @return An array of integers representing the list of accepted QoS for each topic.
* @throws Exception * @throws Exception
*/ */
int[] addSubscriptions(List<MqttTopicSubscription> subscriptions) throws Exception { int[] addSubscriptions(List<MqttTopicSubscription> subscriptions, MqttProperties properties) throws Exception {
synchronized (session.getSessionState()) { synchronized (session.getState()) {
Integer subscriptionIdentifier = null;
if (properties.getProperty(SUBSCRIPTION_IDENTIFIER.value()) != null) {
subscriptionIdentifier = (Integer) properties.getProperty(SUBSCRIPTION_IDENTIFIER.value()).value();
}
int[] qos = new int[subscriptions.size()]; int[] qos = new int[subscriptions.size()];
for (int i = 0; i < subscriptions.size(); i++) { for (int i = 0; i < subscriptions.size(); i++) {
addSubscription(subscriptions.get(i)); try {
qos[i] = subscriptions.get(i).qualityOfService().value(); addSubscription(subscriptions.get(i), subscriptionIdentifier, false);
qos[i] = subscriptions.get(i).qualityOfService().value();
} catch (ActiveMQSecurityException e) {
// user is not authorized to create subsription
if (session.is5()) {
qos[i] = MQTTReasonCodes.NOT_AUTHORIZED;
} else {
qos[i] = MQTTReasonCodes.UNSPECIFIED_ERROR;
}
}
} }
return qos; return qos;
} }
@ -257,8 +325,8 @@ public class MQTTSubscriptionManager {
return consumerQoSLevels; return consumerQoSLevels;
} }
void clean() throws Exception { void clean() {
for (MqttTopicSubscription mqttTopicSubscription : session.getSessionState().getSubscriptions()) { for (MqttTopicSubscription mqttTopicSubscription : session.getState().getSubscriptions()) {
removeSubscription(mqttTopicSubscription.topicName()); removeSubscription(mqttTopicSubscription.topicName());
} }
} }

View File

@ -18,17 +18,27 @@
package org.apache.activemq.artemis.core.protocol.mqtt; package org.apache.activemq.artemis.core.protocol.mqtt;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List;
import com.google.common.base.CaseFormat;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.handler.codec.EncoderException;
import io.netty.handler.codec.mqtt.MqttConnAckVariableHeader; import io.netty.handler.codec.mqtt.MqttConnAckVariableHeader;
import io.netty.handler.codec.mqtt.MqttConnectMessage; import io.netty.handler.codec.mqtt.MqttConnectMessage;
import io.netty.handler.codec.mqtt.MqttConnectPayload; import io.netty.handler.codec.mqtt.MqttConnectPayload;
import io.netty.handler.codec.mqtt.MqttConnectVariableHeader; import io.netty.handler.codec.mqtt.MqttConnectVariableHeader;
import io.netty.handler.codec.mqtt.MqttFixedHeader;
import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttMessage;
import io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader; import io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader;
import io.netty.handler.codec.mqtt.MqttMessageType; import io.netty.handler.codec.mqtt.MqttMessageType;
import io.netty.handler.codec.mqtt.MqttProperties;
import io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType;
import io.netty.handler.codec.mqtt.MqttPubReplyMessageVariableHeader;
import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage;
import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; import io.netty.handler.codec.mqtt.MqttPublishVariableHeader;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.netty.handler.codec.mqtt.MqttReasonCodeAndPropertiesVariableHeader;
import io.netty.handler.codec.mqtt.MqttSubAckMessage; import io.netty.handler.codec.mqtt.MqttSubAckMessage;
import io.netty.handler.codec.mqtt.MqttSubscribeMessage; import io.netty.handler.codec.mqtt.MqttSubscribeMessage;
import io.netty.handler.codec.mqtt.MqttTopicSubscription; import io.netty.handler.codec.mqtt.MqttTopicSubscription;
@ -38,15 +48,21 @@ import org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.core.config.WildcardConfiguration; import org.apache.activemq.artemis.core.config.WildcardConfiguration;
import org.apache.activemq.artemis.core.message.impl.CoreMessage; import org.apache.activemq.artemis.core.message.impl.CoreMessage;
import org.apache.activemq.artemis.reader.MessageUtil;
import org.jboss.logging.Logger;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.CONTENT_TYPE;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.CORRELATION_DATA;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.PAYLOAD_FORMAT_INDICATOR;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.PUBLICATION_EXPIRY_INTERVAL;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.RESPONSE_TOPIC;
import static io.netty.handler.codec.mqtt.MqttProperties.MqttPropertyType.USER_PROPERTY;
/** /**
* A Utility Class for creating Server Side objects and converting MQTT concepts to/from Artemis. * A Utility Class for creating Server Side objects and converting MQTT concepts to/from Artemis.
*/ */
public class MQTTUtil { public class MQTTUtil {
private static final Logger logger = Logger.getLogger(MQTTUtil.class);
// TODO These settings should be configurable.
public static final int DEFAULT_SERVER_MESSAGE_BUFFER_SIZE = 512;
public static final boolean DURABLE_MESSAGES = true; public static final boolean DURABLE_MESSAGES = true;
@ -60,8 +76,6 @@ public class MQTTUtil {
public static final boolean SESSION_AUTO_CREATE_QUEUE = false; public static final boolean SESSION_AUTO_CREATE_QUEUE = false;
public static final int MAX_MESSAGE_SIZE = 268435455;
public static final String MQTT_RETAIN_ADDRESS_PREFIX = "$sys.mqtt.retain."; public static final String MQTT_RETAIN_ADDRESS_PREFIX = "$sys.mqtt.retain.";
public static final SimpleString MQTT_QOS_LEVEL_KEY = SimpleString.toSimpleString("mqtt.qos.level"); public static final SimpleString MQTT_QOS_LEVEL_KEY = SimpleString.toSimpleString("mqtt.qos.level");
@ -72,12 +86,69 @@ public class MQTTUtil {
public static final SimpleString MQTT_MESSAGE_RETAIN_KEY = SimpleString.toSimpleString("mqtt.message.retain"); public static final SimpleString MQTT_MESSAGE_RETAIN_KEY = SimpleString.toSimpleString("mqtt.message.retain");
public static final SimpleString MQTT_PAYLOAD_FORMAT_INDICATOR_KEY = SimpleString.toSimpleString("mqtt.payload.format.indicator");
public static final SimpleString MQTT_RESPONSE_TOPIC_KEY = SimpleString.toSimpleString("mqtt.response.topic");
public static final SimpleString MQTT_CORRELATION_DATA_KEY = SimpleString.toSimpleString("mqtt.correlation.data");
public static final String MQTT_USER_PROPERTY_EXISTS_KEY = "mqtt.user.property.exists";
public static final String MQTT_USER_PROPERTY_KEY_PREFIX = "mqtt.ordered.user.property.";
public static final SimpleString MQTT_USER_PROPERTY_KEY_PREFIX_SIMPLE = SimpleString.toSimpleString(MQTT_USER_PROPERTY_KEY_PREFIX);
public static final SimpleString MQTT_CONTENT_TYPE_KEY = SimpleString.toSimpleString("mqtt.content.type");
public static final String MANAGEMENT_QUEUE_PREFIX = "$sys.mqtt.queue.qos2."; public static final String MANAGEMENT_QUEUE_PREFIX = "$sys.mqtt.queue.qos2.";
public static final int DEFAULT_KEEP_ALIVE_FREQUENCY = 5000; public static final String SHARED_SUBSCRIPTION_PREFIX = "$share/";
public static String convertMQTTAddressFilterToCore(String filter, WildcardConfiguration wildcardConfiguration) { public static final long FOUR_BYTE_INT_MAX = Long.decode("0xFFFFFFFF"); // 4_294_967_295
return MQTT_WILDCARD.convert(filter, wildcardConfiguration);
public static final int TWO_BYTE_INT_MAX = Integer.decode("0xFFFF"); // 65_535
// https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901011
public static final int VARIABLE_BYTE_INT_MAX = 268_435_455;
public static final int MAX_PACKET_SIZE = VARIABLE_BYTE_INT_MAX;
public static final long KEEP_ALIVE_ADJUSTMENT = 1500L;
public static final int DEFAULT_SERVER_KEEP_ALIVE = 60;
public static final int DEFAULT_TOPIC_ALIAS_MAX = TWO_BYTE_INT_MAX;
public static final int DEFAULT_RECEIVE_MAXIMUM = TWO_BYTE_INT_MAX;
public static final int DEFAULT_MAXIMUM_PACKET_SIZE = MAX_PACKET_SIZE;
public static String convertMqttTopicFilterToCoreAddress(String filter, WildcardConfiguration wildcardConfiguration) {
return convertMqttTopicFilterToCoreAddress(null, filter, wildcardConfiguration);
}
public static String convertMqttTopicFilterToCoreAddress(String prefixToAdd, String filter, WildcardConfiguration wildcardConfiguration) {
if (filter == null) {
return "";
}
String converted = MQTT_WILDCARD.convert(filter, wildcardConfiguration);
if (prefixToAdd != null) {
converted = prefixToAdd + converted;
}
return converted;
}
public static String convertCoreAddressToMqttTopicFilter(String address, WildcardConfiguration wildcardConfiguration) {
if (address == null) {
return "";
}
if (address.startsWith(MQTT_RETAIN_ADDRESS_PREFIX)) {
address = address.substring(MQTT_RETAIN_ADDRESS_PREFIX.length());
}
return wildcardConfiguration.convert(address, MQTT_WILDCARD);
} }
public static class MQTTWildcardConfiguration extends WildcardConfiguration { public static class MQTTWildcardConfiguration extends WildcardConfiguration {
@ -90,137 +161,340 @@ public class MQTTUtil {
public static final WildcardConfiguration MQTT_WILDCARD = new MQTTWildcardConfiguration(); public static final WildcardConfiguration MQTT_WILDCARD = new MQTTWildcardConfiguration();
private static final MQTTLogger logger = MQTTLogger.LOGGER; private static ICoreMessage createServerMessage(MQTTSession session, SimpleString address, MqttPublishMessage mqttPublishMessage) {
public static String convertCoreAddressFilterToMQTT(String filter, WildcardConfiguration wildcardConfiguration) {
if (filter == null) {
return "";
}
if (filter.startsWith(MQTT_RETAIN_ADDRESS_PREFIX)) {
filter = filter.substring(MQTT_RETAIN_ADDRESS_PREFIX.length(), filter.length());
}
return wildcardConfiguration.convert(filter, MQTT_WILDCARD);
}
public static String convertMQTTAddressFilterToCoreRetain(String filter, WildcardConfiguration wildcardConfiguration) {
return MQTT_RETAIN_ADDRESS_PREFIX + MQTT_WILDCARD.convert(filter, wildcardConfiguration);
}
private static ICoreMessage createServerMessage(MQTTSession session,
SimpleString address,
boolean retain,
int qos) {
long id = session.getServer().getStorageManager().generateID(); long id = session.getServer().getStorageManager().generateID();
CoreMessage message = new CoreMessage(id, DEFAULT_SERVER_MESSAGE_BUFFER_SIZE, session.getCoreMessageObjectPools()); CoreMessage message = new CoreMessage(id, mqttPublishMessage.fixedHeader().remainingLength(), session.getCoreMessageObjectPools());
message.setAddress(address); message.setAddress(address);
message.putBooleanProperty(MQTT_MESSAGE_RETAIN_KEY, retain); message.putBooleanProperty(MQTT_MESSAGE_RETAIN_KEY, mqttPublishMessage.fixedHeader().isRetain());
message.putIntProperty(MQTT_QOS_LEVEL_KEY, qos); message.putIntProperty(MQTT_QOS_LEVEL_KEY, mqttPublishMessage.fixedHeader().qosLevel().value());
message.setType(Message.BYTES_TYPE); message.setType(Message.BYTES_TYPE);
message.putStringProperty(MessageUtil.CONNECTION_ID_PROPERTY_NAME, session.getState().getClientId());
MqttProperties properties = mqttPublishMessage.variableHeader() == null ? null : mqttPublishMessage.variableHeader().properties();
Integer payloadIndicatorFormat = getProperty(Integer.class, properties, PAYLOAD_FORMAT_INDICATOR);
if (payloadIndicatorFormat != null) {
message.putIntProperty(MQTT_PAYLOAD_FORMAT_INDICATOR_KEY, payloadIndicatorFormat);
}
String responseTopic = getProperty(String.class, properties, RESPONSE_TOPIC);
if (responseTopic != null) {
message.putStringProperty(MQTT_RESPONSE_TOPIC_KEY, responseTopic);
}
byte[] correlationData = getProperty(byte[].class, properties, CORRELATION_DATA);
if (correlationData != null) {
message.putBytesProperty(MQTT_CORRELATION_DATA_KEY, correlationData);
}
/*
* [MQTT-3.3.2-18] The Server MUST maintain the order of User Properties when forwarding the Application Message
*
* Maintain the original order of the properties by using a decomposable name that indicates the original order.
*/
List<MqttProperties.StringPair> userProperties = getProperty(List.class, properties, USER_PROPERTY);
if (userProperties != null && userProperties.size() != 0) {
message.putIntProperty(MQTT_USER_PROPERTY_EXISTS_KEY, userProperties.size());
for (int i = 0; i < userProperties.size(); i++) {
String key = new StringBuilder()
.append(MQTT_USER_PROPERTY_KEY_PREFIX)
.append(i)
.append(".")
.append(userProperties.get(i).key)
.toString();
message.putStringProperty(key, userProperties.get(i).value);
}
}
String contentType = getProperty(String.class, properties, CONTENT_TYPE);
if (contentType != null) {
message.putStringProperty(MQTT_CONTENT_TYPE_KEY, contentType);
}
long time = System.currentTimeMillis();
message.setTimestamp(time);
Integer messageExpiryInterval = getProperty(Integer.class, properties, PUBLICATION_EXPIRY_INTERVAL);
if (messageExpiryInterval != null) {
message.setExpiration(time + (messageExpiryInterval * 1000));
}
return message; return message;
} }
public static Message createServerMessageFromByteBuf(MQTTSession session, public static Message createServerMessageFromByteBuf(MQTTSession session,
String topic, String topic,
boolean retain, MqttPublishMessage mqttPublishMessage) {
int qos, String coreAddress = convertMqttTopicFilterToCoreAddress(topic, session.getWildcardConfiguration());
ByteBuf payload) {
String coreAddress = convertMQTTAddressFilterToCore(topic, session.getWildcardConfiguration());
SimpleString address = SimpleString.toSimpleString(coreAddress, session.getCoreMessageObjectPools().getAddressStringSimpleStringPool()); SimpleString address = SimpleString.toSimpleString(coreAddress, session.getCoreMessageObjectPools().getAddressStringSimpleStringPool());
ICoreMessage message = createServerMessage(session, address, retain, qos); ICoreMessage message = createServerMessage(session, address, mqttPublishMessage);
ByteBuf payload = mqttPublishMessage.payload();
message.getBodyBuffer().writeBytes(payload, 0, payload.readableBytes()); message.getBodyBuffer().writeBytes(payload, 0, payload.readableBytes());
return message; return message;
} }
public static Message createPubRelMessage(MQTTSession session, SimpleString address, int messageId) { public static Message createPubRelMessage(MQTTSession session, SimpleString address, int messageId) {
Message message = createServerMessage(session, address, false, 1); MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBREL, false, MqttQoS.AT_LEAST_ONCE, false, 0);
message.putIntProperty(MQTTUtil.MQTT_MESSAGE_ID_KEY, messageId); MqttPublishMessage publishMessage = new MqttPublishMessage(fixedHeader, null, null);
message.putIntProperty(MQTTUtil.MQTT_MESSAGE_TYPE_KEY, MqttMessageType.PUBREL.value()); Message message = createServerMessage(session, address, publishMessage)
.putIntProperty(MQTTUtil.MQTT_MESSAGE_ID_KEY, messageId)
.putIntProperty(MQTTUtil.MQTT_MESSAGE_TYPE_KEY, MqttMessageType.PUBREL.value());
return message; return message;
} }
public static void logMessage(MQTTSessionState state, MqttMessage message, boolean inbound) { public static void logMessage(MQTTSessionState state, MqttMessage message, boolean inbound) {
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
traceMessage(state, message, inbound); StringBuilder log = new StringBuilder("MQTT(");
if (state != null) {
log.append(state.getClientId());
}
if (inbound) {
log.append("): IN << ");
} else {
log.append("): OUT >> ");
}
if (message.fixedHeader() != null) {
log.append(message.fixedHeader().messageType().toString());
if (message.variableHeader() instanceof MqttMessageIdVariableHeader) {
log.append("(" + ((MqttMessageIdVariableHeader) message.variableHeader()).messageId() + ")");
}
switch (message.fixedHeader().messageType()) {
case PUBLISH:
MqttPublishVariableHeader publishHeader = (MqttPublishVariableHeader) message.variableHeader();
String topicName = publishHeader.topicName();
if (topicName == null || topicName.length() == 0) {
topicName = "<empty>";
}
log.append("(" + publishHeader.packetId() + ")")
.append(" topic=" + topicName)
.append(", qos=" + message.fixedHeader().qosLevel().value())
.append(", retain=" + message.fixedHeader().isRetain())
.append(", dup=" + message.fixedHeader().isDup())
.append(", remainingLength=" + message.fixedHeader().remainingLength());
for (MqttProperties.MqttProperty property : ((MqttPublishMessage)message).variableHeader().properties().listAll()) {
Object value = property.value();
if (value != null && value instanceof byte[]) {
value = new String((byte[]) value, StandardCharsets.UTF_8);
}
log.append(", " + formatCase(MqttPropertyType.valueOf(property.propertyId()).name()) + "=" + value);
}
log.append(", payload=" + getPayloadForLogging((MqttPublishMessage) message, 256));
break;
case CONNECT:
// intentionally omit the username & password from the log
MqttConnectVariableHeader connectHeader = (MqttConnectVariableHeader) message.variableHeader();
MqttConnectPayload payload = ((MqttConnectMessage)message).payload();
log.append(" protocol=(").append(connectHeader.name()).append(", ").append(connectHeader.version()).append(")")
.append(", hasPassword=").append(connectHeader.hasPassword())
.append(", isCleanStart=").append(connectHeader.isCleanSession())
.append(", keepAliveTimeSeconds=").append(connectHeader.keepAliveTimeSeconds())
.append(", clientIdentifier=").append(payload.clientIdentifier())
.append(", hasUserName=").append(connectHeader.hasUserName())
.append(", isWillFlag=").append(connectHeader.isWillFlag());
if (connectHeader.isWillFlag()) {
log.append(", willQos=").append(connectHeader.willQos())
.append(", isWillRetain=").append(connectHeader.isWillRetain())
.append(", willTopic=").append(payload.willTopic());
}
for (MqttProperties.MqttProperty property : connectHeader.properties().listAll()) {
log.append(", " + formatCase(MqttPropertyType.valueOf(property.propertyId()).name()) + "=" + property.value());
}
break;
case CONNACK:
MqttConnAckVariableHeader connackHeader = (MqttConnAckVariableHeader) message.variableHeader();
log.append(" connectReasonCode=").append(formatByte(connackHeader.connectReturnCode().byteValue()))
.append(", sessionPresent=").append(connackHeader.isSessionPresent());
for (MqttProperties.MqttProperty property : connackHeader.properties().listAll()) {
log.append(", " + formatCase(MqttPropertyType.valueOf(property.propertyId()).name()) + "=" + property.value());
}
break;
case SUBSCRIBE:
for (MqttTopicSubscription sub : ((MqttSubscribeMessage) message).payload().topicSubscriptions()) {
log.append("\n\t" + sub.topicName() + " : " + sub.qualityOfService());
}
break;
case SUBACK:
for (Integer qos : ((MqttSubAckMessage) message).payload().grantedQoSLevels()) {
log.append("\n\t" + qos);
}
break;
case UNSUBSCRIBE:
for (String topic : ((MqttUnsubscribeMessage) message).payload().topics()) {
log.append("\n\t" + topic);
}
break;
case PUBACK:
break;
case PUBREC:
case PUBREL:
case PUBCOMP:
MqttPubReplyMessageVariableHeader pubReplyVariableHeader = (MqttPubReplyMessageVariableHeader) message.variableHeader();
log.append(" reasonCode=").append(formatByte(pubReplyVariableHeader.reasonCode()));
break;
case DISCONNECT:
MqttReasonCodeAndPropertiesVariableHeader disconnectVariableHeader = (MqttReasonCodeAndPropertiesVariableHeader) message.variableHeader();
log.append(" reasonCode=").append(formatByte(disconnectVariableHeader.reasonCode()));
break;
}
logger.trace(log.toString());
}
} }
} }
public static void traceMessage(MQTTSessionState state, MqttMessage message, boolean inbound) { private static String formatByte(byte bite) {
StringBuilder log = new StringBuilder("MQTT("); return String.format("0x%02X ", bite);
}
if (state != null) { private static String formatCase(String string) {
log.append(state.getClientId()); return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, string);
}
private static String getPayloadForLogging(MqttPublishMessage message, int maxPayloadLogSize) {
if (message.payload() == null) {
return "<empty>";
} }
String publishPayload = message.payload().toString(StandardCharsets.UTF_8);
if (inbound) { if (publishPayload.length() == 0) {
log.append("): IN << "); return "<empty>";
} else {
log.append("): OUT >> ");
} }
return publishPayload.length() > maxPayloadLogSize ? publishPayload.substring(0, maxPayloadLogSize) : publishPayload;
}
if (message.fixedHeader() != null) { /*
log.append(message.fixedHeader().messageType().toString()); * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Remaining_Length:
* "The Remaining Length is a Variable Byte Integer that represents the number of bytes remaining within the current
* Control Packet, including data in the Variable Header and the Payload. The Remaining Length does not include the
* bytes used to encode the Remaining Length."
*
* https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901106
* "The Variable Header of the PUBLISH Packet contains the following fields in the order: Topic Name, Packet
* Identifier, and Properties."
*/
public static int calculateRemainingLength(String topicName, MqttProperties properties, ByteBuf payload) {
int size = 0;
if (message.variableHeader() instanceof MqttMessageIdVariableHeader) { /*
log.append("(" + ((MqttMessageIdVariableHeader) message.variableHeader()).messageId() + ")"); * https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc358219870
* "The Variable Header component of many of the MQTT Control Packet types includes a Two Byte Integer Packet
* Identifier field."
*/
final int PACKET_ID_SIZE = 2;
size += PACKET_ID_SIZE;
size += ByteBufUtil.utf8Bytes(topicName);
size += calculatePublishPropertiesSize(properties);
size += payload.resetReaderIndex().readableBytes();
return size;
}
/*
* https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901027
*/
private static int calculatePublishPropertiesSize(MqttProperties properties) {
int size = 0;
try {
try {
for (MqttProperties.MqttProperty property : properties.listAll()) {
MqttPropertyType propertyType = MqttPropertyType.valueOf(property.propertyId());
switch (propertyType) {
case PAYLOAD_FORMAT_INDICATOR:
size += calculateVariableByteIntegerSize(property.propertyId());
size += 1;
break;
case TOPIC_ALIAS:
size += calculateVariableByteIntegerSize(property.propertyId());
size += 2;
break;
case PUBLICATION_EXPIRY_INTERVAL: // AKA "Message Expiry Interval"
size += calculateVariableByteIntegerSize(property.propertyId());
size += 4;
break;
case SUBSCRIPTION_IDENTIFIER:
size += calculateVariableByteIntegerSize(property.propertyId());
size += calculateVariableByteIntegerSize(((MqttProperties.IntegerProperty) property).value());
break;
case CONTENT_TYPE:
case RESPONSE_TOPIC:
size += calculateVariableByteIntegerSize(property.propertyId());
size += ByteBufUtil.utf8Bytes(((MqttProperties.StringProperty) property).value());
break;
case USER_PROPERTY:
for (MqttProperties.StringPair pair : ((MqttProperties.UserProperties) property).value()) {
size += calculateVariableByteIntegerSize(property.propertyId());
size += ByteBufUtil.utf8Bytes(pair.key);
size += ByteBufUtil.utf8Bytes(pair.value);
}
break;
case CORRELATION_DATA:
size += calculateVariableByteIntegerSize(property.propertyId());
size += 2;
size += ((MqttProperties.BinaryProperty) property).value().length;
break;
default:
//shouldn't reach here
throw new EncoderException("Unknown property type: " + propertyType);
}
}
size += calculateVariableByteIntegerSize(size);
return size;
} finally {
} }
} catch (RuntimeException e) {
switch (message.fixedHeader().messageType()) { throw e;
case PUBLISH:
MqttPublishVariableHeader publishHeader = (MqttPublishVariableHeader) message.variableHeader();
String publishPayload = ((MqttPublishMessage)message).payload().toString(StandardCharsets.UTF_8);
final int maxPayloadLogSize = 256;
log.append("(" + publishHeader.packetId() + ")")
.append(" topic=" + publishHeader.topicName())
.append(", qos=" + message.fixedHeader().qosLevel())
.append(", retain=" + message.fixedHeader().isRetain())
.append(", dup=" + message.fixedHeader().isDup())
.append(", payload=" + (publishPayload.length() > maxPayloadLogSize ? publishPayload.substring(0, maxPayloadLogSize) : publishPayload));
break;
case CONNECT:
MqttConnectVariableHeader connectHeader = (MqttConnectVariableHeader) message.variableHeader();
MqttConnectPayload payload = ((MqttConnectMessage)message).payload();
log.append(" protocol=(").append(connectHeader.name()).append(", ").append(connectHeader.version()).append(")")
.append(", hasPassword=").append(connectHeader.hasPassword())
.append(", isCleanSession=").append(connectHeader.isCleanSession())
.append(", keepAliveTimeSeconds=").append(connectHeader.keepAliveTimeSeconds())
.append(", clientIdentifier=").append(payload.clientIdentifier())
.append(", hasUserName=").append(connectHeader.hasUserName());
if (connectHeader.hasUserName()) {
log.append(", userName=").append(payload.userName());
}
log.append(", isWillFlag=").append(connectHeader.isWillFlag());
if (connectHeader.isWillFlag()) {
log.append(", willQos=").append(connectHeader.willQos())
.append(", isWillRetain=").append(connectHeader.isWillRetain())
.append(", willTopic=").append(payload.willTopic());
}
break;
case CONNACK:
MqttConnAckVariableHeader connackHeader = (MqttConnAckVariableHeader) message.variableHeader();
log.append(" connectReturnCode=").append(connackHeader.connectReturnCode().byteValue())
.append(", sessionPresent=").append(connackHeader.isSessionPresent());
break;
case SUBSCRIBE:
for (MqttTopicSubscription sub : ((MqttSubscribeMessage) message).payload().topicSubscriptions()) {
log.append("\n\t" + sub.topicName() + " : " + sub.qualityOfService());
}
break;
case SUBACK:
for (Integer qos : ((MqttSubAckMessage) message).payload().grantedQoSLevels()) {
log.append("\n\t" + qos);
}
break;
case UNSUBSCRIBE:
for (String topic : ((MqttUnsubscribeMessage) message).payload().topics()) {
log.append("\n\t" + topic);
}
break;
}
logger.trace(log.toString());
} }
} }
/*
* https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Remaining_Length:
* "The packet size is the total number of bytes in an MQTT Control Packet, this is equal to the length of the Fixed
* Header plus the Remaining Length."
*
* The length of the Fixed Header for a PUBLISH packet is 1 byte + the size of the "Remaining Length" Variable Byte
* Integer.
*/
public static int calculateMessageSize(MqttPublishMessage message) {
return 1 + calculateVariableByteIntegerSize(message.fixedHeader().remainingLength()) + message.fixedHeader().remainingLength();
}
/*
* https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901011
*/
private static int calculateVariableByteIntegerSize(int vbi) {
int count = 0;
do {
vbi /= 128;
count++;
}
while (vbi > 0);
return count;
}
public static <T> T getProperty(Class<T> type, MqttProperties properties, MqttPropertyType propertyName) {
return getProperty(type, properties, propertyName, null);
}
public static <T> T getProperty(Class<T> type, MqttProperties properties, MqttPropertyType propertyName, T defaultReturnValue) {
if (properties != null) {
MqttProperties.MqttProperty o = properties.getProperty(propertyName.value());
if (o != null) {
try {
return type.cast(o.value());
} catch (ClassCastException e) {
MQTTLogger.LOGGER.failedToCastProperty(propertyName.toString());
throw e;
}
}
}
return defaultReturnValue == null ? null : defaultReturnValue;
}
} }

View File

@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.core.protocol.mqtt.exceptions;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTReasonCodes;
public class DisconnectException extends Exception {
private final byte code;
public DisconnectException() {
code = MQTTReasonCodes.UNSPECIFIED_ERROR;
}
public DisconnectException(final byte code) {
this.code = code;
}
public byte getCode() {
return code;
}
@Override
public String toString() {
return this.getClass().getSimpleName() + "[code=" + code + "]";
}
}

View File

@ -253,7 +253,7 @@ public class MQTTRetainMessageManagerTest {
private void logRetainedMessagesQueue() { private void logRetainedMessagesQueue() {
final WildcardConfiguration wildcardConfiguration = new WildcardConfiguration(); final WildcardConfiguration wildcardConfiguration = new WildcardConfiguration();
final String retainAddress = MQTTUtil.convertMQTTAddressFilterToCoreRetain(topic, wildcardConfiguration); final String retainAddress = MQTTUtil.convertMqttTopicFilterToCoreAddress(MQTTUtil.MQTT_RETAIN_ADDRESS_PREFIX, topic, wildcardConfiguration);
final Queue queue = jmsServer.getDestinationQueue(retainAddress); final Queue queue = jmsServer.getDestinationQueue(retainAddress);
final LinkedListIterator<MessageReference> browserIterator = queue.browserIterator(); final LinkedListIterator<MessageReference> browserIterator = queue.browserIterator();
browserIterator.forEachRemaining(messageReference -> { browserIterator.forEachRemaining(messageReference -> {

View File

@ -336,10 +336,6 @@ public class OpenWireProtocolManager extends AbstractProtocolManager<Command, O
return entry; return entry;
} }
@Override
public void removeHandler(String name) {
}
@Override @Override
public void handleBuffer(RemotingConnection connection, ActiveMQBuffer buffer) { public void handleBuffer(RemotingConnection connection, ActiveMQBuffer buffer) {
} }

View File

@ -61,12 +61,12 @@ import org.apache.activemq.artemis.utils.VersionLoader;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import static org.apache.activemq.artemis.core.protocol.stomp.ActiveMQStompProtocolMessageBundle.BUNDLE; import static org.apache.activemq.artemis.core.protocol.stomp.ActiveMQStompProtocolMessageBundle.BUNDLE;
import static org.apache.activemq.artemis.reader.MessageUtil.CONNECTION_ID_PROPERTY_NAME_STRING;
public final class StompConnection implements RemotingConnection { public final class StompConnection implements RemotingConnection {
private static final Logger logger = Logger.getLogger(StompConnection.class); private static final Logger logger = Logger.getLogger(StompConnection.class);
protected static final String CONNECTION_ID_PROP = "__AMQ_CID";
private static final String SERVER_NAME = "ActiveMQ-Artemis/" + VersionLoader.getVersion().getFullVersion() + private static final String SERVER_NAME = "ActiveMQ-Artemis/" + VersionLoader.getVersion().getFullVersion() +
" ActiveMQ Artemis Messaging Engine"; " ActiveMQ Artemis Messaging Engine";
@ -669,7 +669,7 @@ public final class StompConnection implements RemotingConnection {
StompSession stompSession = getSession(txID); StompSession stompSession = getSession(txID);
if (stompSession.isNoLocal()) { if (stompSession.isNoLocal()) {
message.putStringProperty(CONNECTION_ID_PROP, getID().toString()); message.putStringProperty(CONNECTION_ID_PROPERTY_NAME_STRING, getID().toString());
} }
if (isEnableMessageID()) { if (isEnableMessageID()) {
message.putStringProperty("amqMessageId", "STOMP" + message.getMessageID()); message.putStringProperty("amqMessageId", "STOMP" + message.getMessageID());
@ -734,7 +734,7 @@ public final class StompConnection implements RemotingConnection {
checkDestination(destination); checkDestination(destination);
checkRoutingSemantics(destination, subscriptionType); checkRoutingSemantics(destination, subscriptionType);
if (noLocal) { if (noLocal) {
String noLocalFilter = CONNECTION_ID_PROP + " <> '" + getID().toString() + "'"; String noLocalFilter = CONNECTION_ID_PROPERTY_NAME_STRING + " <> '" + getID().toString() + "'";
if (selector == null) { if (selector == null) {
selector = noLocalFilter; selector = noLocalFilter;
} else { } else {

View File

@ -130,10 +130,6 @@ public class StompProtocolManager extends AbstractProtocolManager<StompFrame, St
} }
} }
@Override
public void removeHandler(String name) {
}
@Override @Override
public void handleBuffer(final RemotingConnection connection, final ActiveMQBuffer buffer) { public void handleBuffer(final RemotingConnection connection, final ActiveMQBuffer buffer) {
StompConnection conn = (StompConnection) connection; StompConnection conn = (StompConnection) connection;

View File

@ -1380,4 +1380,17 @@ public interface Configuration {
Configuration setTemporaryQueueNamespace(String temporaryQueueNamespace); Configuration setTemporaryQueueNamespace(String temporaryQueueNamespace);
/**
* This is specific to MQTT, and it's necessary because the session scan interval is a broker-wide setting and can't
* be set on a per-connector basis like the rest of the MQTT-specific settings.
*/
Configuration setMqttSessionScanInterval(long mqttSessionScanInterval);
/**
* @see Configuration#setMqttSessionScanInterval(long)
*
* @return
*/
long getMqttSessionScanInterval();
} }

View File

@ -125,9 +125,13 @@ public class WildcardConfiguration implements Serializable {
} }
public String convert(String filter, WildcardConfiguration to) { public String convert(String filter, WildcardConfiguration to) {
return filter.replace(getDelimiter(), to.getDelimiter()) if (this.equals(to)) {
.replace(getSingleWord(), to.getSingleWord()) return filter;
.replace(getAnyWords(), to.getAnyWords()); } else {
return filter
.replace(getDelimiter(), to.getDelimiter())
.replace(getSingleWord(), to.getSingleWord())
.replace(getAnyWords(), to.getAnyWords());
}
} }
} }

View File

@ -385,6 +385,8 @@ public class ConfigurationImpl implements Configuration, Serializable {
private String temporaryQueueNamespace = ActiveMQDefaultConfiguration.getDefaultTemporaryQueueNamespace(); private String temporaryQueueNamespace = ActiveMQDefaultConfiguration.getDefaultTemporaryQueueNamespace();
private long mqttSessionScanInterval = ActiveMQDefaultConfiguration.getMqttSessionScanInterval();
/** /**
* Parent folder for all data folders. * Parent folder for all data folders.
@ -2677,6 +2679,17 @@ public class ConfigurationImpl implements Configuration, Serializable {
return this; return this;
} }
@Override
public long getMqttSessionScanInterval() {
return mqttSessionScanInterval;
}
@Override
public Configuration setMqttSessionScanInterval(long mqttSessionScanInterval) {
this.mqttSessionScanInterval = mqttSessionScanInterval;
return this;
}
// extend property utils with ability to auto-fill and locate from collections // extend property utils with ability to auto-fill and locate from collections
// collection entries are identified by the name() property // collection entries are identified by the name() property
private static class CollectionAutoFillPropertiesUtil extends PropertyUtilsBean { private static class CollectionAutoFillPropertiesUtil extends PropertyUtilsBean {

View File

@ -434,6 +434,8 @@ public final class FileConfigurationParser extends XMLConfigurationUtil {
config.setTemporaryQueueNamespace(getString(e, "temporary-queue-namespace", config.getTemporaryQueueNamespace(), Validators.NOT_NULL_OR_EMPTY)); config.setTemporaryQueueNamespace(getString(e, "temporary-queue-namespace", config.getTemporaryQueueNamespace(), Validators.NOT_NULL_OR_EMPTY));
config.setMqttSessionScanInterval(getLong(e, "mqtt-session-scan-interval", config.getMqttSessionScanInterval(), Validators.GT_ZERO));
long globalMaxSize = getTextBytesAsLongBytes(e, GLOBAL_MAX_SIZE, -1, Validators.MINUS_ONE_OR_GT_ZERO); long globalMaxSize = getTextBytesAsLongBytes(e, GLOBAL_MAX_SIZE, -1, Validators.MINUS_ONE_OR_GT_ZERO);
if (globalMaxSize > 0) { if (globalMaxSize > 0) {

View File

@ -512,6 +512,17 @@ public class RemotingServiceImpl implements RemotingService, ServerConnectionLif
return conns; return conns;
} }
// for testing purposes to verify TTL has been set properly
public synchronized Set<ConnectionEntry> getConnectionEntries() {
Set<ConnectionEntry> conns = new HashSet<>(connections.size());
for (ConnectionEntry entry : connections.values()) {
conns.add(entry);
}
return conns;
}
@Override @Override
public int getConnectionCount() { public int getConnectionCount() {
return connections.size(); return connections.size();

View File

@ -21,6 +21,7 @@ import java.util.List;
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration; import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration;
import org.apache.activemq.artemis.core.PriorityAware; import org.apache.activemq.artemis.core.PriorityAware;
import org.apache.activemq.artemis.core.filter.Filter; import org.apache.activemq.artemis.core.filter.Filter;
import org.apache.activemq.artemis.core.postoffice.Binding;
import org.apache.activemq.artemis.spi.core.protocol.SessionCallback; import org.apache.activemq.artemis.spi.core.protocol.SessionCallback;
public interface Consumer extends PriorityAware { public interface Consumer extends PriorityAware {
@ -62,6 +63,10 @@ public interface Consumer extends PriorityAware {
*/ */
void proceedDeliver(MessageReference reference) throws Exception; void proceedDeliver(MessageReference reference) throws Exception;
default Binding getBinding() {
return null;
}
Filter getFilter(); Filter getFilter();
/** /**

View File

@ -390,7 +390,7 @@ public class ServerConsumerImpl implements ServerConsumer, ReadyListener {
public HandleStatus handle(final MessageReference ref) throws Exception { public HandleStatus handle(final MessageReference ref) throws Exception {
// available credits can be set back to null with a flow control option. // available credits can be set back to null with a flow control option.
AtomicInteger checkInteger = availableCredits; AtomicInteger checkInteger = availableCredits;
if (callback != null && !callback.hasCredits(this) || checkInteger != null && checkInteger.get() <= 0) { if (callback != null && !callback.hasCredits(this, ref) || checkInteger != null && checkInteger.get() <= 0) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug(this + " is busy for the lack of credits. Current credits = " + logger.debug(this + " is busy for the lack of credits. Current credits = " +
availableCredits + availableCredits +
@ -519,6 +519,11 @@ public class ServerConsumerImpl implements ServerConsumer, ReadyListener {
} }
@Override
public Binding getBinding() {
return binding;
}
@Override @Override
public Filter getFilter() { public Filter getFilter() {
return filter; return filter;
@ -1535,4 +1540,8 @@ public class ServerConsumerImpl implements ServerConsumer, ReadyListener {
public String getConnectionRemoteAddress() { public String getConnectionRemoteAddress() {
return this.session.getRemotingConnection().getTransportConnection().getRemoteAddress(); return this.session.getRemotingConnection().getTransportConnection().getRemoteAddress();
} }
public SessionCallback getCallback() {
return callback;
}
} }

View File

@ -2017,6 +2017,13 @@ public class ServerSessionImpl implements ServerSession, FailureListener {
@Override @Override
public void connectionFailed(final ActiveMQException me, boolean failedOver) { public void connectionFailed(final ActiveMQException me, boolean failedOver) {
/*
* This can be invoked from Netty (via channelInactive) when the connection has already been closed causing
* spurious logging about clearing up resources for failed client connections.
*/
if (closed)
return;
try { try {
ActiveMQServerLogger.LOGGER.clientConnectionFailed(name); ActiveMQServerLogger.LOGGER.clientConnectionFailed(name);

View File

@ -45,21 +45,39 @@ public class Match<T> {
public Match(final String match, final T value, final WildcardConfiguration wildcardConfiguration) { public Match(final String match, final T value, final WildcardConfiguration wildcardConfiguration) {
this.match = match; this.match = match;
this.value = value; this.value = value;
pattern = createPattern(match, wildcardConfiguration, false);
}
/**
*
* @param match
* @param wildcardConfiguration
* @param direct setting true is useful for use-cases where you just want to know whether or not a message sent to
* a particular address would match the pattern
* @return
*/
public static Pattern createPattern(final String match, final WildcardConfiguration wildcardConfiguration, boolean direct) {
String actMatch = match; String actMatch = match;
if (wildcardConfiguration.getAnyWordsString().equals(match)) { if (wildcardConfiguration.getAnyWordsString().equals(match)) {
// replace any regex characters // replace any regex characters
actMatch = Match.WILDCARD_REPLACEMENT; actMatch = Match.WILDCARD_REPLACEMENT;
} else { } else {
// this is to match with what's documented if (!direct) {
actMatch = actMatch.replace(wildcardConfiguration.getDelimiterString() + wildcardConfiguration.getAnyWordsString(), wildcardConfiguration.getAnyWordsString()); // this is to match with what's documented
actMatch = actMatch.replace(wildcardConfiguration.getDelimiterString() + wildcardConfiguration.getAnyWordsString(), wildcardConfiguration.getAnyWordsString());
}
actMatch = actMatch.replace(Match.DOT, Match.DOT_REPLACEMENT); actMatch = actMatch.replace(Match.DOT, Match.DOT_REPLACEMENT);
actMatch = actMatch.replace(wildcardConfiguration.getSingleWordString(), String.format(WORD_WILDCARD_REPLACEMENT_FORMAT, Pattern.quote(wildcardConfiguration.getDelimiterString()))); actMatch = actMatch.replace(wildcardConfiguration.getSingleWordString(), String.format(WORD_WILDCARD_REPLACEMENT_FORMAT, Pattern.quote(wildcardConfiguration.getDelimiterString())));
// this one has to be done by last as we are using .* and it could be replaced wrongly if delimiter is '.' if (direct) {
actMatch = actMatch.replace(wildcardConfiguration.getAnyWordsString(), String.format(WILDCARD_CHILD_REPLACEMENT_FORMAT, Pattern.quote(wildcardConfiguration.getDelimiterString()))); actMatch = actMatch.replace(wildcardConfiguration.getAnyWordsString(), WILDCARD_REPLACEMENT);
} else {
// this one has to be done by last as we are using .* and it could be replaced wrongly if delimiter is '.'
actMatch = actMatch.replace(wildcardConfiguration.getAnyWordsString(), String.format(WILDCARD_CHILD_REPLACEMENT_FORMAT, Pattern.quote(wildcardConfiguration.getDelimiterString())));
}
} }
pattern = Pattern.compile(actMatch); return Pattern.compile(actMatch);
} }
public final String getMatch() { public final String getMatch() {
@ -115,5 +133,4 @@ public class Match<T> {
public String toString() { public String toString() {
return value.toString(); return value.toString();
} }
} }

View File

@ -30,7 +30,8 @@ import org.apache.activemq.artemis.spi.core.remoting.Acceptor;
import org.apache.activemq.artemis.spi.core.remoting.Connection; import org.apache.activemq.artemis.spi.core.remoting.Connection;
/** /**
* Info: ProtocolManager is loaded by {@link org.apache.activemq.artemis.core.remoting.server.impl.RemotingServiceImpl#loadProtocolManagerFactories(Iterable)} */ * Info: ProtocolManager is loaded by {@link org.apache.activemq.artemis.core.remoting.server.impl.RemotingServiceImpl#loadProtocolManagerFactories(Iterable)}
*/
public interface ProtocolManager<P extends BaseInterceptor, R extends RedirectHandler> { public interface ProtocolManager<P extends BaseInterceptor, R extends RedirectHandler> {
ProtocolManagerFactory<P> getFactory(); ProtocolManagerFactory<P> getFactory();
@ -45,7 +46,8 @@ public interface ProtocolManager<P extends BaseInterceptor, R extends RedirectHa
ConnectionEntry createConnectionEntry(Acceptor acceptorUsed, Connection connection); ConnectionEntry createConnectionEntry(Acceptor acceptorUsed, Connection connection);
void removeHandler(String name); default void removeHandler(String name) {
}
void handleBuffer(RemotingConnection connection, ActiveMQBuffer buffer); void handleBuffer(RemotingConnection connection, ActiveMQBuffer buffer);

View File

@ -41,6 +41,14 @@ public interface SessionCallback {
*/ */
boolean hasCredits(ServerConsumer consumerID); boolean hasCredits(ServerConsumer consumerID);
/**
* This one includes the MessageReference for protocols like MQTT 5 (which only enforces flow control on durable
* messages (i.e. QoS 1 &amp; 2))
*/
default boolean hasCredits(ServerConsumer consumerID, MessageReference ref) {
return hasCredits(consumerID);
}
/** /**
* This can be used to complete certain operations outside of the lock, * This can be used to complete certain operations outside of the lock,
* like acks or other operations. * like acks or other operations.

View File

@ -428,6 +428,14 @@
</xsd:annotation> </xsd:annotation>
</xsd:element> </xsd:element>
<xsd:element name="mqtt-session-scan-interval" type="xsd:long" default="5000" maxOccurs="1" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
how often (in ms) to scan for expired MQTT sessions
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="connectors" maxOccurs="1" minOccurs="0"> <xsd:element name="connectors" maxOccurs="1" minOccurs="0">
<xsd:annotation> <xsd:annotation>
<xsd:documentation> <xsd:documentation>

View File

@ -160,6 +160,7 @@ public class FileConfigurationTest extends ConfigurationImplTest {
Assert.assertEquals(12345, conf.getGracefulShutdownTimeout()); Assert.assertEquals(12345, conf.getGracefulShutdownTimeout());
Assert.assertEquals(true, conf.isPopulateValidatedUser()); Assert.assertEquals(true, conf.isPopulateValidatedUser());
Assert.assertEquals(false, conf.isRejectEmptyValidatedUser()); Assert.assertEquals(false, conf.isRejectEmptyValidatedUser());
Assert.assertEquals(123456, conf.getMqttSessionScanInterval());
Assert.assertEquals(98765, conf.getConnectionTtlCheckInterval()); Assert.assertEquals(98765, conf.getConnectionTtlCheckInterval());
Assert.assertEquals(1234567, conf.getConfigurationFileRefreshPeriod()); Assert.assertEquals(1234567, conf.getConfigurationFileRefreshPeriod());
Assert.assertEquals("TEMP", conf.getTemporaryQueueNamespace()); Assert.assertEquals("TEMP", conf.getTemporaryQueueNamespace());

View File

@ -56,6 +56,7 @@
<persist-id-cache>true</persist-id-cache> <persist-id-cache>true</persist-id-cache>
<populate-validated-user>true</populate-validated-user> <populate-validated-user>true</populate-validated-user>
<reject-empty-validated-user>false</reject-empty-validated-user> <reject-empty-validated-user>false</reject-empty-validated-user>
<mqtt-session-scan-interval>123456</mqtt-session-scan-interval>
<connection-ttl-check-interval>98765</connection-ttl-check-interval> <connection-ttl-check-interval>98765</connection-ttl-check-interval>
<configuration-file-refresh-period>1234567</configuration-file-refresh-period> <configuration-file-refresh-period>1234567</configuration-file-refresh-period>
<temporary-queue-namespace>TEMP</temporary-queue-namespace> <temporary-queue-namespace>TEMP</temporary-queue-namespace>

View File

@ -57,6 +57,7 @@
<persist-id-cache>true</persist-id-cache> <persist-id-cache>true</persist-id-cache>
<populate-validated-user>true</populate-validated-user> <populate-validated-user>true</populate-validated-user>
<reject-empty-validated-user>false</reject-empty-validated-user> <reject-empty-validated-user>false</reject-empty-validated-user>
<mqtt-session-scan-interval>123456</mqtt-session-scan-interval>
<connection-ttl-check-interval>98765</connection-ttl-check-interval> <connection-ttl-check-interval>98765</connection-ttl-check-interval>
<configuration-file-refresh-period>1234567</configuration-file-refresh-period> <configuration-file-refresh-period>1234567</configuration-file-refresh-period>
<temporary-queue-namespace>TEMP</temporary-queue-namespace> <temporary-queue-namespace>TEMP</temporary-queue-namespace>

View File

@ -7,16 +7,19 @@ reason MQTT is ideally suited to constrained devices such as sensors and
actuators and is quickly becoming the defacto standard communication protocol actuators and is quickly becoming the defacto standard communication protocol
for IoT. for IoT.
Apache ActiveMQ Artemis supports MQTT v3.1.1 (and also the older v3.1 code Apache ActiveMQ Artemis supports the following MQTT versions (with links to
message format). By default there are `acceptor` elements configured to accept their respective specifications):
MQTT connections on ports `61616` and `1883`.
- [3.1](https://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html)
- [3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html)
- [5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html)
By default there are `acceptor` elements configured to accept MQTT connections
on ports `61616` and `1883`.
See the general [Protocols and Interoperability](protocols-interoperability.md) See the general [Protocols and Interoperability](protocols-interoperability.md)
chapter for details on configuring an `acceptor` for MQTT. chapter for details on configuring an `acceptor` for MQTT.
The best source of information on the MQTT protocol is in the [3.1.1
specification](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html).
Refer to the MQTT examples for a look at some of this functionality in action. Refer to the MQTT examples for a look at some of this functionality in action.
## MQTT Quality of Service ## MQTT Quality of Service
@ -85,19 +88,10 @@ deployment of devices.
## Debug Logging ## Debug Logging
Detailed protocol logging (e.g. packets in/out) can be activated via the Detailed protocol logging (e.g. packets in/out) can be activated by turning
following steps: on `TRACE` logging for `org.apache.activemq.artemis.core.protocol.mqtt`. Follow
[these steps](logging.md#activating-trace-for-a-specific-logger) to configure
1. Open `<ARTEMIS_INSTANCE>/etc/logging.properties` logging appropriately.
2. Add `org.apache.activemq.artemis.core.protocol.mqtt` to the `loggers` list.
3. Add this line to enable `TRACE` logging for this new logger:
`logger.org.apache.activemq.artemis.core.protocol.mqtt.level=TRACE`
4. Ensure the `level` for the `handler` you want to log the message doesn't
block the `TRACE` logging. For example, modify the `level` of the `CONSOLE`
`handler` like so: `handler.CONSOLE.level=TRACE`.
The MQTT specification doesn't dictate the format of the payloads which clients The MQTT specification doesn't dictate the format of the payloads which clients
publish. As far as the broker is concerned a payload is just an array of publish. As far as the broker is concerned a payload is just an array of
@ -106,35 +100,34 @@ UTF-8 strings and print them up to 256 characters. Payload logging is limited
to avoid filling the logs with potentially hundreds of megabytes of unhelpful to avoid filling the logs with potentially hundreds of megabytes of unhelpful
information. information.
## Wildcard subscriptions
## Wild card subscriptions
MQTT addresses are hierarchical much like a file system, and they use a special MQTT addresses are hierarchical much like a file system, and they use a special
character (i.e. `/` by default) to separate hierarchical levels. Subscribers character (i.e. `/` by default) to separate hierarchical levels. Subscribers
are able to subscribe to specific topics or to whole branches of a hierarchy. are able to subscribe to specific topics or to whole branches of a hierarchy.
To subscribe to branches of an address hierarchy a subscriber can use wild To subscribe to branches of an address hierarchy a subscriber can use wild
cards. These wild cards (including the aforementioned separator) are cards. There are 2 types of wildcards in MQTT:
configurable. See the [Wildcard
Syntax](wildcard-syntax.md#customizing-the-syntax) chapter for details about
how to configure custom wild cards.
There are 2 types of wild cards in MQTT: - **Multi level** (`#`)
- **Multi level** (`#` by default) Adding this wildcard to an address would match all branches of the address
Adding this wild card to an address would match all branches of the address
hierarchy under a specified node. For example: `/uk/#` Would match hierarchy under a specified node. For example: `/uk/#` Would match
`/uk/cities`, `/uk/cities/newcastle` and also `/uk/rivers/tyne`. Subscribing to `/uk/cities`, `/uk/cities/newcastle` and also `/uk/rivers/tyne`. Subscribing to
an address `#` would result in subscribing to all topics in the broker. This an address `#` would result in subscribing to all topics in the broker. This
can be useful, but should be done so with care since it has significant can be useful, but should be done so with care since it has significant
performance implications. performance implications.
- **Single level** (`+` by default) - **Single level** (`+`)
Matches a single level in the address hierarchy. For example `/uk/+/stores` Matches a single level in the address hierarchy. For example `/uk/+/stores`
would match `/uk/newcastle/stores` but not `/uk/cities/newcastle/stores`. would match `/uk/newcastle/stores` but not `/uk/cities/newcastle/stores`.
These MQTT-specific wildcards are automatically *translated* into the wildcard
syntax used by ActiveMQ Artemis. These wildcards are configurable. See the
[Wildcard Syntax](wildcard-syntax.md#customizing-the-syntax) chapter for details about
how to configure custom wildcards.
## Web Sockets ## Web Sockets
Apache ActiveMQ Artemis also supports MQTT over [Web Apache ActiveMQ Artemis also supports MQTT over [Web
@ -160,9 +153,113 @@ However, the MQTT session meta-data is still present in memory and needs to be
cleaned up as well. The URL parameter `defaultMqttSessionExpiryInterval` can be cleaned up as well. The URL parameter `defaultMqttSessionExpiryInterval` can be
configured on the MQTT `acceptor` to deal with this situation. configured on the MQTT `acceptor` to deal with this situation.
The default `defaultMqttSessionExpiryInterval` is `-1` which means no session MQTT 5 added a new [session expiry interval](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901048)
state will be expired. Otherwise it represents the number of _milliseconds_ property with the same basic semantics. The broker will use the client's value
which must elapse after the client has disconnected before the broker will for this property if it is set. If it is not set then it will apply the
remove the session state. `defaultMqttSessionExpiryInterval`.
MQTT session state is scanned every 5 seconds. The default `defaultMqttSessionExpiryInterval` is `-1` which means no MQTT 3.x
session states will be expired and no MQTT 5 session states which do not pass
their own session expiry interval will be expired. Otherwise it represents the
number of **seconds** which must elapse after the client has disconnected
before the broker will remove the session state.
MQTT session state is scanned every 5,000 milliseconds by default. This can be
changed using the `mqtt-session-scan-interval` element set in the `core` section
of `broker.xml`.
## Flow Control
MQTT 5 introduced a simple form of [flow control](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Flow_Control).
In short, a broker can tell a client how many QoS 1 & 2 messages it can receive
before being acknowledged and vice versa.
This is controlled on the broker by setting the `receiveMaximum` URL parameter on
the MQTT `acceptor` in `broker.xml`.
The default value is `65535` (the maximum value of the 2-byte integer used by
MQTT).
A value of `0` is prohibited by the MQTT 5 specification.
A value of `-1` will prevent the broker from informing the client of any receive
maximum which means flow-control will be disabled from clients to the broker.
This is effectively the same as setting the value to `65535`, but reduces the size
of the `CONNACK` packet by a few bytes.
## Topic Alias Maximum
MQTT 5 introduced [topic aliasing](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Topic_Alias).
This is an optimization for the size of `PUBLISH` control packets as a 2-byte
integer value can now be substituted for the _name_ of the topic which can
potentially be quite long.
Both the client and the broker can inform each other about the _maximum_ alias
value they support (i.e. how many different aliases they support). This is
controlled on the broker using the `topicAliasMaximum` URL parameter on the
`acceptor` used by the MQTT client.
The default value is `65535` (the maximum value of the 2-byte integer used by
MQTT).
A value of `0` will disable topic aliasing from clients to the broker.
A value of `-1` will prevent the broker from informing the client of any topic
alias maximum which means aliasing will be disabled from clients to the broker.
This is effectively the same as setting the value to `0`, but reduces the size
of the `CONNACK` packet by a few bytes.
## Maximum Packet Size
MQTT 5 introduced the [maximum packet size](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901086).
This is the maximum packet size the server or client is willing to accept.
This is controlled on the broker by setting the `maximumPacketSize` URL parameter
on the MQTT `acceptor` in `broker.xml`.
The default value is `268435455` (i.e. 256MB - the maximum value of the variable
byte integer used by MQTT).
A value of `0` is prohibited by the MQTT 5 specification.
A value of `-1` will prevent the broker from informing the client of any maximum
packet size which means no limit will be enforced on the size of incoming packets.
This also reduces the size of the `CONNACK` packet by a few bytes.
## Server Keep Alive
All MQTT versions support a connection keep alive value defined by the *client*.
MQTT 5 introduced a [server keep alive](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901094)
value so that a broker can define the value that the client should use. The
primary use of the server keep alive is for the server to inform the client that
it will disconnect the client for inactivity sooner than the keep alive specified
by the client.
This is controlled on the broker by setting the `serverKeepAlive` URL parameter
on the MQTT `acceptor` in `broker.xml`.
The default value is `60` and is measured in **seconds**.
A value of `0` completely disables keep alives no matter the client's keep alive
value. This is **not recommended** because disabling keep alives is generally
considered dangerous since it could lead to resource exhaustion.
A value of `-1` means the broker will *always* accept the client's keep alive
value (even if that value is `0`).
Any other value means the `serverKeepAlive` will be applied if it is *less than*
the client's keep alive value **unless** the client's keep alive value is `0` in
which case the `serverKeepAlive` is applied. This is because a value of `0` would
disable keep alives and disabling keep alives is generally considered dangerous
since it could lead to resource exhaustion.
## Enhanced Authentication
MQTT 5 introduced [enhanced authentication](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901256)
which extends the existing name & password authentication to include challenge /
response style authentication.
However, there are currently no challenge / response mechanisms implemented so if
a client passes the "Authentication Method" property in its `CONNECT` packet it will
receive a `CONNACK` with a reason code of `0x8C` (i.e. bad authentication method)
and the network connection will be closed.

13
pom.xml
View File

@ -153,7 +153,7 @@
<junit.version>4.13.2</junit.version> <junit.version>4.13.2</junit.version>
<surefire.version>2.22.2</surefire.version> <surefire.version>2.22.2</surefire.version>
<version.jaxb.runtime>2.3.3</version.jaxb.runtime> <version.jaxb.runtime>2.3.3</version.jaxb.runtime>
<paho.client.mqttv3.version>1.2.5</paho.client.mqttv3.version> <paho.client.mqtt.version>1.2.5</paho.client.mqtt.version>
<!-- for JakrtaEE --> <!-- for JakrtaEE -->
<version.batavia>1.0.10.Final</version.batavia> <version.batavia>1.0.10.Final</version.batavia>
@ -293,9 +293,14 @@
<!-- ### For MQTT Tests && Examples --> <!-- ### For MQTT Tests && Examples -->
<dependency> <dependency>
<groupId>org.eclipse.paho</groupId> <groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId> <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>${paho.client.mqttv3.version}</version> <version>${paho.client.mqtt.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.mqttv5.client</artifactId>
<version>${paho.client.mqtt.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.fusesource.mqtt-client</groupId> <groupId>org.fusesource.mqtt-client</groupId>

View File

@ -209,6 +209,15 @@
<groupId>org.eclipse.paho</groupId> <groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId> <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.mqttv5.client</artifactId>
</dependency>
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>jakarta.resource</groupId> <groupId>jakarta.resource</groupId>
<artifactId>jakarta.resource-api</artifactId> <artifactId>jakarta.resource-api</artifactId>

View File

@ -17,12 +17,21 @@
package org.apache.activemq.artemis.tests.integration.balancing; package org.apache.activemq.artemis.tests.integration.balancing;
import io.netty.handler.codec.mqtt.MqttConnectReturnCode; import javax.management.openmbean.CompositeData;
import javax.management.openmbean.TabularData;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.activemq.artemis.api.core.QueueConfiguration; import org.apache.activemq.artemis.api.core.QueueConfiguration;
import org.apache.activemq.artemis.api.core.RoutingType; import org.apache.activemq.artemis.api.core.RoutingType;
import org.apache.activemq.artemis.api.core.management.BrokerBalancerControl; import org.apache.activemq.artemis.api.core.management.BrokerBalancerControl;
import org.apache.activemq.artemis.api.core.management.QueueControl; import org.apache.activemq.artemis.api.core.management.QueueControl;
import org.apache.activemq.artemis.api.core.management.ResourceNames; import org.apache.activemq.artemis.api.core.management.ResourceNames;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTReasonCodes;
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants;
import org.apache.activemq.artemis.core.server.ActiveMQServers; import org.apache.activemq.artemis.core.server.ActiveMQServers;
import org.apache.activemq.artemis.core.server.balancing.policies.FirstElementPolicy; import org.apache.activemq.artemis.core.server.balancing.policies.FirstElementPolicy;
@ -38,15 +47,6 @@ import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.TabularData;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class MQTTRedirectTest extends BalancingTestBase { public class MQTTRedirectTest extends BalancingTestBase {
static { static {
@ -91,7 +91,7 @@ public class MQTTRedirectTest extends BalancingTestBase {
client0.connect(connOpts); client0.connect(connOpts);
Assert.fail(); Assert.fail();
} catch (MqttException e) { } catch (MqttException e) {
Assert.assertEquals(MqttConnectReturnCode.CONNECTION_REFUSED_USE_ANOTHER_SERVER, MqttConnectReturnCode.valueOf((byte) e.getReasonCode())); Assert.assertEquals(MQTTReasonCodes.USE_ANOTHER_SERVER, (byte) e.getReasonCode());
} }
client0.close(); client0.close();
@ -151,7 +151,7 @@ public class MQTTRedirectTest extends BalancingTestBase {
client0.connect(connOpts); client0.connect(connOpts);
fail("Expect to be rejected as not in role b"); fail("Expect to be rejected as not in role b");
} catch (MqttException e) { } catch (MqttException e) {
Assert.assertEquals(MqttConnectReturnCode.CONNECTION_REFUSED_USE_ANOTHER_SERVER, MqttConnectReturnCode.valueOf((byte) e.getReasonCode())); Assert.assertEquals(MQTTReasonCodes.USE_ANOTHER_SERVER, (byte) e.getReasonCode());
} }
client0.close(); client0.close();

View File

@ -23,7 +23,6 @@ import javax.jms.MessageConsumer;
import javax.jms.MessageProducer; import javax.jms.MessageProducer;
import javax.jms.Session; import javax.jms.Session;
import javax.jms.TextMessage; import javax.jms.TextMessage;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported; package org.apache.activemq.artemis.tests.integration.mqtt;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported; package org.apache.activemq.artemis.tests.integration.mqtt;
public interface MQTTClientProvider { public interface MQTTClientProvider {

View File

@ -25,7 +25,6 @@ import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptor;
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants; import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants;
import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerBindingPlugin; import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerBindingPlugin;
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
import org.apache.activemq.artemis.tests.integration.mqtt.imported.MQTTTestSupport;
import org.apache.activemq.artemis.utils.Wait; import org.apache.activemq.artemis.utils.Wait;
import org.fusesource.mqtt.client.BlockingConnection; import org.fusesource.mqtt.client.BlockingConnection;
import org.fusesource.mqtt.client.MQTT; import org.fusesource.mqtt.client.MQTT;

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported; package org.apache.activemq.artemis.tests.integration.mqtt;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported; package org.apache.activemq.artemis.tests.integration.mqtt;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@ -22,10 +22,8 @@ import java.util.concurrent.TimeUnit;
import io.netty.handler.codec.mqtt.MqttFixedHeader; import io.netty.handler.codec.mqtt.MqttFixedHeader;
import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttMessage;
import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttMessageType;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTInterceptor; import org.apache.activemq.artemis.core.protocol.mqtt.MQTTInterceptor;
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
import org.apache.felix.resolver.util.ArrayMap; import org.apache.felix.resolver.util.ArrayMap;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@ -44,8 +42,8 @@ public class MQTTInterceptorPropertiesTest extends MQTTTestSupport {
assertNotNull(server.getNodeID()); assertNotNull(server.getNodeID());
MqttFixedHeader header = message.fixedHeader(); MqttFixedHeader header = message.fixedHeader();
assertNotNull(header.messageType()); assertNotNull(header.messageType());
assertEquals(header.qosLevel().value(), AT_MOST_ONCE); assertEquals(AT_MOST_ONCE, header.qosLevel().value());
assertEquals(header.isRetain(), expectedProperties.get(RETAINED)); assertEquals(expectedProperties.get(RETAINED), header.isRetain());
} catch (Throwable t) { } catch (Throwable t) {
collector.addError(t); collector.addError(t);
} }
@ -73,26 +71,19 @@ public class MQTTInterceptorPropertiesTest extends MQTTTestSupport {
subscribeProvider.subscribe(addressQueue, AT_MOST_ONCE); subscribeProvider.subscribe(addressQueue, AT_MOST_ONCE);
final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch latch = new CountDownLatch(1);
MQTTInterceptor incomingInterceptor = new MQTTInterceptor() { MQTTInterceptor incomingInterceptor = (packet, connection) -> {
@Override if (packet.fixedHeader().messageType() == MqttMessageType.PUBLISH) {
public boolean intercept(MqttMessage packet, RemotingConnection connection) throws ActiveMQException { return checkMessageProperties(packet, expectedProperties);
if (packet.getClass() == MqttPublishMessage.class) { } else {
return checkMessageProperties(packet, expectedProperties); return true;
} else {
return true;
}
} }
}; };
MQTTInterceptor outgoingInterceptor = new MQTTInterceptor() { MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
@Override if (packet.fixedHeader().messageType() == MqttMessageType.PUBLISH) {
public boolean intercept(MqttMessage packet, RemotingConnection connection) throws ActiveMQException { return checkMessageProperties(packet, expectedProperties);
if (packet.getClass() == MqttPublishMessage.class) { } else {
return checkMessageProperties(packet, expectedProperties); return true;
} else {
return true;
}
} }
}; };
server.getRemotingService().addIncomingInterceptor(incomingInterceptor); server.getRemotingService().addIncomingInterceptor(incomingInterceptor);

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported; package org.apache.activemq.artemis.tests.integration.mqtt;
import org.apache.activemq.ActiveMQConnection; import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.ActiveMQConnectionFactory;

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported; package org.apache.activemq.artemis.tests.integration.mqtt;
import org.apache.activemq.artemis.api.core.QueueConfiguration; import org.apache.activemq.artemis.api.core.QueueConfiguration;
import org.apache.activemq.artemis.api.core.RoutingType; import org.apache.activemq.artemis.api.core.RoutingType;

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported; package org.apache.activemq.artemis.tests.integration.mqtt;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported; package org.apache.activemq.artemis.tests.integration.mqtt;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported; package org.apache.activemq.artemis.tests.integration.mqtt;
import java.net.URL; import java.net.URL;
import java.util.Arrays; import java.util.Arrays;

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported; package org.apache.activemq.artemis.tests.integration.mqtt;
import java.io.EOFException; import java.io.EOFException;
import java.util.Arrays; import java.util.Arrays;

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported; package org.apache.activemq.artemis.tests.integration.mqtt;
import org.apache.activemq.artemis.tests.util.Wait; import org.apache.activemq.artemis.tests.util.Wait;
import org.fusesource.mqtt.client.BlockingConnection; import org.fusesource.mqtt.client.BlockingConnection;

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported; package org.apache.activemq.artemis.tests.integration.mqtt;
import javax.jms.BytesMessage; import javax.jms.BytesMessage;
import javax.jms.Connection; import javax.jms.Connection;
@ -1128,6 +1128,7 @@ public class MQTTTest extends MQTTTestSupport {
connection.kill(); connection.kill();
Message m = connection2.receive(1000, TimeUnit.MILLISECONDS); Message m = connection2.receive(1000, TimeUnit.MILLISECONDS);
assertNotNull(m);
assertEquals("test message", new String(m.getPayload())); assertEquals("test message", new String(m.getPayload()));
} }

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported; package org.apache.activemq.artemis.tests.integration.mqtt;
import javax.jms.ConnectionFactory; import javax.jms.ConnectionFactory;
import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManager;

View File

@ -18,7 +18,6 @@ package org.apache.activemq.artemis.tests.integration.mqtt;
import java.util.LinkedList; import java.util.LinkedList;
import org.apache.activemq.artemis.tests.integration.mqtt.imported.MQTTTestSupport;
import org.apache.activemq.artemis.tests.util.Wait; import org.apache.activemq.artemis.tests.util.Wait;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback; import org.eclipse.paho.client.mqttv3.MqttCallback;

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported; package org.apache.activemq.artemis.tests.integration.mqtt;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -577,10 +577,13 @@ public class MqttClusterRemoteSubscribeTest extends ClusterTestBase {
Message message1 = connection1.receive(5, TimeUnit.SECONDS); Message message1 = connection1.receive(5, TimeUnit.SECONDS);
assertNotNull(message1);
message1.ack(); message1.ack();
Message message2 = connection2.receive(5, TimeUnit.SECONDS); Message message2 = connection2.receive(5, TimeUnit.SECONDS);
assertNotNull(message2);
message2.ack(); message2.ack();
Message message3 = connection1.receive(5, TimeUnit.SECONDS); Message message3 = connection1.receive(5, TimeUnit.SECONDS);
assertNotNull(message3);
message3.ack(); message3.ack();
assertEquals(payload1, new String(message1.getPayload())); assertEquals(payload1, new String(message1.getPayload()));
@ -601,10 +604,13 @@ public class MqttClusterRemoteSubscribeTest extends ClusterTestBase {
connection1.publish("anycast/test/1/some/la", payload3.getBytes(), QoS.AT_MOST_ONCE, false); connection1.publish("anycast/test/1/some/la", payload3.getBytes(), QoS.AT_MOST_ONCE, false);
Message message11 = connection1.receive(5, TimeUnit.SECONDS); Message message11 = connection1.receive(5, TimeUnit.SECONDS);
assertNotNull(message11);
message11.ack(); message11.ack();
Message message21 = connection1.receive(5, TimeUnit.SECONDS); Message message21 = connection1.receive(5, TimeUnit.SECONDS);
assertNotNull(message21);
message21.ack(); message21.ack();
Message message31 = connection1.receive(5, TimeUnit.SECONDS); Message message31 = connection1.receive(5, TimeUnit.SECONDS);
assertNotNull(message31);
message31.ack(); message31.ack();

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported; package org.apache.activemq.artemis.tests.integration.mqtt;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;

View File

@ -33,7 +33,6 @@ import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.core.settings.impl.AddressSettings; import org.apache.activemq.artemis.core.settings.impl.AddressSettings;
import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory;
import org.apache.activemq.artemis.logs.AssertionLoggerHandler; import org.apache.activemq.artemis.logs.AssertionLoggerHandler;
import org.apache.activemq.artemis.tests.integration.mqtt.imported.MQTTTestSupport;
import org.apache.activemq.artemis.tests.util.Wait; import org.apache.activemq.artemis.tests.util.Wait;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback; import org.eclipse.paho.client.mqttv3.MqttCallback;

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported; package org.apache.activemq.artemis.tests.integration.mqtt;
import org.apache.activemq.artemis.core.security.Role; import org.apache.activemq.artemis.core.security.Role;
import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.core.server.ActiveMQServer;

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported; package org.apache.activemq.artemis.tests.integration.mqtt;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.apache.activemq.artemis.tests.integration.mqtt.imported.util; package org.apache.activemq.artemis.tests.integration.mqtt.util;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManager;

View File

@ -0,0 +1,57 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5;
import java.util.Arrays;
import java.util.Collection;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.jboss.logging.Logger;
import org.junit.Test;
import org.junit.runners.Parameterized;
public class BasicSslTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(BasicSslTests.class);
public BasicSslTests(String protocol) {
super(protocol);
}
@Parameterized.Parameters(name = "protocol={0}")
public static Collection<Object[]> getParams() {
return Arrays.asList(new Object[][] {
{SSL},
{WSS}
});
}
@Override
public boolean isUseSsl() {
return true;
}
/*
* Basic SSL test. Just connect.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testSsl() throws Exception {
MqttClient client = createPahoClient("client");
client.connect(getSslMqttConnectOptions());
}
}

View File

@ -0,0 +1,87 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.jboss.logging.Logger;
import org.junit.Test;
import org.junit.runners.Parameterized;
public class CertificateAuthenticationSslTests extends MQTT5TestSupport {
static {
String path = System.getProperty("java.security.auth.login.config");
if (path == null) {
URL resource = CertificateAuthenticationSslTests.class.getClassLoader().getResource("login.config");
if (resource != null) {
path = resource.getFile();
System.setProperty("java.security.auth.login.config", path);
}
}
}
private static final Logger log = Logger.getLogger(CertificateAuthenticationSslTests.class);
public CertificateAuthenticationSslTests(String protocol) {
super(protocol);
}
@Parameterized.Parameters(name = "protocol={0}")
public static Collection<Object[]> getParams() {
return Arrays.asList(new Object[][] {
{SSL},
{WSS}
});
}
@Override
public boolean isUseSsl() {
return true;
}
@Override
public boolean isMutualSsl() {
return true;
}
@Override
public boolean isSecurityEnabled() {
return true;
}
@Override
protected void configureBrokerSecurity(ActiveMQServer server) {
server.setSecurityManager(new ActiveMQJAASSecurityManager("CertLogin"));
server.getConfiguration().setSecurityEnabled(true);
}
/*
* Basic mutual SSL test with certificate-based authentication
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testMutualSsl() throws Exception {
MqttClient client = createPahoClient("client");
client.connect(getSslMqttConnectOptions());
}
}

View File

@ -0,0 +1,113 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5;
import javax.jms.JMSConsumer;
import javax.jms.JMSContext;
import javax.jms.Message;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTUtil;
import org.apache.activemq.artemis.tests.util.RandomUtil;
import org.apache.activemq.artemis.utils.Wait;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.eclipse.paho.mqttv5.common.MqttMessage;
import org.jboss.logging.Logger;
import org.junit.Assume;
import org.junit.Test;
/*
* General tests for things not covered directly in the specification.
*/
public class MQTT5Test extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(MQTT5Test.class);
public MQTT5Test(String protocol) {
super(protocol);
}
/*
* Ensure that the broker adds a timestamp on the message when sending via MQTT
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testTimestamp() throws Exception {
final String DESTINATION = RandomUtil.randomString();
createJMSConnection();
JMSContext context = cf.createContext();
JMSConsumer consumer = context.createConsumer(context.createQueue(DESTINATION));
long time = System.currentTimeMillis();
MqttClient producer = createPahoClient(RandomUtil.randomString());
producer.connect();
producer.publish(DESTINATION, new byte[0], 1, false);
producer.disconnect();
producer.close();
Message m = consumer.receive(200);
assertNotNull(m);
assertTrue(m.getJMSTimestamp() > time);
context.close();
}
/*
* Trying to reproduce error from https://issues.apache.org/jira/browse/ARTEMIS-1184
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testMaxMessageSize() throws Exception {
// this doesn't work with websockets because the websocket frame size is too low
Assume.assumeTrue(protocol.equals(TCP));
final String TOPIC = RandomUtil.randomString();
// subtract a little to leave room for the header
final int SIZE = MQTTUtil.MAX_PACKET_SIZE - 48;
StringBuilder builder = new StringBuilder(SIZE);
for (int i = 0; i < SIZE; i++) {
builder.append("=");
}
byte[] bytes = builder.toString().getBytes(StandardCharsets.UTF_8);
final CountDownLatch latch = new CountDownLatch(1);
MqttClient consumer = createPahoClient("consumer");
consumer.setCallback(new DefaultMqttCallback() {
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
assertEqualsByteArrays(bytes.length, bytes, message.getPayload());
latch.countDown();
}
});
consumer.connect();
consumer.subscribe(TOPIC, 1);
MqttClient producer = createPahoClient(RandomUtil.randomString());
producer.connect();
producer.publish(TOPIC, bytes, 1, false);
producer.disconnect();
producer.close();
Wait.assertEquals(1L, () -> getSubscriptionQueue(TOPIC).getMessagesAdded(), 2000, 100);
assertTrue(latch.await(30, TimeUnit.SECONDS));
consumer.disconnect();
consumer.close();
}
}

View File

@ -0,0 +1,478 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5;
import javax.jms.ConnectionFactory;
import java.io.File;
import java.io.IOException;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import io.netty.handler.codec.mqtt.MqttMessage;
import io.netty.handler.codec.mqtt.MqttPublishMessage;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.api.core.TransportConfiguration;
import org.apache.activemq.artemis.core.config.Configuration;
import org.apache.activemq.artemis.core.postoffice.Binding;
import org.apache.activemq.artemis.core.postoffice.impl.LocalQueueBinding;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTInterceptor;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTProtocolManager;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTSessionState;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTUtil;
import org.apache.activemq.artemis.core.remoting.impl.AbstractAcceptor;
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants;
import org.apache.activemq.artemis.core.security.Role;
import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.core.server.Queue;
import org.apache.activemq.artemis.core.settings.HierarchicalRepository;
import org.apache.activemq.artemis.core.settings.impl.AddressSettings;
import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory;
import org.apache.activemq.artemis.spi.core.protocol.ProtocolManager;
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
import org.apache.activemq.artemis.spi.core.remoting.Acceptor;
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
import org.apache.activemq.artemis.tests.util.ActiveMQTestBase;
import org.apache.activemq.artemis.utils.ClassloadingUtil;
import org.eclipse.paho.mqttv5.client.IMqttToken;
import org.eclipse.paho.mqttv5.client.MqttAsyncClient;
import org.eclipse.paho.mqttv5.client.MqttCallback;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse;
import org.eclipse.paho.mqttv5.client.persist.MemoryPersistence;
import org.eclipse.paho.mqttv5.common.MqttException;
import org.eclipse.paho.mqttv5.common.packet.MqttProperties;
import org.jboss.logging.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import static java.util.Collections.singletonList;
import static org.apache.activemq.artemis.core.protocol.mqtt.MQTTProtocolManagerFactory.MQTT_PROTOCOL_NAME;
@RunWith(Parameterized.class)
public class MQTT5TestSupport extends ActiveMQTestBase {
protected static final String TCP = "tcp";
protected static final String WS = "ws";
protected static final String SSL = "ssl";
protected static final String WSS = "wss";
protected static final SimpleString DEAD_LETTER_ADDRESS = new SimpleString("DLA");
protected static final SimpleString EXPIRY_ADDRESS = new SimpleString("EXPIRY");
@Parameterized.Parameters(name = "protocol={0}")
public static Collection<Object[]> getParams() {
return Arrays.asList(new Object[][] {
{TCP},
{WS}
});
}
protected String protocol;
public MQTT5TestSupport(String protocol) {
this.protocol = protocol;
}
protected MqttClient createPahoClient(String clientId) throws MqttException {
return new MqttClient(protocol + "://localhost:" + (isUseSsl() ? getSslPort() : getPort()), clientId, new MemoryPersistence());
}
protected MqttAsyncClient createAsyncPahoClient(String clientId) throws MqttException {
return new MqttAsyncClient(protocol + "://localhost:" + (isUseSsl() ? getSslPort() : getPort()), clientId, new MemoryPersistence());
}
private static final Logger log = Logger.getLogger(MQTT5TestSupport.class);
protected static final long DEFAULT_TIMEOUT = 300000;
protected ActiveMQServer server;
protected int port = 1883;
protected int sslPort = 8883;
protected ConnectionFactory cf;
protected LinkedList<Throwable> exceptions = new LinkedList<>();
protected boolean persistent;
protected String protocolScheme;
protected static final int NUM_MESSAGES = 250;
public static final int AT_MOST_ONCE = 0;
public static final int AT_LEAST_ONCE = 1;
public static final int EXACTLY_ONCE = 2;
protected String noprivUser = "noprivs";
protected String noprivPass = "noprivs";
protected String browseUser = "browser";
protected String browsePass = "browser";
protected String guestUser = "guest";
protected String guestPass = "guest";
protected String fullUser = "user";
protected String fullPass = "pass";
@Rule
public TestName name = new TestName();
public MQTT5TestSupport() {
this.protocolScheme = "mqtt";
}
public File basedir() throws IOException {
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
return new File(new File(protectionDomain.getCodeSource().getLocation().getPath()), "../..").getCanonicalFile();
}
@Override
public String getName() {
return name.getMethodName();
}
public ActiveMQServer getServer() {
return server;
}
@Override
@Before
public void setUp() throws Exception {
exceptions.clear();
startBroker();
createJMSConnection();
org.jboss.logmanager.Logger.getLogger(MQTTUtil.class.getName()).setLevel(org.jboss.logmanager.Level.TRACE);
}
@Override
@After
public void tearDown() throws Exception {
stopBroker();
super.tearDown();
}
public void configureBroker() throws Exception {
super.setUp();
server = createServerForMQTT();
addCoreConnector();
addMQTTConnector();
AddressSettings addressSettings = new AddressSettings();
addressSettings.setMaxSizeBytes(999999999);
addressSettings.setAutoCreateQueues(true);
addressSettings.setAutoCreateAddresses(true);
configureBrokerSecurity(server);
server.getAddressSettingsRepository().addMatch("#", addressSettings);
server.getConfiguration().setMessageExpiryScanPeriod(500);
}
/**
* Copied from org.apache.activemq.artemis.tests.integration.amqp.AmqpClientTestSupport#configureBrokerSecurity()
*/
protected void configureBrokerSecurity(ActiveMQServer server) {
if (isSecurityEnabled()) {
ActiveMQJAASSecurityManager securityManager = (ActiveMQJAASSecurityManager) server.getSecurityManager();
// User additions
securityManager.getConfiguration().addUser(noprivUser, noprivPass);
securityManager.getConfiguration().addRole(noprivUser, "nothing");
securityManager.getConfiguration().addUser(browseUser, browsePass);
securityManager.getConfiguration().addRole(browseUser, "browser");
securityManager.getConfiguration().addUser(guestUser, guestPass);
securityManager.getConfiguration().addRole(guestUser, "guest");
securityManager.getConfiguration().addUser(fullUser, fullPass);
securityManager.getConfiguration().addRole(fullUser, "full");
// Configure roles
HierarchicalRepository<Set<Role>> securityRepository = server.getSecurityRepository();
HashSet<Role> value = new HashSet<>();
value.add(new Role("nothing", false, false, false, false, false, false, false, false, false, false));
value.add(new Role("browser", false, false, false, false, false, false, false, true, false, false));
value.add(new Role("guest", false, true, false, false, false, false, false, true, false, false));
value.add(new Role("full", true, true, true, true, true, true, true, true, true, true));
securityRepository.addMatch("#", value);
server.getConfiguration().setSecurityEnabled(true);
} else {
server.getConfiguration().setSecurityEnabled(false);
}
}
public void startBroker() throws Exception {
configureBroker();
server.start();
server.waitForActivation(10, TimeUnit.SECONDS);
}
public void createJMSConnection() throws Exception {
cf = new ActiveMQConnectionFactory(false, new TransportConfiguration(ActiveMQTestBase.NETTY_CONNECTOR_FACTORY));
}
private ActiveMQServer createServerForMQTT() throws Exception {
Configuration defaultConfig = createDefaultConfig(true).setIncomingInterceptorClassNames(singletonList(MQTTIncomingInterceptor.class.getName())).setOutgoingInterceptorClassNames(singletonList(MQTTOutoingInterceptor.class.getName()));
AddressSettings addressSettings = new AddressSettings();
addressSettings.setDeadLetterAddress(DEAD_LETTER_ADDRESS);
addressSettings.setExpiryAddress(EXPIRY_ADDRESS);
defaultConfig.getAddressesSettings().put("#", addressSettings);
defaultConfig.setMqttSessionScanInterval(200);
return createServer(true, defaultConfig);
}
protected void addCoreConnector() throws Exception {
// Overrides of this method can add additional configuration options or add multiple
// MQTT transport connectors as needed, the port variable is always supposed to be
// assigned the primary MQTT connector's port.
Map<String, Object> params = new HashMap<>();
params.put(TransportConstants.PORT_PROP_NAME, "" + 5445);
params.put(TransportConstants.PROTOCOLS_PROP_NAME, "CORE");
TransportConfiguration transportConfiguration = new TransportConfiguration(NETTY_ACCEPTOR_FACTORY, params);
server.getConfiguration().getAcceptorConfigurations().add(transportConfiguration);
log.debug("Added CORE connector to broker");
}
protected void addMQTTConnector() throws Exception {
// Overrides of this method can add additional configuration options or add multiple
// MQTT transport connectors as needed, the port variable is always supposed to be
// assigned the primary MQTT connector's port.
server.getConfiguration().addAcceptorConfiguration(MQTT_PROTOCOL_NAME, "tcp://localhost:" + (isUseSsl() ? sslPort : port) + "?protocols=MQTT;anycastPrefix=anycast:;multicastPrefix=multicast:" + (isUseSsl() ? "&sslEnabled=true&keyStorePath=server-keystore.p12&keyStorePassword=securepass" : "") + (isMutualSsl() ? "&needClientAuth=true&trustStorePath=client-ca-truststore.p12&trustStorePassword=securepass" : ""));
server.getConfiguration().setConnectionTtlCheckInterval(100);
log.debug("Added MQTT connector to broker");
}
public void stopBroker() throws Exception {
if (server.isStarted()) {
server.stop();
server = null;
}
}
protected String getQueueName() {
return getClass().getName() + "." + name.getMethodName();
}
protected String getTopicName() {
return getClass().getName() + "." + name.getMethodName();
}
public boolean isPersistent() {
return persistent;
}
public int getPort() {
return this.port;
}
public int getSslPort() {
return this.sslPort;
}
public boolean isSecurityEnabled() {
return false;
}
public boolean isUseSsl() {
return false;
}
public boolean isMutualSsl() {
return false;
}
protected interface Task {
void run() throws Exception;
}
public Map<String, MQTTSessionState> getSessionStates() {
Acceptor acceptor = server.getRemotingService().getAcceptor("MQTT");
if (acceptor instanceof AbstractAcceptor) {
ProtocolManager protocolManager = ((AbstractAcceptor) acceptor).getProtocolMap().get("MQTT");
if (protocolManager instanceof MQTTProtocolManager) {
return ((MQTTProtocolManager) protocolManager).getSessionStates();
}
}
return Collections.emptyMap();
}
protected Queue getSubscriptionQueue(String TOPIC) {
try {
return ((LocalQueueBinding)server.getPostOffice().getBindingsForAddress(SimpleString.toSimpleString(TOPIC)).getBindings().toArray()[0]).getQueue();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
protected Queue getSubscriptionQueue(String TOPIC, String clientId) {
try {
for (Binding b : server.getPostOffice().getMatchingBindings(SimpleString.toSimpleString(TOPIC))) {
if (((LocalQueueBinding)b).getQueue().getName().startsWith(SimpleString.toSimpleString(clientId))) {
return ((LocalQueueBinding)b).getQueue();
}
}
return null;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
protected void setAcceptorProperty(String property) throws Exception {
server.getRemotingService().getAcceptor(MQTT_PROTOCOL_NAME).stop();
server.getRemotingService().createAcceptor(MQTT_PROTOCOL_NAME, "tcp://localhost:" + port + "?protocols=MQTT;" + property).start();
}
/*
* From the Paho MQTT client's JavaDoc:
*
* com.ibm.ssl.protocol - One of: SSL, SSLv3, TLS, TLSv1, SSL_TLS.
* com.ibm.ssl.contextProvider - Underlying JSSE provider. For example "IBMJSSE2" or "SunJSSE"
* com.ibm.ssl.keyStore - The name of the file that contains the KeyStore object that you want the KeyManager to use. For example /mydir/etc/key.p12
* com.ibm.ssl.keyStorePassword -The password for the KeyStore object that you want the KeyManager to use. The password can either be in plain-text, or may be obfuscated using the static method: com.ibm.micro.security.Password.obfuscate(char[] password). This obfuscates the password using a simple and insecure XOR and Base64 encoding mechanism. Note that this is only a simple scrambler to obfuscate clear-text passwords.
* com.ibm.ssl.keyStoreType - Type of key store, for example "PKCS12", "JKS", or "JCEKS".
* com.ibm.ssl.keyStoreProvider - Key store provider, for example "IBMJCE" or "IBMJCEFIPS".
* com.ibm.ssl.trustStore - The name of the file that contains the KeyStore object that you want the TrustManager to use.
* com.ibm.ssl.trustStorePassword - The password for the TrustStore object that you want the TrustManager to use. The password can either be in plain-text, or may be obfuscated using the static method: com.ibm.micro.security.Password.obfuscate(char[] password). This obfuscates the password using a simple and insecure XOR and Base64 encoding mechanism. Note that this is only a simple scrambler to obfuscate clear-text passwords.
* com.ibm.ssl.trustStoreType - The type of KeyStore object that you want the default TrustManager to use. Same possible values as "keyStoreType".
* com.ibm.ssl.trustStoreProvider - Trust store provider, for example "IBMJCE" or "IBMJCEFIPS".
* com.ibm.ssl.enabledCipherSuites - A list of which ciphers are enabled. Values are dependent on the provider, for example: SSL_RSA_WITH_AES_128_CBC_SHA;SSL_RSA_WITH_3DES_EDE_CBC_SHA.
* com.ibm.ssl.keyManager - Sets the algorithm that will be used to instantiate a KeyManagerFactory object instead of using the default algorithm available in the platform. Example values: "IbmX509" or "IBMJ9X509".
* com.ibm.ssl.trustManager - Sets the algorithm that will be used to instantiate a TrustManagerFactory object instead of using the default algorithm available in the platform. Example values: "PKIX" or "IBMJ9X509".
*/
protected MqttConnectionOptions getSslMqttConnectOptions() {
MqttConnectionOptions connectionOptions = new MqttConnectionOptions();
Properties properties = new Properties();
properties.setProperty("com.ibm.ssl.trustStore", ClassloadingUtil.findResource("server-ca-truststore.p12").getPath());
properties.setProperty("com.ibm.ssl.trustStorePassword", "securepass");
if (isMutualSsl()) {
properties.setProperty("com.ibm.ssl.keyStore", ClassloadingUtil.findResource("client-keystore.p12").getPath());
properties.setProperty("com.ibm.ssl.keyStorePassword", "securepass");
}
connectionOptions.setSSLProperties(properties);
return connectionOptions;
}
public static class MQTTIncomingInterceptor implements MQTTInterceptor {
private static int messageCount = 0;
@Override
public boolean intercept(MqttMessage packet, RemotingConnection connection) throws ActiveMQException {
if (packet.getClass() == MqttPublishMessage.class) {
messageCount++;
}
return true;
}
public static void clear() {
messageCount = 0;
}
public static int getMessageCount() {
return messageCount;
}
}
public static class MQTTOutoingInterceptor implements MQTTInterceptor {
private static int messageCount = 0;
@Override
public boolean intercept(MqttMessage packet, RemotingConnection connection) throws ActiveMQException {
if (packet.getClass() == MqttPublishMessage.class) {
messageCount++;
}
return true;
}
public static void clear() {
messageCount = 0;
}
public static int getMessageCount() {
return messageCount;
}
}
protected interface DefaultMqttCallback extends MqttCallback {
@Override
default void disconnected(MqttDisconnectResponse disconnectResponse) {
}
@Override
default void mqttErrorOccurred(MqttException exception) {
}
@Override
default void messageArrived(String topic, org.eclipse.paho.mqttv5.common.MqttMessage message) throws Exception {
}
@Override
default void deliveryComplete(IMqttToken token) {
}
@Override
default void connectComplete(boolean reconnect, String serverURI) {
}
@Override
default void authPacketArrived(int reasonCode, MqttProperties properties) {
}
}
protected class LatchedMqttCallback implements DefaultMqttCallback {
CountDownLatch latch;
boolean fail;
public LatchedMqttCallback(CountDownLatch latch) {
this.latch = latch;
this.fail = false;
}
public LatchedMqttCallback(CountDownLatch latch, boolean fail) {
this.latch = latch;
this.fail = fail;
}
@Override
public void messageArrived(String topic, org.eclipse.paho.mqttv5.common.MqttMessage message) throws Exception {
System.out.println("Message arrived: " + message);
latch.countDown();
if (fail) {
throw new Exception();
}
}
}
}

View File

@ -0,0 +1,32 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This package contains all the tests for the MQTT 5 implementation.
*
* The tests are named according to the chapter of the specification from which they come. Each test class and method
* includes references from the specification for what they are (and are not) testing. The specification is available
* at:
*
* https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html
*
* Summary of the new features in MQTT 5 vs. 3.1.1:
*
* https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901293
*
*/
package org.apache.activemq.artemis.tests.integration.mqtt5;

View File

@ -0,0 +1,330 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import io.netty.handler.codec.mqtt.MqttMessageIdAndPropertiesVariableHeader;
import io.netty.handler.codec.mqtt.MqttMessageType;
import io.netty.handler.codec.mqtt.MqttPubAckMessage;
import io.netty.handler.codec.mqtt.MqttPubReplyMessageVariableHeader;
import io.netty.handler.codec.mqtt.MqttPublishMessage;
import io.netty.handler.codec.mqtt.MqttSubscribeMessage;
import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTInterceptor;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.apache.activemq.artemis.tests.util.RandomUtil;
import org.apache.activemq.artemis.tests.util.Wait;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.eclipse.paho.mqttv5.common.MqttMessage;
import org.jboss.logging.Logger;
import org.junit.Test;
/**
* Fulfilled by client or Netty codec (i.e. not explicitly tested here):
*
* [MQTT-2.1.3-1] Where a flag bit is marked as Reserved it is reserved for future use and MUST be set to the value listed.
* [MQTT-2.2.2-1] If there are no properties, this MUST be indicated by including a Property Length of zero.
* [MQTT-2.2.1-3] Each time a Client sends a new SUBSCRIBE, UNSUBSCRIBE,or PUBLISH (where QoS > 0) MQTT Control Packet it MUST assign it a non-zero Packet Identifier that is currently unused.
*/
public class ControlPacketFormatTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(ControlPacketFormatTests.class);
public ControlPacketFormatTests(String protocol) {
super(protocol);
}
/*
* [MQTT-2.2.1-2] A PUBLISH packet MUST NOT contain a Packet Identifier if its QoS value is set to 0.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testPacketIdQoSZero() throws Exception {
final String TOPIC = this.getTopicName();
final int MESSAGE_COUNT = 100;
final CountDownLatch latch = new CountDownLatch(MESSAGE_COUNT);
MqttClient consumer = createPahoClient("consumer");
consumer.setCallback(new DefaultMqttCallback() {
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
assertEquals(0, message.getId());
assertEquals(0, message.getQos());
latch.countDown();
}
});
consumer.connect();
consumer.subscribe(TOPIC, 0);
MqttClient producer = createPahoClient("producer");
producer.connect();
for (int i = 0; i < MESSAGE_COUNT; i++) {
producer.publish(TOPIC, ("foo" + i).getBytes(), 0, false);
}
Wait.assertEquals(MESSAGE_COUNT, () -> getSubscriptionQueue(TOPIC).getMessagesAdded());
producer.disconnect();
producer.close();
assertTrue(latch.await(3, TimeUnit.SECONDS));
consumer.disconnect();
consumer.close();
}
/*
* [MQTT-2.2.1-4] Each time a Server sends a new PUBLISH (with QoS > 0) MQTT Control Packet it MUST assign it a non
* zero Packet Identifier that is currently unused.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testPacketIdQoSGreaterThanZero() throws Exception {
final String CONSUMER_ID = RandomUtil.randomString();
final String TOPIC = this.getTopicName();
final int MESSAGE_COUNT = 10;
final List IDS = new ArrayList();
final Object lock = new Object();
final CountDownLatch latch = new CountDownLatch(MESSAGE_COUNT);
MqttClient consumer = createPahoClient(CONSUMER_ID);
consumer.setCallback(new DefaultMqttCallback() {
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
synchronized (lock) {
System.out.println(message.getId());
assertFalse(IDS.contains(message.getId()));
IDS.add(message.getId());
latch.countDown();
}
}
});
consumer.connect();
consumer.subscribe(TOPIC, 2);
Wait.assertTrue(() -> getSubscriptionQueue(TOPIC) != null);
Wait.assertEquals(1, () -> getSubscriptionQueue(TOPIC).getConsumerCount());
MqttClient producer = createPahoClient("producer");
producer.connect();
for (int i = 0; i < MESSAGE_COUNT; i++) {
producer.publish(TOPIC, ("foo" + i).getBytes(), (RandomUtil.randomPositiveInt() % 2) + 1, false);
}
Wait.assertEquals(MESSAGE_COUNT, () -> getSubscriptionQueue(TOPIC).getMessagesAdded());
producer.disconnect();
producer.close();
assertTrue(latch.await(3, TimeUnit.SECONDS));
consumer.disconnect();
consumer.close();
}
/*
* [MQTT-2.2.1-5] A PUBACK, PUBREC, PUBREL, or PUBCOMP packet MUST contain the same Packet Identifier as the PUBLISH
* packet that was originally sent.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testPacketIdPubAckQoS2() throws Exception {
AtomicInteger id = new AtomicInteger(0);
AtomicBoolean failed = new AtomicBoolean(false);
AtomicInteger packetCount = new AtomicInteger(0);
MQTTInterceptor incomingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBLISH) {
id.set(((MqttPublishMessage)packet).variableHeader().packetId());
packetCount.incrementAndGet();
}
if (packet.fixedHeader().messageType() == MqttMessageType.PUBREL || packet.fixedHeader().messageType() == MqttMessageType.PUBREC || packet.fixedHeader().messageType() == MqttMessageType.PUBCOMP) {
if (((MqttPubReplyMessageVariableHeader)packet.variableHeader()).messageId() != id.get()) {
failed.set(true);
}
packetCount.incrementAndGet();
}
return true;
};
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBLISH) {
if (((MqttPublishMessage)packet).variableHeader().packetId() != id.get()) {
failed.set(true);
}
packetCount.incrementAndGet();
}
if (packet.fixedHeader().messageType() == MqttMessageType.PUBREL || packet.fixedHeader().messageType() == MqttMessageType.PUBREC || packet.fixedHeader().messageType() == MqttMessageType.PUBCOMP) {
if (((MqttPubAckMessage)packet).variableHeader().messageId() != id.get()) {
failed.set(true);
}
packetCount.incrementAndGet();
}
return true;
};
server.getRemotingService().addIncomingInterceptor(incomingInterceptor);
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
final String TOPIC = this.getTopicName();
final CountDownLatch latch = new CountDownLatch(1);
MqttClient consumer = createPahoClient("consumer");
consumer.setCallback(new DefaultMqttCallback() {
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
latch.countDown();
}
});
consumer.connect();
consumer.subscribe(TOPIC, 2);
MqttClient producer = createPahoClient("producer");
producer.connect();
producer.publish(TOPIC, "foo".getBytes(StandardCharsets.UTF_8), 2, false);
Wait.assertEquals((long) 1, () -> getSubscriptionQueue(TOPIC).getMessagesAdded(), 2000, 100);
producer.disconnect();
producer.close();
Wait.assertEquals(1L, () -> getSubscriptionQueue(TOPIC).getMessagesAcknowledged(), 15000, 100);
assertTrue(latch.await(15, TimeUnit.SECONDS));
Wait.assertFalse(() -> failed.get(), 2000, 100);
Wait.assertEquals(8, () -> packetCount.get());
consumer.disconnect();
consumer.close();
}
/*
* [MQTT-2.2.1-5] A PUBACK, PUBREC, PUBREL, or PUBCOMP packet MUST contain the same Packet Identifier as the PUBLISH
* packet that was originally sent.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testPacketIdPubAckQoS1() throws Exception {
AtomicInteger id = new AtomicInteger(0);
AtomicBoolean failed = new AtomicBoolean(false);
AtomicInteger packetCount = new AtomicInteger(0);
MQTTInterceptor incomingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBLISH) {
id.set(((MqttPublishMessage)packet).variableHeader().packetId());
packetCount.incrementAndGet();
}
if (packet.fixedHeader().messageType() == MqttMessageType.PUBACK) {
if (((MqttPubReplyMessageVariableHeader)packet.variableHeader()).messageId() != id.get()) {
failed.set(true);
}
packetCount.incrementAndGet();
}
return true;
};
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBLISH) {
if (((MqttPublishMessage)packet).variableHeader().packetId() != id.get()) {
failed.set(true);
}
packetCount.incrementAndGet();
}
if (packet.fixedHeader().messageType() == MqttMessageType.PUBACK) {
if (((MqttPubAckMessage)packet).variableHeader().messageId() != id.get()) {
failed.set(true);
}
packetCount.incrementAndGet();
}
return true;
};
server.getRemotingService().addIncomingInterceptor(incomingInterceptor);
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
final String TOPIC = this.getTopicName();
final CountDownLatch latch = new CountDownLatch(1);
MqttClient consumer = createPahoClient("consumer");
consumer.setCallback(new DefaultMqttCallback() {
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
latch.countDown();
}
});
consumer.connect();
consumer.subscribe(TOPIC, 1);
MqttClient producer = createPahoClient("producer");
producer.connect();
producer.publish(TOPIC, "foo".getBytes(StandardCharsets.UTF_8), 1, false);
Wait.assertEquals((long) 1, () -> getSubscriptionQueue(TOPIC).getMessagesAdded(), 2000, 100);
producer.disconnect();
producer.close();
Wait.assertEquals(1L, () -> getSubscriptionQueue(TOPIC).getMessagesAcknowledged(), 15000, 100);
assertTrue(latch.await(15, TimeUnit.SECONDS));
Wait.assertFalse(() -> failed.get(), 2000, 100);
Wait.assertEquals(4, () -> packetCount.get());
consumer.disconnect();
consumer.close();
}
/*
* [MQTT-2.2.1-6] A SUBACK and UNSUBACK MUST contain the Packet Identifier that was used in the corresponding
* SUBSCRIBE and UNSUBSCRIBE packet respectively.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testPacketIdSubAckAndUnsubAck() throws Exception {
AtomicInteger subId = new AtomicInteger(0);
AtomicInteger unsubId = new AtomicInteger(0);
AtomicInteger packetCount = new AtomicInteger(0);
AtomicBoolean failed = new AtomicBoolean(false);
MQTTInterceptor incomingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.SUBSCRIBE) {
subId.set(((MqttSubscribeMessage)packet).variableHeader().messageId());
packetCount.incrementAndGet();
}
if (packet.fixedHeader().messageType() == MqttMessageType.UNSUBSCRIBE) {
unsubId.set(((MqttUnsubscribeMessage)packet).variableHeader().messageId());
packetCount.incrementAndGet();
}
return true;
};
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.SUBACK) {
if (((MqttMessageIdAndPropertiesVariableHeader)packet.variableHeader()).messageId() != subId.get()) {
failed.set(true);
}
packetCount.incrementAndGet();
} else if (packet.fixedHeader().messageType() == MqttMessageType.UNSUBACK) {
if (((MqttMessageIdAndPropertiesVariableHeader)packet.variableHeader()).messageId() != unsubId.get()) {
failed.set(true);
}
packetCount.incrementAndGet();
}
return true;
};
server.getRemotingService().addIncomingInterceptor(incomingInterceptor);
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
final String TOPIC = this.getTopicName();
MqttClient consumer = createPahoClient("consumer");
consumer.connect();
consumer.subscribe(TOPIC, 1);
consumer.unsubscribe(TOPIC);
Wait.assertFalse(() -> failed.get(), 2000, 100);
Wait.assertEquals(4, () -> packetCount.get());
consumer.disconnect();
consumer.close();
}
}

View File

@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.jboss.logging.Logger;
import org.junit.Ignore;
/**
* Fulfilled by client or Netty codec (i.e. not explicitly tested here):
*
* [MQTT-1.5.4-1] The character data in a UTF-8 Encoded String MUST be well-formed UTF-8 as defined by the Unicode specification [Unicode] and restated in RFC 3629 [RFC3629]. In particular, the character data MUST NOT include encodings of code points between U+D800 and U+DFFF.
* [MQTT-1.5.4-2] A UTF-8 Encoded String MUST NOT include an encoding of the null character U+0000.
* [MQTT-1.5.4-3] A UTF-8 encoded sequence 0xEF 0xBB 0xBF is always interpreted as U+FEFF ("ZERO WIDTH NO-BREAK SPACE") wherever it appears in a string and MUST NOT be skipped over or stripped off by a packet receiver.
* [MQTT-1.5.5-1] The encoded value MUST use the minimum number of bytes necessary to represent the value.
* [MQTT-1.5.7-1] Both strings MUST comply with the requirements for UTF-8 Encoded Strings.
*/
@Ignore
public class DataFormatTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(DataFormatTests.class);
public DataFormatTests(String protocol) {
super(protocol);
}
}

View File

@ -0,0 +1,74 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTReasonCodes;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.apache.activemq.artemis.tests.util.RandomUtil;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
import org.eclipse.paho.mqttv5.common.MqttException;
import org.jboss.logging.Logger;
import org.junit.Test;
/**
* Fulfilled by client or Netty codec (i.e. not tested here):
*
* [MQTT-4.12.0-3] The Client responds to an AUTH packet from the Server by sending a further AUTH packet. This packet MUST contain a Reason Code of 0x18 (Continue authentication).
* [MQTT-4.12.0-7] If the Client does not include an Authentication Method in the CONNECT, the Client MUST NOT send an AUTH packet to the Server.
*
*
* Not Implemented:
*
* [MQTT-4.12.0-2] If the Server requires additional information to complete the authorization, it can send an AUTH packet to the Client. This packet MUST contain a Reason Code of 0x18 (Continue authentication).
* [MQTT-4.12.0-4] The Server can reject the authentication at any point in this process. It MAY send a CONNACK with a Reason Code of 0x80 or above as described in section 4.13, and MUST close the Network Connection.
* [MQTT-4.12.0-5] If the initial CONNECT packet included an Authentication Method property then all AUTH packets, and any successful CONNACK packet MUST include an Authentication Method Property with the same value as in the CONNECT packet.
* [MQTT-4.12.0-6] If the Client does not include an Authentication Method in the CONNECT, the Server MUST NOT send an AUTH packet, and it MUST NOT send an Authentication Method in the CONNACK packet.
* [MQTT-4.12.1-1] If the Client supplied an Authentication Method in the CONNECT packet it can initiate a re-authentication at any time after receiving a CONNACK. It does this by sending an AUTH packet with a Reason Code of 0x19 (Re-authentication). The Client MUST set the Authentication Method to the same value as the Authentication Method originally used to authenticate the Network Connection.
* [MQTT-4.12.1-2] If the re-authentication fails, the Client or Server SHOULD send DISCONNECT with an appropriate Reason Code and MUST close the Network Connection.
*/
public class EnhancedAuthenticationTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(EnhancedAuthenticationTests.class);
public EnhancedAuthenticationTests(String protocol) {
super(protocol);
}
/*
* [MQTT-4.12.0-1] If the Server does not support the Authentication Method supplied by the Client, it MAY send a
* CONNACK with a Reason Code of 0x8C (Bad authentication method) or 0x87 (Not Authorized) as described in section
* 4.13 and MUST close the Network Connection.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testBadAuthenticationMethod() throws Exception {
final String CLIENT_ID = RandomUtil.randomString();
MqttClient client = createPahoClient(CLIENT_ID);
MqttConnectionOptions options = new MqttConnectionOptions();
options.setAuthMethod(RandomUtil.randomString());
try {
client.connect(options);
fail("should have thrown an exception when connecting");
} catch (MqttException e) {
assertEquals(MQTTReasonCodes.BAD_AUTHENTICATION_METHOD, (byte) e.getReasonCode());
}
assertFalse(client.isConnected());
}
}

View File

@ -0,0 +1,44 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.jboss.logging.Logger;
import org.junit.Ignore;
/**
* The MQTT 5 specification discusses a "send quota," but this is really an implementation detail and therefore not explicitly tested here:
*
* [MQTT-4.9.0-1] The Client or Server MUST set its initial send quota to a non-zero value not exceeding the Receive Maximum.
* [MQTT-4.9.0-2] Each time the Client or Server sends a PUBLISH packet at QoS > 0, it decrements the send quota. If the send quota reaches zero, the Client or Server MUST NOT send any more PUBLISH packets with QoS > 0.
*
*
* This is tested in org.apache.activemq.artemis.tests.integration.mqtt5.spec.controlpackets.PublishTests#testPacketDelayReceiveMaximum():
*
* [MQTT-4.9.0-3] The Client and Server MUST continue to process and respond to all other MQTT Control Packets even if the quota is zero.
*/
@Ignore
public class FlowControlTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(FlowControlTests.class);
public FlowControlTests(String protocol) {
super(protocol);
}
}

View File

@ -0,0 +1,70 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTReasonCodes;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptionsBuilder;
import org.eclipse.paho.mqttv5.common.MqttException;
import org.jboss.logging.Logger;
import org.junit.Assume;
import org.junit.Test;
/**
* Fulfilled by client or Netty codec (i.e. not tested here):
*
* [MQTT-4.13.1-1] When a Server detects a Malformed Packet or Protocol Error, and a Reason Code is given in the specification, it MUST close the Network Connection.
*/
public class HandlingErrorTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(HandlingErrorTests.class);
public HandlingErrorTests(String protocol) {
this.protocol = protocol;
}
/*
* [MQTT-4.13.2-1] The CONNACK and DISCONNECT packets allow a Reason Code of 0x80 or greater to indicate that the
* Network Connection will be closed. If a Reason Code of 0x80 or greater is specified, then the Network Connection
* MUST be closed whether or not the CONNACK or DISCONNECT is sent.
*
* This is one possible error condition where a Reason Code > 0x80 is specified and the network connection is closed.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testEmptyClientIDWithoutCleanStart() throws Exception {
// This is apparently broken with the Paho client + web socket. The broker never even receives a CONNECT packet.
Assume.assumeTrue(protocol.equals(TCP));
MqttClient client = createPahoClient("");
MqttConnectionOptions options = new MqttConnectionOptionsBuilder()
.cleanStart(false)
.build();
try {
client.connect(options);
fail("Should throw exception about invalid client identifier");
} catch (MqttException e) {
assertEquals(Byte.toUnsignedInt(MQTTReasonCodes.CLIENT_IDENTIFIER_NOT_VALID), e.getReasonCode());
}
assertFalse(client.isConnected());
assertEquals(0, getSessionStates().size());
}
}

View File

@ -0,0 +1,112 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.apache.activemq.artemis.tests.util.RandomUtil;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptionsBuilder;
import org.jboss.logging.Logger;
import org.junit.Test;
public class MessageDeliveryRetryTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(MessageDeliveryRetryTests.class);
public MessageDeliveryRetryTests(String protocol) {
super(protocol);
}
/*
* [MQTT-4.4.0-1] When a Client reconnects with Clean Start set to 0 and a session is present, both the Client and
* Server MUST resend any unacknowledged PUBLISH packets (where QoS > 0) and PUBREL packets using their original
* Packet Identifiers. This is the only circumstance where a Client or Server is REQUIRED to resend messages.
* Clients and Servers MUST NOT resend messages at any other time.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testCleanStartFalseWithReconnect() throws Exception {
final String CONSUMER_ID = RandomUtil.randomString();
final String TOPIC = this.getTopicName();
final CountDownLatch latch = new CountDownLatch(1);
MqttClient producer = createPahoClient(RandomUtil.randomString());
MqttClient consumer = createPahoClient(CONSUMER_ID);
MqttConnectionOptions options = new MqttConnectionOptionsBuilder()
.cleanStart(false)
.sessionExpiryInterval(300L)
.build();
consumer.connect(options);
consumer.subscribe(TOPIC, 2);
consumer.disconnect();
// session should still exist since session expiry interval > 0
assertEquals(1, getSessionStates().size());
assertNotNull(getSessionStates().get(CONSUMER_ID));
producer.connect();
producer.publish(TOPIC, "hello".getBytes(), 2, false);
producer.disconnect();
producer.close();
// session should still exist since session expiry interval > 0
assertEquals(1, getSessionStates().size());
assertNotNull(getSessionStates().get(CONSUMER_ID));
// consumer should resume previous session (i.e. get the messages sent to the queue where it was previously subscribed)
consumer.setCallback(new LatchedMqttCallback(latch));
consumer.connect(options);
waitForLatch(latch);
consumer.disconnect();
consumer.close();
// session should still exist since session expiry interval > 0
assertEquals(1, getSessionStates().size());
assertNotNull(getSessionStates().get(CONSUMER_ID));
}
/*
* [MQTT-4.4.0-2] If PUBACK or PUBREC is received containing a Reason Code of 0x80 or greater the corresponding
* PUBLISH packet is treated as acknowledged, and MUST NOT be retransmitted.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testTopicFilter() throws Exception {
final String CONSUMER_ID = RandomUtil.randomString();
final String TOPIC = this.getTopicName();
final CountDownLatch latch = new CountDownLatch(1);
MqttClient consumer = createPahoClient(CONSUMER_ID);
consumer.connect();
consumer.setCallback(new LatchedMqttCallback(latch));
consumer.subscribe(TOPIC, 1);
MqttClient producer = createPahoClient("producer");
producer.connect();
producer.publish(TOPIC, "hello".getBytes(), 1, false);
producer.disconnect();
producer.close();
assertTrue(latch.await(1, TimeUnit.SECONDS));
consumer.disconnect();
consumer.close();
}
}

View File

@ -0,0 +1,47 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.jboss.logging.Logger;
import org.junit.Ignore;
/**
* Fulfilled by client or Netty codec (i.e. not tested here):
*
* [MQTT-4.6.0-1] When the Client re-sends any PUBLISH packets, it MUST re-send them in the order in which the original PUBLISH packets were sent (this applies to QoS 1 and QoS 2 messages).
* [MQTT-4.6.0-2] The Client MUST send PUBACK packets in the order in which the corresponding PUBLISH packets were received (QoS 1 messages).
* [MQTT-4.6.0-3] The Client MUST send PUBREC packets in the order in which the corresponding PUBLISH packets were received (QoS 2 messages).
* [MQTT-4.6.0-4] The Client MUST send PUBREL packets in the order in which the corresponding PUBREC packets were received (QoS 2 messages).
*
*
* Message order is one of the fundamental semantics of a queue and since subscriptions are queues the messages are therefore ordered implicitly so I'm not testing these here.
*
* [MQTT-4.6.0-5] When a Server processes a message that has been published to an Ordered Topic, it MUST send PUBLISH packets to consumers (for the same Topic and QoS) in the order that they were received from any given Client.
* [MQTT-4.6.0-6] A Server MUST treat every, Topic as an Ordered Topic when it is forwarding messages on Nonshared Subscriptions.
*/
@Ignore
public class MessageOrderingTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(MessageOrderingTests.class);
public MessageOrderingTests(String protocol) {
super(protocol);
}
}

View File

@ -0,0 +1,93 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.apache.activemq.artemis.tests.util.RandomUtil;
import org.apache.activemq.artemis.tests.util.Wait;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.eclipse.paho.mqttv5.common.MqttMessage;
import org.jboss.logging.Logger;
import org.junit.Test;
/**
* Fulfilled by client or Netty codec (i.e. not tested here):
*
* [MQTT-4.5.0-2] The Client MUST acknowledge any Publish packet it receives according to the applicable QoS rules regardless of whether it elects to process the Application Message that it contains.
*/
public class MessageReceiptTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(MessageReceiptTests.class);
public MessageReceiptTests(String protocol) {
super(protocol);
}
/*
* [MQTT-4.5.0-1] When a Server takes ownership of an incoming Application Message it MUST add it to the Session
* State for those Clients that have matching Subscriptions.
*
* The spec here is speaking in terms of an implementation detail. To be clear, messages are not added "to the
* Session State" specifically. They are added to the queue(s) associated with the matching client subscriptions.
*
* This test is pretty generic. It just creates a bunch of individual consumers and sends a message to each one.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testMessageReceipt() throws Exception {
final String TOPIC = RandomUtil.randomString();
final int CONSUMER_COUNT = 25;
final MqttClient[] consumers = new MqttClient[CONSUMER_COUNT];
final CountDownLatch latch = new CountDownLatch(CONSUMER_COUNT);
for (int i = 0; i < CONSUMER_COUNT; i++) {
MqttClient consumer = createPahoClient(RandomUtil.randomString());
consumers[i] = consumer;
consumer.connect();
int finalI = i;
consumer.setCallback(new DefaultMqttCallback() {
@Override
public void messageArrived(String incomingTopic, MqttMessage message) throws Exception {
System.out.println("=== Message: " + message + " from: " + incomingTopic);
assertEquals(TOPIC + finalI, incomingTopic);
assertEquals("hello" + finalI, new String(message.getPayload()));
latch.countDown();
}
});
consumer.subscribe(TOPIC + i, 0);
}
MqttClient producer = createPahoClient("producer");
producer.connect();
for (int i = 0; i < CONSUMER_COUNT; i++) {
producer.publish(TOPIC + i, ("hello" + i).getBytes(), 0, false);
}
Wait.assertEquals((long) CONSUMER_COUNT, () -> server.getActiveMQServerControl().getTotalMessagesAdded(), 2000, 100);
producer.disconnect();
producer.close();
assertTrue(latch.await(30, TimeUnit.SECONDS));
for (int i = 0; i < CONSUMER_COUNT; i++) {
consumers[i].disconnect();
consumers[i].close();
}
}
}

View File

@ -0,0 +1,38 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.jboss.logging.Logger;
import org.junit.Ignore;
/**
* Fulfilled by client or Netty codec (i.e. not tested explicitly here, but tested implicitly through all the tests using TCP):
*
* [MQTT-4.2.0-1] A Client or Server MUST support the use of one or more underlying transport protocols that provide an ordered, lossless, stream of bytes from the Client to Server and Server to Client.
*/
@Ignore
public class NetworkConnectionTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(NetworkConnectionTests.class);
public NetworkConnectionTests(String protocol) {
super(protocol);
}
}

View File

@ -0,0 +1,616 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import io.netty.handler.codec.mqtt.MqttMessageType;
import io.netty.handler.codec.mqtt.MqttPubAckMessage;
import io.netty.handler.codec.mqtt.MqttPubReplyMessageVariableHeader;
import io.netty.handler.codec.mqtt.MqttPublishMessage;
import org.apache.activemq.artemis.api.core.QueueConfiguration;
import org.apache.activemq.artemis.api.core.RoutingType;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTInterceptor;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTReasonCodes;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTUtil;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.apache.activemq.artemis.tests.util.RandomUtil;
import org.apache.activemq.artemis.tests.util.Wait;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.eclipse.paho.mqttv5.common.MqttMessage;
import org.eclipse.paho.mqttv5.common.packet.MqttProperties;
import org.jboss.logging.Logger;
import org.junit.Test;
/**
* Fulfilled by client or Netty codec (i.e. not tested here):
*
* [MQTT-4.3.1-1] In the QoS 0 delivery protocol, the sender MUST send a PUBLISH packet with QoS 0 and DUP flag set to 0.
* [MQTT-4.3.2-1] In the QoS 1 delivery protocol, the sender MUST assign an unused Packet Identifier each time it has a new Application Message to publish.
* [MQTT-4.3.3-1] In the QoS 2 delivery protocol, the sender MUST assign an unused Packet Identifier when it has a new Application Message to publish.
* [MQTT-4.3.3-2] In the QoS 2 delivery protocol, the sender MUST send a PUBLISH packet containing this Packet Identifier with QoS 2 and DUP flag set to 0.
*
*
* Unsure how to test:
*
* [MQTT-4.3.2-5] In the QoS 1 delivery protocol, the receiver after it has sent a PUBACK packet the receiver MUST treat any incoming PUBLISH packet that contains the same Packet Identifier as being a new Application Message, irrespective of the setting of its DUP flag.
* [MQTT-4.3.3-6] In the QoS 2 delivery protocol, the sender MUST NOT re-send the PUBLISH once it has sent the corresponding PUBREL packet.
* [MQTT-4.3.3-9] In the QoS 2 delivery protocol, the receiver if it has sent a PUBREC with a Reason Code of 0x80 or greater, the receiver MUST treat any subsequent PUBLISH packet that contains that Packet Identifier as being a new Application Message.
* [MQTT-4.3.3-10] In the QoS 2 delivery protocol, the receiver until it has received the corresponding PUBREL packet, the receiver MUST acknowledge any subsequent PUBLISH packet with the same Packet Identifier by sending a PUBREC. It MUST NOT cause duplicate messages to be delivered to any onward recipients in this case.
* [MQTT-4.3.3-12] In the QoS 2 delivery protocol, the receiver After it has sent a PUBCOMP, the receiver MUST treat any subsequent PUBLISH packet that contains that Packet Identifier as being a new Application Message.
*/
public class QoSTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(QoSTests.class);
public QoSTests(String protocol) {
super(protocol);
}
/*
* [MQTT-4.3.2-2] In the QoS 1 delivery protocol, the sender MUST send a PUBLISH packet containing this Packet
* Identifier with QoS 1 and DUP flag set to 0.
*
* This test looks at the PUBLISH packet coming from *the broker* to the client
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testQoS1andDupFlag() throws Exception {
final String TOPIC = RandomUtil.randomString();
final CountDownLatch latch = new CountDownLatch(1);
MqttClient consumer = createPahoClient("consumer");
consumer.connect();
consumer.setCallback(new DefaultMqttCallback() {
@Override
public void messageArrived(String incomingTopic, MqttMessage message) throws Exception {
assertEquals(1, message.getQos());
assertFalse(message.isDuplicate());
latch.countDown();
}
});
consumer.subscribe(TOPIC, 1);
MqttClient producer = createPahoClient("producer");
producer.connect();
producer.publish(TOPIC, RandomUtil.randomString().getBytes(), 1, false);
producer.disconnect();
producer.close();
assertTrue(latch.await(2, TimeUnit.SECONDS));
consumer.disconnect();
consumer.close();
}
/*
* [MQTT-4.3.2-3] In the QoS 1 delivery protocol, the sender MUST treat the PUBLISH packet as unacknowledged until
* it has received the corresponding PUBACK packet from the receiver.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testQoS1PubAck() throws Exception {
final String TOPIC = RandomUtil.randomString();
final CountDownLatch ackLatch = new CountDownLatch(1);
final AtomicInteger packetId = new AtomicInteger();
MQTTInterceptor incomingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBACK) {
// ensure the message is still in the queue before we get the ack from the client
assertEquals(1, getSubscriptionQueue(TOPIC).getMessageCount());
assertEquals(1, getSubscriptionQueue(TOPIC).getDeliveringCount());
// ensure the ids match so we know this is the "corresponding" PUBACK for the previous PUBLISH
assertEquals(packetId.get(), ((MqttPubReplyMessageVariableHeader)packet.variableHeader()).messageId());
ackLatch.countDown();
}
return true;
};
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBLISH) {
packetId.set(((MqttPublishMessage)packet).variableHeader().packetId());
}
return true;
};
server.getRemotingService().addIncomingInterceptor(incomingInterceptor);
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
final CountDownLatch latch = new CountDownLatch(1);
MqttClient consumer = createPahoClient("consumer");
consumer.connect();
consumer.setCallback(new LatchedMqttCallback(latch));
consumer.subscribe(TOPIC, 1);
MqttClient producer = createPahoClient("producer");
producer.connect();
producer.publish(TOPIC, RandomUtil.randomString().getBytes(), 1, false);
producer.disconnect();
producer.close();
assertTrue(ackLatch.await(2, TimeUnit.SECONDS));
assertTrue(latch.await(2, TimeUnit.SECONDS));
assertEquals(0, getSubscriptionQueue(TOPIC).getMessageCount());
assertEquals(0, getSubscriptionQueue(TOPIC).getDeliveringCount());
consumer.disconnect();
consumer.close();
}
/*
* [MQTT-4.3.2-4] In the QoS 1 delivery protocol, the receiver MUST respond with a PUBACK packet containing the
* Packet Identifier from the incoming PUBLISH packet, having accepted ownership of the Application Message.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testQoS1PubAckId() throws Exception {
final String TOPIC = RandomUtil.randomString();
final CountDownLatch ackLatch = new CountDownLatch(1);
final AtomicInteger packetId = new AtomicInteger();
MQTTInterceptor incomingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBLISH) {
packetId.set(((MqttPublishMessage)packet).variableHeader().packetId());
}
return true;
};
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBACK) {
assertEquals(packetId.get(), ((MqttPubAckMessage)packet).variableHeader().messageId());
ackLatch.countDown();
}
return true;
};
server.getRemotingService().addIncomingInterceptor(incomingInterceptor);
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
final CountDownLatch latch = new CountDownLatch(1);
MqttClient consumer = createPahoClient("consumer");
consumer.connect();
consumer.setCallback(new LatchedMqttCallback(latch));
consumer.subscribe(TOPIC, 1);
MqttClient producer = createPahoClient("producer");
producer.connect();
producer.publish(TOPIC, RandomUtil.randomString().getBytes(), 1, false);
producer.disconnect();
producer.close();
assertTrue(ackLatch.await(2, TimeUnit.SECONDS));
assertTrue(latch.await(2, TimeUnit.SECONDS));
consumer.disconnect();
consumer.close();
}
/*
* QoS 2 exactly-once delivery semantics. This diagram was adapted from:
* https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901237
*
* ================================================================================
* | Sender Action | MQTT Control Packet | Receiver Action |
* |-------------------------|--------------------------|-------------------------|
* | Store Message | | |
* --------------------------------------------------------------------------------
* | PUBLISH QoS 2, DUP=0 | | |
* | <Packet ID> | | |
* |-------------------------|--------------------------|-------------------------|
* | | =====> | |
* |-------------------------|--------------------------|-------------------------|
* | | |Store <Packet ID> then |
* | | |initiate onward delivery |
* | | |of the Application |
* | | |Message |
* |-------------------------|--------------------------|-------------------------|
* | | |PUBREC <Packet ID> |
* | | |<Reason Code> |
* |-------------------------|--------------------------|-------------------------|
* | | <===== | |
* |-------------------------|--------------------------|-------------------------|
* | Discard message, store | | |
* | PUBREC <Packet ID> | | |
* |-------------------------|--------------------------|-------------------------|
* | PUBREL <Packet ID> | | |
* |-------------------------|--------------------------|-------------------------|
* | | =====> | |
* |-------------------------|--------------------------|-------------------------|
* | | |Discard <Packet ID> |
* |-------------------------|--------------------------|-------------------------|
* | | |Send PUBCOMP <Packet ID> |
* |-------------------------|--------------------------|-------------------------|
* | | <===== | |
* |-------------------------|--------------------------|-------------------------|
* | Discard stored state | | |
* ================================================================================
*/
/*
* [MQTT-4.3.3-3] In the QoS 2 delivery protocol, the sender MUST treat the PUBLISH packet as unacknowledged until
* it has received the corresponding PUBREC packet from the receiver.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testQoS2PubRec() throws Exception {
final String TOPIC = RandomUtil.randomString();
final CountDownLatch ackLatch = new CountDownLatch(1);
final AtomicInteger packetId = new AtomicInteger();
MQTTInterceptor incomingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBREC) {
// ensure the message is still in the queue before we get the ack from the client
assertEquals(1, getSubscriptionQueue(TOPIC).getMessageCount());
assertEquals(1, getSubscriptionQueue(TOPIC).getDeliveringCount());
// ensure the ids match so we know this is the "corresponding" PUBREC for the previous PUBLISH
assertEquals(packetId.get(), ((MqttPubReplyMessageVariableHeader)packet.variableHeader()).messageId());
ackLatch.countDown();
}
return true;
};
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBLISH) {
packetId.set(((MqttPublishMessage)packet).variableHeader().packetId());
}
return true;
};
server.getRemotingService().addIncomingInterceptor(incomingInterceptor);
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
final CountDownLatch latch = new CountDownLatch(1);
MqttClient consumer = createPahoClient("consumer");
consumer.connect();
consumer.setCallback(new LatchedMqttCallback(latch));
consumer.subscribe(TOPIC, 2);
MqttClient producer = createPahoClient("producer");
producer.connect();
producer.publish(TOPIC, RandomUtil.randomString().getBytes(), 2, false);
producer.disconnect();
producer.close();
assertTrue(ackLatch.await(2, TimeUnit.SECONDS));
assertTrue(latch.await(2, TimeUnit.SECONDS));
assertEquals(0, getSubscriptionQueue(TOPIC).getMessageCount());
assertEquals(0, getSubscriptionQueue(TOPIC).getDeliveringCount());
consumer.disconnect();
consumer.close();
}
/*
* [MQTT-4.3.3-4] In the QoS 2 delivery protocol, the sender MUST send a PUBREL packet when it receives a PUBREC
* packet from the receiver with a Reason Code value less than 0x80. This PUBREL packet MUST contain the same Packet
* Identifier as the original PUBLISH packet.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testQoS2PubRelId() throws Exception {
final String TOPIC = RandomUtil.randomString();
final CountDownLatch ackLatch = new CountDownLatch(1);
final AtomicInteger packetId = new AtomicInteger();
final AtomicBoolean pubRecReceived = new AtomicBoolean(false);
MQTTInterceptor incomingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBLISH) {
packetId.set(((MqttPublishMessage)packet).variableHeader().packetId());
}
if (packet.fixedHeader().messageType() == MqttMessageType.PUBREC) {
assertEquals(packetId.get(), ((MqttPubReplyMessageVariableHeader)packet.variableHeader()).messageId());
assertEquals(MQTTReasonCodes.SUCCESS, ((MqttPubReplyMessageVariableHeader)packet.variableHeader()).reasonCode());
pubRecReceived.set(true);
}
return true;
};
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBREL) {
assertTrue(pubRecReceived.get());
assertEquals(packetId.get(), ((MqttPubAckMessage)packet).variableHeader().messageId());
ackLatch.countDown();
}
return true;
};
server.getRemotingService().addIncomingInterceptor(incomingInterceptor);
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
final CountDownLatch latch = new CountDownLatch(1);
MqttClient consumer = createPahoClient("consumer");
consumer.connect();
consumer.setCallback(new LatchedMqttCallback(latch));
consumer.subscribe(TOPIC, 2);
MqttClient producer = createPahoClient("producer");
producer.connect();
producer.publish(TOPIC, RandomUtil.randomString().getBytes(), 2, false);
producer.disconnect();
producer.close();
assertTrue(ackLatch.await(2, TimeUnit.SECONDS));
assertTrue(latch.await(2, TimeUnit.SECONDS));
consumer.disconnect();
consumer.close();
}
/*
* [MQTT-4.3.3-5] In the QoS 2 delivery protocol, the sender MUST treat the PUBREL packet as unacknowledged until
* it has received the corresponding PUBCOMP packet from the receiver.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testQoS2PubRel() throws Exception {
final String TOPIC = RandomUtil.randomString();
final String CONSUMER_CLIENT_ID = "consumer";
final CountDownLatch ackLatch = new CountDownLatch(1);
final AtomicInteger packetId = new AtomicInteger();
MQTTInterceptor incomingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBCOMP) {
try {
// ensure the message is still in the management queue before we get the PUBCOMP from the client
Wait.assertEquals(1L, () -> server.locateQueue(MQTTUtil.MANAGEMENT_QUEUE_PREFIX + CONSUMER_CLIENT_ID).getMessageCount(), 2000, 100);
Wait.assertEquals(1L, () -> server.locateQueue(MQTTUtil.MANAGEMENT_QUEUE_PREFIX + CONSUMER_CLIENT_ID).getDeliveringCount(), 2000, 100);
} catch (Exception e) {
return false;
}
// ensure the ids match so we know this is the "corresponding" PUBCOMP for the previous PUBLISH
assertEquals(packetId.get(), ((MqttPubReplyMessageVariableHeader)packet.variableHeader()).messageId());
ackLatch.countDown();
}
return true;
};
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBLISH) {
packetId.set(((MqttPublishMessage)packet).variableHeader().packetId());
}
return true;
};
server.getRemotingService().addIncomingInterceptor(incomingInterceptor);
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
final CountDownLatch latch = new CountDownLatch(1);
MqttClient consumer = createPahoClient(CONSUMER_CLIENT_ID);
consumer.connect();
consumer.setCallback(new LatchedMqttCallback(latch));
consumer.subscribe(TOPIC, 2);
MqttClient producer = createPahoClient("producer");
producer.connect();
producer.publish(TOPIC, RandomUtil.randomString().getBytes(), 2, false);
producer.disconnect();
producer.close();
assertTrue(ackLatch.await(2, TimeUnit.SECONDS));
assertTrue(latch.await(2, TimeUnit.SECONDS));
assertEquals(0, getSubscriptionQueue(TOPIC).getMessageCount());
assertEquals(0, getSubscriptionQueue(TOPIC).getDeliveringCount());
consumer.disconnect();
consumer.close();
}
/*
* [MQTT-4.3.3-7] In the QoS 2 delivery protocol, the sender MUST NOT apply Application Message expiry if a PUBLISH
* packet has been sent.
*
* [MQTT-4.3.3-13] In the QoS 2 delivery protocol, the receiver MUST continue the QoS 2 acknowledgement sequence even if it has applied Application Message expiry.
*
* Due to the nature of the underlying queue semantics once a message is "in delivery" it's no longer available for
* expiration. This test demonstrates that.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testQoS2WithExpiration() throws Exception {
final String TOPIC = "myTopic";
final CountDownLatch ackLatch = new CountDownLatch(1);
final CountDownLatch expireRefsLatch = new CountDownLatch(1);
final long messageExpiryInterval = 2;
MQTTInterceptor incomingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBREC) {
// ensure the message is still in the queue before we get the PUBREC from the client
assertEquals(1, getSubscriptionQueue(TOPIC).getMessageCount());
assertEquals(1, getSubscriptionQueue(TOPIC).getDeliveringCount());
try {
// ensure enough time has passed for the message to expire
Thread.sleep(messageExpiryInterval * 1500);
getSubscriptionQueue(TOPIC).expireReferences(expireRefsLatch::countDown);
assertTrue(expireRefsLatch.await(2, TimeUnit.SECONDS));
} catch (InterruptedException e) {
e.printStackTrace();
fail();
}
ackLatch.countDown();
}
return true;
};
server.getRemotingService().addIncomingInterceptor(incomingInterceptor);
final CountDownLatch latch = new CountDownLatch(1);
MqttClient consumer = createPahoClient("consumer");
consumer.connect();
consumer.setCallback(new DefaultMqttCallback() {
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
latch.countDown();
}
});
consumer.subscribe(TOPIC, 2);
MqttClient producer = createPahoClient("producer");
producer.connect();
MqttMessage m = new MqttMessage();
MqttProperties props = new MqttProperties();
props.setMessageExpiryInterval(messageExpiryInterval);
m.setProperties(props);
m.setQos(2);
m.setPayload("foo".getBytes(StandardCharsets.UTF_8));
producer.publish(TOPIC, m);
producer.disconnect();
producer.close();
assertTrue(ackLatch.await(messageExpiryInterval * 2, TimeUnit.SECONDS));
assertTrue(latch.await(messageExpiryInterval * 2, TimeUnit.SECONDS));
Wait.assertEquals(0, () -> getSubscriptionQueue(TOPIC).getMessageCount());
Wait.assertEquals(0, () -> getSubscriptionQueue(TOPIC).getDeliveringCount());
Wait.assertEquals(0, () -> getSubscriptionQueue(TOPIC).getMessagesExpired());
consumer.disconnect();
consumer.close();
}
/*
* [MQTT-4.3.3-8] In the QoS 2 delivery protocol, the receiver MUST respond with a PUBREC containing the Packet
* Identifier from the incoming PUBLISH packet, having accepted ownership of the Application Message.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testQoS2PubRecId() throws Exception {
final String TOPIC = RandomUtil.randomString();
final CountDownLatch ackLatch = new CountDownLatch(1);
final AtomicInteger packetId = new AtomicInteger();
MQTTInterceptor incomingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBLISH) {
packetId.set(((MqttPublishMessage)packet).variableHeader().packetId());
}
return true;
};
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBREC) {
assertEquals(packetId.get(), ((MqttPubAckMessage)packet).variableHeader().messageId());
ackLatch.countDown();
}
return true;
};
server.getRemotingService().addIncomingInterceptor(incomingInterceptor);
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
final CountDownLatch latch = new CountDownLatch(1);
MqttClient consumer = createPahoClient("consumer");
consumer.connect();
consumer.setCallback(new LatchedMqttCallback(latch));
consumer.subscribe(TOPIC, 2);
MqttClient producer = createPahoClient("producer");
producer.connect();
producer.publish(TOPIC, RandomUtil.randomString().getBytes(), 2, false);
producer.disconnect();
producer.close();
assertTrue(ackLatch.await(2, TimeUnit.SECONDS));
assertTrue(latch.await(2, TimeUnit.SECONDS));
consumer.disconnect();
consumer.close();
}
/*
* [MQTT-4.3.3-11] In the QoS 2 delivery protocol, the receiver MUST respond to a PUBREL packet by sending a PUBCOMP
* packet containing the same Packet Identifier as the PUBREL.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testQoS2PubCompId() throws Exception {
final String TOPIC = RandomUtil.randomString();
final CountDownLatch ackLatch = new CountDownLatch(1);
final AtomicInteger packetId = new AtomicInteger();
MQTTInterceptor incomingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBREL) {
packetId.set(((MqttPubReplyMessageVariableHeader)packet.variableHeader()).messageId());
}
return true;
};
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBCOMP) {
assertEquals(packetId.get(), ((MqttPubAckMessage)packet).variableHeader().messageId());
ackLatch.countDown();
}
return true;
};
server.getRemotingService().addIncomingInterceptor(incomingInterceptor);
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
final CountDownLatch latch = new CountDownLatch(1);
MqttClient consumer = createPahoClient("consumer");
consumer.connect();
consumer.setCallback(new LatchedMqttCallback(latch));
consumer.subscribe(TOPIC, 2);
MqttClient producer = createPahoClient("producer");
producer.connect();
producer.publish(TOPIC, RandomUtil.randomString().getBytes(), 2, false);
producer.disconnect();
producer.close();
assertTrue(ackLatch.await(2, TimeUnit.SECONDS));
assertTrue(latch.await(2, TimeUnit.SECONDS));
consumer.disconnect();
consumer.close();
}
/*
* [MQTT-4.3.3-13] In the QoS 2 delivery protocol, the receiver MUST continue the QoS 2 acknowledgement sequence even
* if it has applied Application Message expiry.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testQoS2WithExpiration2() throws Exception {
final String TOPIC = "myTopic";
server.createQueue(new QueueConfiguration(RandomUtil.randomString()).setAddress(TOPIC).setRoutingType(RoutingType.MULTICAST));
final CountDownLatch ackLatch = new CountDownLatch(1);
final CountDownLatch expireRefsLatch = new CountDownLatch(1);
final long messageExpiryInterval = 1;
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBREC) {
// ensure the message is in the queue before trying to expire
Wait.assertTrue(() -> getSubscriptionQueue(TOPIC).getMessageCount() == 1, 2000, 100);
try {
// ensure enough time has passed for the message to expire
Thread.sleep(messageExpiryInterval * 1500);
getSubscriptionQueue(TOPIC).expireReferences(expireRefsLatch::countDown);
assertTrue(expireRefsLatch.await(2, TimeUnit.SECONDS));
} catch (InterruptedException e) {
e.printStackTrace();
fail();
}
ackLatch.countDown();
}
return true;
};
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
MqttClient producer = createPahoClient("producer");
producer.connect();
MqttMessage m = new MqttMessage();
MqttProperties props = new MqttProperties();
props.setMessageExpiryInterval(messageExpiryInterval);
m.setProperties(props);
m.setQos(2);
m.setPayload("foo".getBytes(StandardCharsets.UTF_8));
producer.publish(TOPIC, m);
producer.disconnect();
producer.close();
assertTrue(ackLatch.await(messageExpiryInterval * 2, TimeUnit.SECONDS));
Wait.assertEquals(1, () -> getSubscriptionQueue(TOPIC).getMessagesExpired());
}
}

View File

@ -0,0 +1,38 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.jboss.logging.Logger;
import org.junit.Ignore;
/**
* Unsure how to test this as it's a negative. Many other tests exercise this implicitly:
*
* [MQTT-4.1.0-1] The Client and Server MUST NOT discard the Session State while the Network Connection is open.
*/
@Ignore
public class SessionStateTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(SessionStateTests.class);
public SessionStateTests(String protocol) {
super(protocol);
}
}

View File

@ -0,0 +1,155 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTUtil;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.apache.activemq.artemis.utils.Wait;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.eclipse.paho.mqttv5.common.MqttMessage;
import org.jboss.logging.Logger;
import org.junit.Test;
/**
* Fulfilled by client or Netty codec (i.e. not tested here):
*
* [MQTT-4.8.2-1] A Shared Subscription's Topic Filter MUST start with $share/ and MUST contain a ShareName that is at least one character long.
* [MQTT-4.8.2-2] The ShareName MUST NOT contain the characters "/", "+" or "#", but MUST be followed by a "/" character. This "/" character MUST be followed by a Topic Filter.
*
*
* These requirements are related to shared subscriptions and consumption via QoS 2. These are not tested:
*
* [MQTT-4.8.2-4] The Server MUST complete the delivery of the message to that Client when it reconnects.
* [MQTT-4.8.2-5] If the Client's Session terminates before the Client reconnects, the Server MUST NOT send the Application Message to any other subscribed Client.
*/
public class SubscriptionTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(SubscriptionTests.class);
public SubscriptionTests(String protocol) {
super(protocol);
}
/*
* [MQTT-4.8.2-3] The Server MUST respect the granted QoS for the Client's subscription.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testSharedSubscriptionRespectQoS() throws Exception {
final String TOPIC = "myTopic";
final String SUB_NAME = "myShare";
final String SHARED_SUB = MQTTUtil.SHARED_SUBSCRIPTION_PREFIX + SUB_NAME + "/" + TOPIC;
final int MESSAGE_COUNT = 100;
final AtomicInteger consumer1MessagesReceived = new AtomicInteger(0);
final AtomicInteger consumer2MessagesReceived = new AtomicInteger(0);
MqttClient consumer1 = createPahoClient("consumer1");
consumer1.connect();
consumer1.setCallback(new DefaultMqttCallback() {
@Override
public void messageArrived(String incomingTopic, MqttMessage message) throws Exception {
if (message.getQos() == 0) {
consumer1MessagesReceived.incrementAndGet();
} else {
fail("Wrong QoS for consumer 1: " + message.getId() + " " + message.getQos());
}
}
});
consumer1.subscribe(SHARED_SUB, 0);
assertNotNull(server.locateQueue(SUB_NAME));
assertEquals(TOPIC, server.locateQueue(SUB_NAME).getAddress().toString());
MqttClient consumer2 = createPahoClient("consumer2");
consumer2.connect();
consumer2.setCallback(new DefaultMqttCallback() {
@Override
public void messageArrived(String incomingTopic, MqttMessage message) throws Exception {
if (message.getQos() == 1) {
consumer2MessagesReceived.incrementAndGet();
} else {
fail("Wrong QoS for consumer 2: " + message.getId() + " " + message.getQos());
}
}
});
consumer2.subscribe(SHARED_SUB, 1);
assertEquals(2, server.locateQueue(SUB_NAME).getConsumerCount());
MqttClient producer = createPahoClient("producer");
producer.connect();
for (int i = 0; i < MESSAGE_COUNT; i++) {
producer.publish(TOPIC, new byte[0], 1, false);
}
producer.disconnect();
producer.close();
Wait.assertTrue(() -> consumer1MessagesReceived.get() > 0, 2000, 100);
Wait.assertTrue(() -> consumer2MessagesReceived.get() > 0, 2000, 100);
Wait.assertEquals(MESSAGE_COUNT, () -> consumer1MessagesReceived.get() + consumer2MessagesReceived.get(), 2000, 100);
consumer1.disconnect();
consumer1.close();
consumer2.disconnect();
consumer2.close();
}
/*
* [MQTT-4.8.2-6] If a Client responds with a PUBACK or PUBREC containing a Reason Code of 0x80 or greater to a
* PUBLISH packet from the Server, the Server MUST discard the Application Message and not attempt to send it to any
* other Subscriber.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testSharedSubscriptionWithAck() throws Exception {
final String TOPIC = "myTopic";
final String SUB_NAME = "myShare";
final String SHARED_SUB = MQTTUtil.SHARED_SUBSCRIPTION_PREFIX + SUB_NAME + "/" + TOPIC;
CountDownLatch ackLatch = new CountDownLatch(1);
CountDownLatch negativeAckLatch = new CountDownLatch(1);
MqttClient consumer1 = createPahoClient("consumer1");
consumer1.connect();
consumer1.setCallback(new LatchedMqttCallback(ackLatch));
consumer1.subscribe(SHARED_SUB, 1);
assertNotNull(server.locateQueue(SUB_NAME));
assertEquals(TOPIC, server.locateQueue(SUB_NAME).getAddress().toString());
assertEquals(1, server.locateQueue(SUB_NAME).getConsumerCount());
MqttClient producer = createPahoClient("producer");
producer.connect();
producer.publish(TOPIC, new byte[0], 1, false);
producer.disconnect();
producer.close();
MqttClient consumer2 = createPahoClient("consumer2");
consumer2.connect();
consumer2.setCallback(new LatchedMqttCallback(negativeAckLatch));
consumer2.subscribe(SHARED_SUB, 1);
assertTrue(ackLatch.await(2, TimeUnit.SECONDS));
assertFalse(negativeAckLatch.await(2, TimeUnit.SECONDS));
consumer1.disconnect();
consumer1.close();
consumer2.disconnect();
consumer2.close();
}
}

View File

@ -0,0 +1,100 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec;
import java.util.EnumSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.activemq.artemis.api.core.RoutingType;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.api.core.client.ActiveMQClient;
import org.apache.activemq.artemis.api.core.client.ClientSession;
import org.apache.activemq.artemis.api.core.client.ClientSessionFactory;
import org.apache.activemq.artemis.api.core.client.ServerLocator;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.eclipse.paho.mqttv5.common.MqttMessage;
import org.jboss.logging.Logger;
import org.junit.Test;
/**
* Fulfilled by client or Netty codec (i.e. not tested here):
*
* [MQTT-4.7.0-1] The wildcard characters can be used in Topic Filters, but MUST NOT be used within a Topic Name.
* [MQTT-4.7.1-1] The multi-level wildcard character MUST be specified either on its own or following a topic level separator. In either case it MUST be the last character specified in the Topic Filter.
* [MQTT-4.7.1-2] The single-level wildcard can be used at any level in the Topic Filter, including first and last levels. Where it is used, it MUST occupy an entire level of the filter.
* [MQTT-4.7.3-1] All Topic Names and Topic Filters MUST be at least one character long.
* [MQTT-4.7.3-2] Topic Names and Topic Filters MUST NOT include the null character (Unicode U+0000).
* [MQTT-4.7.3-3] Topic Names and Topic Filters are UTF-8 Encoded Strings; they MUST NOT encode to more than 65,535 bytes.
*
*
* I'm not sure how to test this since it's a negative:
*
* [MQTT-4.7.3-4] When it performs subscription matching the Server MUST NOT perform any normalization of Topic Names or Topic Filters, or any modification or substitution of unrecognized characters.
*/
public class TopicNameAndFilterTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(TopicNameAndFilterTests.class);
public TopicNameAndFilterTests(String protocol) {
super(protocol);
}
/*
* [MQTT-4.7.2-1] The Server MUST NOT match Topic Filters starting with a wildcard character (# or +) with Topic
* Names beginning with a $ character.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testMatchingHash() throws Exception {
testMatchingWildcard("#");
}
/*
* [MQTT-4.7.2-1] The Server MUST NOT match Topic Filters starting with a wildcard character (# or +) with Topic
* Names beginning with a $ character.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testMatchingPlus() throws Exception {
testMatchingWildcard("+");
}
private void testMatchingWildcard(String wildcard) throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
MqttClient consumer = createPahoClient("consumer");
consumer.connect();
consumer.setCallback(new DefaultMqttCallback() {
@Override
public void messageArrived(String incomingTopic, MqttMessage message) throws Exception {
latch.countDown();
}
});
consumer.subscribe(wildcard, 0);
final SimpleString DOLLAR_TOPIC = SimpleString.toSimpleString("$foo");
ServerLocator locator = ActiveMQClient.createServerLocator("vm://0");
ClientSessionFactory csf = locator.createSessionFactory();
ClientSession s = csf.createSession();
s.createAddress(DOLLAR_TOPIC, EnumSet.allOf(RoutingType.class), false);
s.createProducer(DOLLAR_TOPIC).send(s.createMessage(true));
assertFalse(latch.await(2, TimeUnit.SECONDS));
}
}

View File

@ -0,0 +1,50 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.jboss.logging.Logger;
import org.junit.Ignore;
/**
* Fulfilled by client (i.e. not tested here):
*
* [MQTT-6.0.0-1] MQTT Control Packets MUST be sent in WebSocket binary data frames. If any other type of data frame is received the recipient MUST close the Network Connection.
* [MQTT-6.0.0-3] The Client MUST include mqtt in the list of WebSocket Sub Protocols it offers.
*
*
* Fulfilled by the Netty codec on broker (i.e. not explicitly tested here):
*
* [MQTT-6.0.0-2] A single WebSocket data frame can contain multiple or partial MQTT Control Packets. The receiver MUST NOT assume that MQTT Control Packets are aligned on WebSocket frame boundaries.
*
*
* This is tested implicitly as almost all tests are run using both TCP and WebSocket connections. The subprotocol is defined in org.apache.activemq.artemis.core.protocol.mqtt.MQTTProtocolManager#websocketRegistryNames:
*
* [MQTT-6.0.0-4] The WebSocket Subprotocol name selected and returned by the Server MUST be mqtt.
*
*/
@Ignore
public class WebSocketTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(WebSocketTests.class);
public WebSocketTests(String protocol) {
super(protocol);
}
}

View File

@ -0,0 +1,50 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec.controlpackets;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.jboss.logging.Logger;
import org.junit.Ignore;
/**
* Fulfilled by client or Netty codec (i.e. not tested here):
*
* [MQTT-3.15.1-1] Bits 3,2,1 and 0 of the Fixed Header of the AUTH packet are reserved and MUST all be set to 0. The Client or Server MUST treat any other value as malformed and close the Network Connection.
*
*
* The broker doesn't send any "Reason String" or "User Property" in the AUTH packet for any reason. Therefore, these are not tested here:
*
* [MQTT-3.15.2-2] The sender MUST NOT send this property if it would increase the size of the AUTH packet beyond the Maximum Packet Size specified by the receiver
* [MQTT-3.15.2-3] The sender MUST NOT send this property if it would increase the size of the AUTH packet beyond the Maximum Packet Size specified by the receiver.
*
*
* Not implemented.
*
* [MQTT-3.15.2-1] The sender of the AUTH Packet MUST use one of the Authenticate Reason Codes.
*
*/
@Ignore
public class AuthTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(AuthTests.class);
public AuthTests(String protocol) {
this.protocol = protocol;
}
}

View File

@ -0,0 +1,544 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec.controlpackets;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import io.netty.handler.codec.mqtt.MqttConnAckVariableHeader;
import io.netty.handler.codec.mqtt.MqttMessageType;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTInterceptor;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTReasonCodes;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTUtil;
import org.apache.activemq.artemis.core.remoting.server.impl.RemotingServiceImpl;
import org.apache.activemq.artemis.spi.core.protocol.ConnectionEntry;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.apache.activemq.artemis.tests.util.RandomUtil;
import org.apache.activemq.artemis.utils.Wait;
import org.eclipse.paho.mqttv5.client.IMqttToken;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptionsBuilder;
import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse;
import org.eclipse.paho.mqttv5.common.MqttException;
import org.eclipse.paho.mqttv5.common.packet.MqttReturnCode;
import org.jboss.logging.Logger;
import org.junit.Assume;
import org.junit.Ignore;
import org.junit.Test;
/**
* Fulfilled by client or Netty codec (i.e. not tested here):
*
* [MQTT-3.2.2-1] Byte 1 is the "Connect Acknowledge Flags". Bits 7-1 are reserved and MUST be set to 0.
* [MQTT-3.2.2-4] If the Client does not have Session State and receives Session Present set to 1 it MUST close the Network Connection.
* [MQTT-3.2.2-5] If the Client does have Session State and receives Session Present set to 0 it MUST discard its Session State if it continues with the Network Connection.
* [MQTT-3.2.2-11] If a Client receives a Maximum QoS from a Server, it MUST NOT send PUBLISH packets at a QoS level exceeding the Maximum QoS level specified.
* [MQTT-3.2.2-14] A Client receiving Retain Available set to 0 from the Server MUST NOT send a PUBLISH packet with the RETAIN flag set to 1. *
* [MQTT-3.2.2-17] The Client MUST NOT send a Topic Alias in a PUBLISH packet to the Server greater than this value.
*
*
* Unsure how to test as this it's a negative:
*
* [MQTT-3.2.0-2] The Server MUST NOT send more than one CONNACK in a Network Connection.
*
*
* Not tested because the broker supports all QoS values:
*
* [MQTT-3.2.2-9] If a Server does not support QoS 1 or QoS 2 PUBLISH packets it MUST send a Maximum QoS in the CONNACK packet specifying the highest QoS it supports.
* [MQTT-3.2.2-10] A Server that does not support QoS 1 or QoS 2 PUBLISH packets MUST still accept SUBSCRIBE packets containing a Requested QoS of 0, 1 or 2.
* [MQTT-3.2.2-12] If a Server receives a CONNECT packet containing a Will QoS that exceeds its capabilities, it MUST reject the connection. It SHOULD use a CONNACK packet with Reason Code 0x9B (QoS not supported) as described in section 4.13 Handling errors, and MUST close the Network Connection.
*
*
* Not tested because the broker always supports retained messages:
*
* [MQTT-3.2.2-13] If a Server receives a CONNECT packet containing a Will Message with the Will Retain 1, and it does not support retained messages, the Server MUST reject the connection request. It SHOULD send CONNACK with Reason Code 0x9A (Retain not supported) and then it MUST close the Network Connection. *
*
*
* The broker doesn't send any "Reason String" or "User Property" in the CONNACK packet for any reason. Therefore, these are not tested here:
*
* [MQTT-3.2.2-19] The Server MUST NOT send this (i.e. Reason String) property if it would increase the size of the CONNACK packet beyond the Maximum Packet Size specified by the Client.
* [MQTT-3.2.2-20] The Server MUST NOT send this (i.e. User Property) property if it would increase the size of the CONNACK packet beyond the Maximum Packet Size specified by the Client.
*
*/
public class ConnAckTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(ConnAckTests.class);
public ConnAckTests(String protocol) {
this.protocol = protocol;
}
/*
* [MQTT-3.1.3-6] A Server MAY allow a Client to supply a ClientID that has a length of zero bytes, however if it
* does so the Server MUST treat this as a special case and assign a unique ClientID to that Client.
*
* [MQTT-3.1.3-7] It MUST then process the CONNECT packet as if the Client had provided that unique ClientID, and
* MUST return the Assigned Client Identifier in the CONNACK packet.
*
* [MQTT-3.2.2-16] If the Client connects using a zero length Client Identifier, the Server MUST respond with a
* CONNACK containing an Assigned Client Identifier. The Assigned Client Identifier MUST be a new Client Identifier
* not used by any other Session currently in the Server.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testEmptyClientID() throws Exception {
// This is apparently broken with the Paho client + web socket. The broker never even receives a CONNECT packet.
Assume.assumeTrue(protocol.equals(TCP));
// no session should exist
assertEquals(0, getSessionStates().size());
MqttClient client = createPahoClient("");
IMqttToken result = client.connectWithResult(null);
assertFalse(result.getSessionPresent());
String assignedClientID = result.getResponseProperties().getAssignedClientIdentifier();
assertNotNull(assignedClientID);
// session should exist
assertEquals(1, getSessionStates().size());
assertNotNull(getSessionStates().get(assignedClientID));
client.disconnect();
}
/*
* [MQTT-3.1.4-4] The Server MUST perform the processing of Clean Start.
*
* [MQTT-3.2.2-3] If the Server accepts a connection with Clean Start set to 0 and the Server has Session State for
* the ClientID, it MUST set Session Present to 1 in the CONNACK packet, otherwise it MUST set Session Present to 0
* in the CONNACK packet. In both cases it MUST set a 0x00 (Success) Reason Code in the CONNACK packet.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testConnackWhenCleanStartFalse() throws Exception {
final String CONSUMER_ID = RandomUtil.randomString();
// no session should exist
assertEquals(0, getSessionStates().size());
MqttClient consumer = createPahoClient(CONSUMER_ID);
MqttConnectionOptions options = new MqttConnectionOptionsBuilder()
.cleanStart(false)
.sessionExpiryInterval(MQTTUtil.FOUR_BYTE_INT_MAX)
.build();
IMqttToken result = consumer.connectWithResult(options);
assertFalse(result.getSessionPresent());
assertTrue(getListOfCodes(result.getResponse().getReasonCodes()).contains(MqttReturnCode.RETURN_CODE_SUCCESS));
consumer.disconnect();
// session should still exist
assertEquals(1, getSessionStates().size());
assertNotNull(getSessionStates().get(CONSUMER_ID));
result = consumer.connectWithResult(options);
assertTrue(result.getSessionPresent());
assertTrue(getListOfCodes(result.getResponse().getReasonCodes()).contains(MqttReturnCode.RETURN_CODE_SUCCESS));
}
/*
* [MQTT-3.1.4-4] The Server MUST perform the processing of Clean Start.
*
* [MQTT-3.2.2-2] If the Server accepts a connection with Clean Start set to 1, the Server MUST set Session Present
* to 0 in the CONNACK packet in addition to setting a 0x00 (Success) Reason Code in the CONNACK packet.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testConnackWhenCleanStartTrue() throws Exception {
final String CONSUMER_ID = RandomUtil.randomString();
// no session should exist
assertEquals(0, getSessionStates().size());
MqttClient consumer = createPahoClient(CONSUMER_ID);
MqttConnectionOptions options = new MqttConnectionOptionsBuilder()
.cleanStart(true)
.build();
IMqttToken result = consumer.connectWithResult(options);
assertFalse(result.getSessionPresent());
assertTrue(getListOfCodes(result.getResponse().getReasonCodes()).contains(MqttReturnCode.RETURN_CODE_SUCCESS));
consumer.disconnect();
}
/*
* [MQTT-3.1.4-4] The Server MUST perform the processing of Clean Start.
*
* From section 3.1.2.11.2:
*
* If the Session Expiry Interval is absent the value 0 is used. If it is set to 0, or is absent, the Session ends
* when the Network Connection is closed.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testCleanStartFalseWithAbsentSessionExpiryInterval() throws Exception {
final String CONSUMER_ID = RandomUtil.randomString();
final String TOPIC = this.getTopicName();
MqttClient consumer = createPahoClient(CONSUMER_ID);
// do not set the session expiry interval
MqttConnectionOptions options = new MqttConnectionOptionsBuilder()
.cleanStart(false)
.build();
consumer.connect(options);
consumer.subscribe(TOPIC, 2);
consumer.disconnect();
// session should *not* still exist since session expiry interval was absent
assertEquals(0, getSessionStates().size());
assertNull(getSessionStates().get(CONSUMER_ID));
}
/*
* [MQTT-3.1.4-4] The Server MUST perform the processing of Clean Start.
*
* From section 3.1.2.11.2:
*
* If the Session Expiry Interval is 0xFFFFFFFF (UINT_MAX), the Session does not expire.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testCleanStartFalseWithMaxSessionExpiryInterval() throws Exception {
final String CONSUMER_ID = RandomUtil.randomString();
final String TOPIC = this.getTopicName();
final long EXPIRY_INTERVAL = 2000L;
MqttClient consumer = createPahoClient(CONSUMER_ID);
MqttConnectionOptions options = new MqttConnectionOptionsBuilder()
.cleanStart(false)
.sessionExpiryInterval(MQTTUtil.FOUR_BYTE_INT_MAX)
.build();
consumer.connect(options);
consumer.subscribe(TOPIC, 2);
consumer.disconnect();
// session should still exist since session expiry interval used the max value
assertFalse(Wait.waitFor(() -> getSessionStates().size() == 0, EXPIRY_INTERVAL * 2, 100));
assertNotNull(getSessionStates().get(CONSUMER_ID));
}
/*
* [MQTT-3.2.0-1] The Server MUST send a CONNACK with a 0x00 (Success) Reason Code before sending any Packet other
* than AUTH.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testConnackSentFirst() throws Exception {
final String CONSUMER_ID = RandomUtil.randomString();
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean failed = new AtomicBoolean(false);
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.CONNACK) {
latch.countDown();
} else {
failed.set(true);
}
return true;
};
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
MqttClient consumer = createPahoClient(CONSUMER_ID);
consumer.connect();
assertTrue(latch.await(2, TimeUnit.SECONDS));
assertFalse(failed.get());
consumer.disconnect();
}
/*
* [MQTT-3.2.2-6] If a Server sends a CONNACK packet containing a non-zero Reason Code it MUST set Session Present to
* 0.
*
* [MQTT-3.2.2-7] If a Server sends a CONNACK packet containing a Reason code of 0x80 or greater it MUST then close
* the Network Connection.
*
* This test *only* exercises one scenario where a CONNACK packet contains a non-zero Reason Code (i.e. when the
* client ID is invalid)
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testSessionPresentWithNonZeroConnackReasonCode() throws Exception {
// This is apparently broken with the Paho client + web socket. The broker never even receives a CONNECT packet.
Assume.assumeTrue(protocol.equals(TCP));
CountDownLatch latch = new CountDownLatch(1);
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.CONNACK) {
assertFalse(((MqttConnAckVariableHeader)packet.variableHeader()).isSessionPresent());
latch.countDown();
}
return true;
};
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
// no session should exist
assertEquals(0, getSessionStates().size());
MqttClient client = createPahoClient("");
MqttConnectionOptions options = new MqttConnectionOptionsBuilder()
.cleanStart(false)
.build();
try {
client.connect(options);
fail("Should throw exception about invalid client identifier");
} catch (MqttException e) {
assertEquals(MQTTReasonCodes.CLIENT_IDENTIFIER_NOT_VALID, (byte) e.getReasonCode());
}
assertFalse(client.isConnected());
assertEquals(0, getSessionStates().size());
}
/*
* [MQTT-3.2.2-8] The Server sending the CONNACK packet MUST use one of the Connect Reason Code values.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testConnackReasonCode() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.CONNACK) {
assertNotNull(((MqttConnAckVariableHeader)packet.variableHeader()).connectReturnCode());
latch.countDown();
}
return true;
};
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
MqttClient client = createPahoClient(RandomUtil.randomString());
client.connect();
assertTrue(latch.await(2, TimeUnit.SECONDS));
}
/*
* [MQTT-3.2.2-15] The Client MUST NOT send packets exceeding Maximum Packet Size to the Server.
*
* The normal use-case with a postive maximum packet size.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testMaxPacketSize() throws Exception {
final int SIZE = 256;
setAcceptorProperty("maximumPacketSize=" + SIZE);
final String TOPIC = RandomUtil.randomString();
final CountDownLatch latch = new CountDownLatch(1);
StringBuilder builder = new StringBuilder(SIZE * 2);
for (int i = 0; i < SIZE * 2; i++) {
builder.append("=");
}
byte[] bytes = builder.toString().getBytes(StandardCharsets.UTF_8);
MqttClient producer = createPahoClient(RandomUtil.randomString());
producer.connect();
producer.setCallback(new DefaultMqttCallback() {
@Override
public void disconnected(MqttDisconnectResponse disconnectResponse) {
assertEquals(MQTTReasonCodes.PACKET_TOO_LARGE, (byte) disconnectResponse.getReturnCode());
latch.countDown();
}
});
try {
producer.publish(TOPIC, bytes, 1, false);
fail("Publishing should have failed with an MqttException");
} catch (MqttException e) {
// expected
} catch (Exception e) {
fail("Should have thrown an MqttException");
}
assertTrue(latch.await(2, TimeUnit.SECONDS));
assertFalse(producer.isConnected());
}
/*
* [MQTT-3.2.2-15] The Client MUST NOT send packets exceeding Maximum Packet Size to the Server.
*
* Disable maximum packet size on the broker so that it doesn't appear in the CONNACK at all.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testMaxPacketSizeNegativeOne() throws Exception {
final int SIZE = -1;
setAcceptorProperty("maximumPacketSize=" + SIZE);
MqttClient client = createPahoClient(RandomUtil.randomString());
IMqttToken result = client.connectWithResult(null);
assertNotNull(result.getResponseProperties());
assertNull(result.getResponseProperties().getMaximumPacketSize());
}
/*
* From section 3.2.2.3.6:
*
* It is a Protocol Error to include the Maximum Packet Size more than once, or for the value to be set to zero.
*
* I expected the Paho client to validate the returning maximumPacketSize in the CONNACK, but it doesn't. :(
*/
@Ignore
@Test(timeout = DEFAULT_TIMEOUT)
public void testMaxPacketSizeZero() throws Exception {
final int SIZE = 0;
setAcceptorProperty("maximumPacketSize=" + SIZE);
MqttClient producer = createPahoClient(RandomUtil.randomString());
try {
producer.connect();
fail("Connecting should have thrown an exception");
} catch (Exception e) {
// expected
}
Wait.assertFalse(() -> producer.isConnected(), 2000, 100);
}
/*
* [MQTT-3.2.2-18] If Topic Alias Maximum is absent or 0, the Client MUST NOT send any Topic Aliases on to the
* Server.
*
* This doesn't test whether or not the client actually sends topic aliases as that's up to the client
* implementation. This just tests that the expected property value is returned to the client based on the broker's
* setting.
*
* Disable topic alias maximum on the broker so that it is absent from the CONNACK.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testTopicAliasMaxNegativeOne() throws Exception {
final int SIZE = -1;
setAcceptorProperty("topicAliasMaximum=" + SIZE);
MqttClient client = createPahoClient(RandomUtil.randomString());
IMqttToken result = client.connectWithResult(null);
assertNotNull(result.getResponseProperties());
assertNull(result.getResponseProperties().getTopicAliasMaximum());
}
/*
* [MQTT-3.2.2-18] Topic Alias Maximum is absent, the Client MUST NOT send any Topic Aliases on to the Server.
*
* This doesn't test whether or not the client actually sends topic aliases as that's up to the client
* implementation. This just tests that the expected property value is returned to the client based on the broker's
* setting.
*
* Disable topic alias maximum on the broker.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testTopicAliasMaxZero() throws Exception {
final int SIZE = 0;
setAcceptorProperty("topicAliasMaximum=" + SIZE);
MqttClient client = createPahoClient(RandomUtil.randomString());
IMqttToken result = client.connectWithResult(null);
assertNotNull(result.getResponseProperties());
assertEquals(0, result.getResponseProperties().getTopicAliasMaximum().intValue());
}
/*
* [MQTT-3.2.2-21] If the Server sends a Server Keep Alive on the CONNACK packet, the Client MUST use this value
* instead of the Keep Alive value the Client sent on CONNECT.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testServerKeepAlive() throws Exception {
final int SERVER_KEEP_ALIVE = 123;
setAcceptorProperty("serverKeepAlive=" + SERVER_KEEP_ALIVE);
MqttClient client = createPahoClient(RandomUtil.randomString());
MqttConnectionOptions options = new MqttConnectionOptions();
options.setKeepAliveInterval(1234);
IMqttToken result = client.connectWithResult(options);
assertNotNull(result.getResponseProperties());
assertEquals(SERVER_KEEP_ALIVE, (long) result.getResponseProperties().getServerKeepAlive());
client.disconnect();
}
/*
* [MQTT-3.2.2-22] If the Server does not send the Server Keep Alive, the Server MUST use the Keep Alive value set by
* the Client on CONNECT.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testServerKeepAliveNegativeOne() throws Exception {
final int KEEP_ALIVE = 1234;
setAcceptorProperty("serverKeepAlive=-1");
MqttClient client = createPahoClient(RandomUtil.randomString());
MqttConnectionOptions options = new MqttConnectionOptions();
options.setKeepAliveInterval(KEEP_ALIVE);
IMqttToken result = client.connectWithResult(options);
assertNull(result.getResponseProperties().getServerKeepAlive());
boolean found = false;
// make sure the keep-alive set by the client is used by the server (multiplied by 1500 because the client uses milliseconds instead of seconds and the value is modified by 1.5 per the spec)
for (ConnectionEntry entry : ((RemotingServiceImpl)getServer().getRemotingService()).getConnectionEntries()) {
assertEquals(entry.ttl, KEEP_ALIVE * MQTTUtil.KEEP_ALIVE_ADJUSTMENT);
found = true;
}
assertTrue(found);
client.disconnect();
}
/*
* [MQTT-3.2.2-22] If the Server does not send the Server Keep Alive, the Server MUST use the Keep Alive value set by
* the Client on CONNECT.
*
* serverKeepAlive=0 completely disables keep alives no matter the client's keep alive value.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testServerKeepAliveZero() throws Exception {
final int SERVER_KEEP_ALIVE = 0;
setAcceptorProperty("serverKeepAlive=" + SERVER_KEEP_ALIVE);
MqttClient client = createPahoClient(RandomUtil.randomString());
MqttConnectionOptions options = new MqttConnectionOptions();
options.setKeepAliveInterval(1234);
IMqttToken result = client.connectWithResult(options);
assertEquals(SERVER_KEEP_ALIVE, (long) result.getResponseProperties().getServerKeepAlive());
boolean found = false;
for (ConnectionEntry entry : ((RemotingServiceImpl)getServer().getRemotingService()).getConnectionEntries()) {
assertEquals(entry.ttl, -1);
found = true;
}
assertTrue(found);
client.disconnect();
}
/*
* [MQTT-3.2.2-22] If the Server does not send the Server Keep Alive, the Server MUST use the Keep Alive value set by
* the Client on CONNECT.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testServerKeepAliveWithClientKeepAliveZero() throws Exception {
final int SERVER_KEEP_ALIVE = 123;
setAcceptorProperty("serverKeepAlive=" + SERVER_KEEP_ALIVE);
MqttClient client = createPahoClient(RandomUtil.randomString());
MqttConnectionOptions options = new MqttConnectionOptions();
options.setKeepAliveInterval(0);
IMqttToken result = client.connectWithResult(options);
assertEquals(SERVER_KEEP_ALIVE, (long) result.getResponseProperties().getServerKeepAlive());
boolean found = false;
for (ConnectionEntry entry : ((RemotingServiceImpl)getServer().getRemotingService()).getConnectionEntries()) {
assertEquals(entry.ttl, SERVER_KEEP_ALIVE * MQTTUtil.KEEP_ALIVE_ADJUSTMENT);
found = true;
}
assertTrue(found);
client.disconnect();
}
private List<Integer> getListOfCodes(int[] codes) {
return IntStream.of(codes).boxed().collect(Collectors.toList());
}
}

View File

@ -0,0 +1,99 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec.controlpackets;
import java.nio.charset.StandardCharsets;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTReasonCodes;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.apache.activemq.artemis.tests.util.RandomUtil;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptionsBuilder;
import org.eclipse.paho.mqttv5.common.MqttException;
import org.jboss.logging.Logger;
import org.junit.Test;
public class ConnectTestsWithSecurity extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(ConnectTestsWithSecurity.class);
public ConnectTestsWithSecurity(String protocol) {
super(protocol);
}
@Override
public boolean isSecurityEnabled() {
return true;
}
/*
* [MQTT-3.1.4-2] The Server MAY check that the contents of the CONNECT packet meet any further restrictions and
* SHOULD perform authentication and authorization checks. If any of these checks fail, it MUST close the Network
* Connection.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testAuthenticationFailureWithBadCredentials() throws Exception {
testAuthentication(new MqttConnectionOptionsBuilder()
.username(RandomUtil.randomString())
.password(RandomUtil.randomString().getBytes(StandardCharsets.UTF_8))
.build());
}
/*
* [MQTT-3.1.4-2] The Server MAY check that the contents of the CONNECT packet meet any further restrictions and
* SHOULD perform authentication and authorization checks. If any of these checks fail, it MUST close the Network
* Connection.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testAuthenticationFailureWithNoCredentials() throws Exception {
testAuthentication(new MqttConnectionOptionsBuilder().build());
}
private void testAuthentication(MqttConnectionOptions options) throws Exception {
final String CLIENT_ID = RandomUtil.randomString();
MqttClient client = createPahoClient(CLIENT_ID);
try {
client.connect(options);
fail("Connecting should have failed with a security problem");
} catch (MqttException e) {
assertEquals(MQTTReasonCodes.BAD_USER_NAME_OR_PASSWORD, (byte) e.getReasonCode());
} catch (Exception e) {
fail("Should have thrown an MqttException");
}
assertFalse(client.isConnected());
}
@Test(timeout = DEFAULT_TIMEOUT)
public void testAuthenticationSuccess() throws Exception {
final String CLIENT_ID = RandomUtil.randomString();
MqttConnectionOptions options = new MqttConnectionOptionsBuilder()
.username(fullUser)
.password(fullPass.getBytes(StandardCharsets.UTF_8))
.build();
MqttClient client = createPahoClient(CLIENT_ID);
try {
client.connect(options);
} catch (Exception e) {
fail("Should not have thrown an Exception");
}
assertTrue(client.isConnected());
client.disconnect();
}
}

View File

@ -0,0 +1,122 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec.controlpackets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import io.netty.handler.codec.mqtt.MqttMessageType;
import io.netty.handler.codec.mqtt.MqttProperties;
import io.netty.handler.codec.mqtt.MqttReasonCodeAndPropertiesVariableHeader;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTInterceptor;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTReasonCodes;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.apache.activemq.artemis.tests.util.RandomUtil;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptionsBuilder;
import org.eclipse.paho.mqttv5.common.MqttMessage;
import org.jboss.logging.Logger;
import org.junit.Test;
/**
* Fulfilled by client or Netty codec (i.e. not tested here):
*
* [MQTT-3.14.1-1] The Client or Server MUST validate that reserved bits are set to 0. If they are not zero it sends a DISCONNECT packet with a Reason code of 0x81 (Malformed Packet).
* [MQTT-3.14.4-1] After sending a DISCONNECT packet the sender MUST NOT send any more MQTT Control Packets on that Network Connection.
* [MQTT-3.14.4-2] After sending a DISCONNECT packet the sender MUST close the Network Connection.
*
*
* Not sure how to test this since it's a negative:
*
* [MQTT-3.14.0-1] A Server MUST NOT send a DISCONNECT until after it has sent a CONNACK with Reason Code of less than 0x80.
*
*
* The broker doesn't send any "Reason String" or "User Property" in the DISCONNECT packet for any reason. Therefore, these are not tested here:
*
* [MQTT-3.14.2-3] The sender MUST NOT use this Property if it would increase the size of the DISCONNECT packet beyond the Maximum Packet Size specified by the receiver.
* [MQTT-3.14.2-4] The sender MUST NOT send this property if it would increase the size of the DISCONNECT packet beyond the Maximum Packet Size specified by the receiver.
*/
public class DisconnectTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(DisconnectTests.class);
public DisconnectTests(String protocol) {
super(protocol);
}
/*
* [MQTT-3.14.2-1] The Client or Server sending the DISCONNECT packet MUST use one of the DISCONNECT Reason Codes.
*
* [MQTT-3.14.2-2] The Session Expiry Interval MUST NOT be sent on a DISCONNECT by the Server.
*
* Currently the only way to trigger a DISCONNECT from the broker is to "take over" an existing session at which
* point the broker will disconnect the existing session.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testDisconnectReasonCode() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.DISCONNECT) {
System.out.println(packet);
assertEquals(MQTTReasonCodes.SESSION_TAKEN_OVER, ((MqttReasonCodeAndPropertiesVariableHeader)packet.variableHeader()).reasonCode());
assertNull(((MqttReasonCodeAndPropertiesVariableHeader)packet.variableHeader()).properties().getProperty(MqttProperties.MqttPropertyType.SESSION_EXPIRY_INTERVAL.value()));
latch.countDown();
}
return true;
};
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
MqttClient publisher1 = createPahoClient("publisher");
publisher1.connect();
MqttClient publisher2 = createPahoClient("publisher");
publisher2.connect();
assertTrue(latch.await(2, TimeUnit.SECONDS));
publisher2.disconnect();
publisher2.close();
}
/*
* [MQTT-3.14.4-3] On receipt of DISCONNECT with a Reason Code of 0x00 (Success) the Server MUST discard any Will
* Message associated with the current Connection without publishing it.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testWillMessageRemovedOnDisconnect() throws Exception {
final String CLIENT_ID = org.apache.activemq.artemis.tests.util.RandomUtil.randomString();
final byte[] WILL = RandomUtil.randomBytes();
MqttClient client = createPahoClient(CLIENT_ID);
MqttConnectionOptions options = new MqttConnectionOptionsBuilder()
.will("/topic/foo", new MqttMessage(WILL))
.build();
client.connect(options);
assertNotNull(getSessionStates().get(CLIENT_ID).getWillMessage());
client.disconnect();
// normal disconnect removes all session state if session expiration interval is 0
assertNull(getSessionStates().get(CLIENT_ID));
client.close();
}
}

View File

@ -0,0 +1,64 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec.controlpackets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import io.netty.handler.codec.mqtt.MqttMessageType;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTInterceptor;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.apache.activemq.artemis.tests.util.RandomUtil;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
import org.jboss.logging.Logger;
import org.junit.Test;
public class PingReqTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(PingReqTests.class);
public PingReqTests(String protocol) {
super(protocol);
}
/*
* [MQTT-3.12.4-1] The Server MUST send a PINGRESP packet in response to a PINGREQ packet.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testPingResp() throws Exception {
final CountDownLatch latch = new CountDownLatch(4);
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PINGRESP) {
latch.countDown();
}
return true;
};
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
MqttClient client = createPahoClient(RandomUtil.randomString());
MqttConnectionOptions options = new MqttConnectionOptions();
options.setKeepAliveInterval(1);
client.connect(options);
assertTrue(latch.await(5, TimeUnit.SECONDS));
client.disconnect();
}
}

View File

@ -0,0 +1,37 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec.controlpackets;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.jboss.logging.Logger;
import org.junit.Ignore;
/**
* There were no normative statements in the specification for this control packet. See
* https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901200.
*/
@Ignore
public class PingRespTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(PingRespTests.class);
public PingRespTests(String protocol) {
super(protocol);
}
}

View File

@ -0,0 +1,74 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec.controlpackets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import io.netty.handler.codec.mqtt.MqttMessageType;
import io.netty.handler.codec.mqtt.MqttPubReplyMessageVariableHeader;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTInterceptor;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTReasonCodes;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.apache.activemq.artemis.utils.RandomUtil;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.jboss.logging.Logger;
import org.junit.Test;
/**
* The broker doesn't send any "Reason String" or "User Property" in the PUBACK packet for any reason. Therefore, these are not tested here:
*
* [MQTT-3.4.2-2] The sender MUST NOT send this property if it would increase the size of the PUBACK packet beyond the Maximum Packet Size specified by the receiver.
* [MQTT-3.4.2-3] The sender MUST NOT send this property if it would increase the size of the PUBACK packet beyond the Maximum Packet Size specified by the receiver.
*/
public class PubAckTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(PubAckTests.class);
public PubAckTests(String protocol) {
super(protocol);
}
/*
* [MQTT-3.4.2-1] The Client or Server sending the PUBACK packet MUST use one of the PUBACK Reason Codes.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testPubAckReasonCode() throws Exception {
final String TOPIC = RandomUtil.randomString();
final CountDownLatch latch = new CountDownLatch(1);
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBACK) {
assertEquals(MQTTReasonCodes.SUCCESS, ((MqttPubReplyMessageVariableHeader)packet.variableHeader()).reasonCode());
latch.countDown();
}
return true;
};
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
MqttClient publisher = createPahoClient("publisher");
publisher.connect();
publisher.publish(TOPIC, new byte[0], 1, false);
assertTrue(latch.await(2, TimeUnit.SECONDS));
publisher.disconnect();
publisher.close();
}
}

View File

@ -0,0 +1,74 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec.controlpackets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import io.netty.handler.codec.mqtt.MqttMessageType;
import io.netty.handler.codec.mqtt.MqttPubReplyMessageVariableHeader;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTInterceptor;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTReasonCodes;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.apache.activemq.artemis.utils.RandomUtil;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.jboss.logging.Logger;
import org.junit.Test;
/**
* The broker doesn't send any "Reason String" or "User Property" in the PUBCOMP packet for any reason. Therefore, these are not tested here:
*
* [MQTT-3.7.2-2] The sender MUST NOT use this Property if it would increase the size of the PUBCOMP packet beyond the Maximum Packet Size specified by the receiver.
* [MQTT-3.7.2-3] The sender MUST NOT send this property if it would increase the size of the PUBCOMP packet beyond the Maximum Packet Size specified by receiver.
*/
public class PubCompTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(PubCompTests.class);
public PubCompTests(String protocol) {
super(protocol);
}
/*
* [MQTT-3.7.2-1] The Client or Server sending the PUBCOMP packets MUST use one of the PUBCOMP Reason Codes.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testPubCompReasonCode() throws Exception {
final String TOPIC = RandomUtil.randomString();
final CountDownLatch latch = new CountDownLatch(1);
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBCOMP) {
assertEquals(MQTTReasonCodes.SUCCESS, ((MqttPubReplyMessageVariableHeader)packet.variableHeader()).reasonCode());
latch.countDown();
}
return true;
};
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
MqttClient publisher = createPahoClient("publisher");
publisher.connect();
publisher.publish(TOPIC, new byte[0], 2, false);
assertTrue(latch.await(2, TimeUnit.SECONDS));
publisher.disconnect();
publisher.close();
}
}

View File

@ -0,0 +1,74 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec.controlpackets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import io.netty.handler.codec.mqtt.MqttMessageType;
import io.netty.handler.codec.mqtt.MqttPubReplyMessageVariableHeader;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTInterceptor;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTReasonCodes;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.apache.activemq.artemis.utils.RandomUtil;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.jboss.logging.Logger;
import org.junit.Test;
/**
* The broker doesn't send any "Reason String" or "User Property" in the PUBREL packet for any reason. Therefore, these are not tested here:
*
* [MQTT-3.5.2-2] The sender MUST NOT send this property if it would increase the size of the PUBREC packet beyond the Maximum Packet Size specified by the receiver.
* [MQTT-3.5.2-3] The sender MUST NOT send this property if it would increase the size of the PUBREC packet beyond the Maximum Packet Size specified by the receiver.
*/
public class PubRecTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(PubRecTests.class);
public PubRecTests(String protocol) {
super(protocol);
}
/*
* [MQTT-3.5.2-1] The Client or Server sending the PUBREC packet MUST use one of the PUBREC Reason Codes.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testPubRecReasonCode() throws Exception {
final String TOPIC = RandomUtil.randomString();
final CountDownLatch latch = new CountDownLatch(1);
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBREC) {
assertEquals(MQTTReasonCodes.SUCCESS, ((MqttPubReplyMessageVariableHeader)packet.variableHeader()).reasonCode());
latch.countDown();
}
return true;
};
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
MqttClient publisher = createPahoClient("publisher");
publisher.connect();
publisher.publish(TOPIC, new byte[0], 2, false);
assertTrue(latch.await(2, TimeUnit.SECONDS));
publisher.disconnect();
publisher.close();
}
}

View File

@ -0,0 +1,84 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec.controlpackets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import io.netty.handler.codec.mqtt.MqttMessageType;
import io.netty.handler.codec.mqtt.MqttPubReplyMessageVariableHeader;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTInterceptor;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTReasonCodes;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.apache.activemq.artemis.utils.RandomUtil;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.jboss.logging.Logger;
import org.junit.Test;
/**
* Fulfilled by client or Netty codec (i.e. not tested here):
*
* [MQTT-3.6.1-1] Bits 3,2,1 and 0 of the Fixed Header in the PUBREL packet are reserved and MUST be set to 0,0,1 and 0 respectively. The Server MUST treat any other value as malformed and close the Network Connection.
*
*
* The broker doesn't send any "Reason String" or "User Property" in the PUBREL packet for any reason. Therefore, these are not tested here:
*
* [MQTT-3.6.2-2] The sender MUST NOT send this Property if it would increase the size of the PUBREL packet beyond the Maximum Packet Size specified by the receiver.
* [MQTT-3.6.2-3] The sender MUST NOT send this property if it would increase the size of the PUBREL packet beyond the Maximum Packet Size specified by the receiver.
*/
public class PubRelTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(PubRelTests.class);
public PubRelTests(String protocol) {
super(protocol);
}
/*
* [MQTT-3.6.2-1] The Client or Server sending the PUBREL packet MUST use one of the PUBREL Reason Codes.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testPubRelReasonCode() throws Exception {
final String TOPIC = RandomUtil.randomString();
final CountDownLatch latch = new CountDownLatch(2);
MQTTInterceptor outgoingInterceptor = (packet, connection) -> {
if (packet.fixedHeader().messageType() == MqttMessageType.PUBREL) {
assertEquals(MQTTReasonCodes.SUCCESS, ((MqttPubReplyMessageVariableHeader)packet.variableHeader()).reasonCode());
latch.countDown();
}
return true;
};
server.getRemotingService().addOutgoingInterceptor(outgoingInterceptor);
MqttClient consumer = createPahoClient("consumer");
consumer.connect();
consumer.setCallback(new LatchedMqttCallback(latch));
consumer.subscribe(TOPIC, 2);
MqttClient publisher = createPahoClient("publisher");
publisher.connect();
publisher.publish(TOPIC, new byte[0], 2, false);
assertTrue(latch.await(2, TimeUnit.SECONDS));
publisher.disconnect();
publisher.close();
}
}

View File

@ -0,0 +1,86 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec.controlpackets;
import java.nio.charset.StandardCharsets;
import org.apache.activemq.artemis.core.protocol.mqtt.MQTTReasonCodes;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.eclipse.paho.mqttv5.client.MqttClient;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptionsBuilder;
import org.eclipse.paho.mqttv5.common.MqttException;
import org.jboss.logging.Logger;
import org.junit.Test;
public class PublishTestsWithSecurity extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(PublishTestsWithSecurity.class);
public PublishTestsWithSecurity(String protocol) {
super(protocol);
}
@Override
public boolean isSecurityEnabled() {
return true;
}
@Test(timeout = DEFAULT_TIMEOUT)
public void testAuthorizationFailure() throws Exception {
final String CLIENT_ID = "publisher";
MqttConnectionOptions options = new MqttConnectionOptionsBuilder()
.username(noprivUser)
.password(noprivPass.getBytes(StandardCharsets.UTF_8))
.build();
MqttClient client = createPahoClient(CLIENT_ID);
client.connect(options);
try {
client.publish("/foo", new byte[0], 2, false);
fail("Publishing should have failed with a security problem");
} catch (MqttException e) {
assertEquals(MQTTReasonCodes.NOT_AUTHORIZED, (byte) e.getReasonCode());
} catch (Exception e) {
fail("Should have thrown an MqttException");
}
assertFalse(client.isConnected());
}
@Test(timeout = DEFAULT_TIMEOUT)
public void testAuthorizationSuccess() throws Exception {
final String CLIENT_ID = "publisher";
MqttConnectionOptions options = new MqttConnectionOptionsBuilder()
.username(fullUser)
.password(fullPass.getBytes(StandardCharsets.UTF_8))
.build();
MqttClient client = createPahoClient(CLIENT_ID);
client.connect(options);
try {
client.publish("/foo", new byte[0], 2, false);
} catch (MqttException e) {
fail("Publishing should not have failed with a security problem");
} catch (Exception e) {
fail("Should have thrown an MqttException");
}
client.isConnected();
}
}

View File

@ -0,0 +1,83 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.mqtt5.spec.controlpackets;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.tests.integration.mqtt5.MQTT5TestSupport;
import org.apache.activemq.artemis.utils.RandomUtil;
import org.eclipse.paho.mqttv5.client.IMqttToken;
import org.eclipse.paho.mqttv5.client.MqttAsyncClient;
import org.eclipse.paho.mqttv5.common.MqttSubscription;
import org.eclipse.paho.mqttv5.common.packet.MqttSubAck;
import org.jboss.logging.Logger;
import org.junit.Test;
/**
* The broker doesn't send any "Reason String" or "User Property" in the SUBACK packet for any reason. Therefore, these are not tested here:
*
* [MQTT-3.9.2-1] The Server MUST NOT send this Property if it would increase the size of the SUBACK packet beyond the Maximum Packet Size specified by the Client.
* [MQTT-3.9.2-2] The Server MUST NOT send this property if it would increase the size of the SUBACK packet beyond the Maximum Packet Size specified by the Client.
*/
public class SubAckTests extends MQTT5TestSupport {
private static final Logger log = Logger.getLogger(SubAckTests.class);
public SubAckTests(String protocol) {
super(protocol);
}
/*
* [MQTT-3.9.3-1] The order of Reason Codes in the SUBACK packet MUST match the order of Topic Filters in the
* SUBSCRIBE packet.
*
* [MQTT-3.9.3-2] The Server sending the SUBACK packet MUST send one of the Subscribe Reason Code values for each
* Topic Filter received.
*/
@Test(timeout = DEFAULT_TIMEOUT)
public void testSubscribeAck() throws Exception {
final int SUBSCRIPTION_COUNT = 30;
final String TOPIC = RandomUtil.randomString();
SimpleString[] topicNames = new SimpleString[SUBSCRIPTION_COUNT];
for (int i = 0; i < SUBSCRIPTION_COUNT; i++) {
topicNames[i] = new SimpleString(i + "-" + TOPIC);
}
MqttAsyncClient consumer = createAsyncPahoClient("consumer");
consumer.connect().waitForCompletion();
MqttSubscription[] subscriptions = new MqttSubscription[SUBSCRIPTION_COUNT];
for (int i = 0; i < SUBSCRIPTION_COUNT; i++) {
subscriptions[i] = new MqttSubscription(topicNames[i].toString(), RandomUtil.randomInterval(0, 3));
}
IMqttToken token = consumer.subscribe(subscriptions);
token.waitForCompletion();
MqttSubAck response = (MqttSubAck) token.getResponse();
assertEquals(subscriptions.length, response.getReturnCodes().length);
for (int i = 0; i < response.getReturnCodes().length; i++) {
assertEquals(subscriptions[i].getQos(), response.getReturnCodes()[i]);
}
for (int i = 0; i < SUBSCRIPTION_COUNT; i++) {
assertTrue(server.getPostOffice().isAddressBound(topicNames[i]));
}
consumer.disconnect();
consumer.close();
}
}

Some files were not shown because too many files have changed in this diff Show More