broker/Topic: class added
broker/Topic: String Topics replaced by Topic class broker/Topic#parseTopic: access set to private broker/SubscriptionsStoreTest: topic tests moved to own class broker/TopicTest: containsExactly used broker/TopicTest: TopicAssert class added broker/TopicTest$TopicAssert#containsToken: method added broker/TopicTest$TopicAssert#matches: method added
This commit is contained in:
+1
-1
@@ -26,7 +26,7 @@ public class InterceptSubscribeMessage implements InterceptMessage {
|
||||
}
|
||||
|
||||
public String getTopicFilter() {
|
||||
return subscription.getTopicFilter();
|
||||
return subscription.getTopicFilter().toString();
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
|
||||
@@ -147,7 +147,7 @@ public class ConnectionDescriptorStore implements IConnectionsManager {
|
||||
new MqttSubscription(
|
||||
subscription.getRequestedQos().toString(),
|
||||
subscription.getClientId(),
|
||||
subscription.getTopicFilter(),
|
||||
subscription.getTopicFilter().toString(),
|
||||
subscription.isActive()));
|
||||
}
|
||||
result.setActiveSubscriptions(mqttSubscriptions);
|
||||
|
||||
@@ -18,7 +18,7 @@ package io.moquette.spi;
|
||||
|
||||
import io.moquette.spi.ISessionsStore.ClientTopicCouple;
|
||||
import io.moquette.spi.impl.subscriptions.Subscription;
|
||||
import io.moquette.spi.impl.subscriptions.SubscriptionsStore;
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import java.util.HashSet;
|
||||
@@ -89,7 +89,7 @@ public class ClientSession {
|
||||
newSubscription.getClientId(),
|
||||
newSubscription.getTopicFilter(),
|
||||
newSubscription.getRequestedQos());
|
||||
boolean validTopic = SubscriptionsStore.validate(newSubscription.getTopicFilter());
|
||||
boolean validTopic = newSubscription.getTopicFilter().isValid();
|
||||
if (!validTopic) {
|
||||
LOG.warn(
|
||||
"The topic filter is not valid. MqttClientId = {}, topics = {}.",
|
||||
@@ -118,7 +118,7 @@ public class ClientSession {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void unsubscribeFrom(String topicFilter) {
|
||||
public void unsubscribeFrom(Topic topicFilter) {
|
||||
LOG.info("Removing subscription. MqttClientID = {}, topics = {}.", clientID, topicFilter);
|
||||
m_sessionsStore.removeSubscription(topicFilter, clientID);
|
||||
Set<Subscription> subscriptionsToRemove = new HashSet<>();
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
|
||||
package io.moquette.spi;
|
||||
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
|
||||
/**
|
||||
*/
|
||||
public interface IMatchingCondition {
|
||||
|
||||
boolean match(String key);
|
||||
boolean match(Topic key);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package io.moquette.spi;
|
||||
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
@@ -114,7 +115,7 @@ public interface IMessagesStore {
|
||||
* @param guid
|
||||
* of the message to mark as retained.
|
||||
*/
|
||||
void storeRetained(String topic, MessageGUID guid);
|
||||
void storeRetained(Topic topic, MessageGUID guid);
|
||||
|
||||
/**
|
||||
* Return a list of retained messages that satisfy the condition.
|
||||
@@ -138,7 +139,7 @@ public interface IMessagesStore {
|
||||
|
||||
StoredMessage getMessageByGuid(MessageGUID guid);
|
||||
|
||||
void cleanRetained(String topic);
|
||||
void cleanRetained(Topic topic);
|
||||
|
||||
int getPendingPublishMessages(String clientID);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.List;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import io.moquette.spi.IMessagesStore.StoredMessage;
|
||||
import io.moquette.spi.impl.subscriptions.Subscription;
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
|
||||
/**
|
||||
* Store used to handle the persistence of the subscriptions tree.
|
||||
@@ -31,10 +32,10 @@ public interface ISessionsStore {
|
||||
|
||||
class ClientTopicCouple {
|
||||
|
||||
public final String topicFilter;
|
||||
public final Topic topicFilter;
|
||||
public final String clientID;
|
||||
|
||||
public ClientTopicCouple(String clientID, String topicFilter) {
|
||||
public ClientTopicCouple(String clientID, Topic topicFilter) {
|
||||
this.clientID = clientID;
|
||||
this.topicFilter = topicFilter;
|
||||
}
|
||||
@@ -87,7 +88,7 @@ public interface ISessionsStore {
|
||||
* @param clientID
|
||||
* the session client.
|
||||
*/
|
||||
void removeSubscription(String topic, String clientID);
|
||||
void removeSubscription(Topic topic, String clientID);
|
||||
|
||||
/**
|
||||
* Remove all the subscriptions of the session
|
||||
|
||||
@@ -26,6 +26,7 @@ import io.moquette.spi.*;
|
||||
import io.moquette.spi.IMessagesStore.StoredMessage;
|
||||
import io.moquette.spi.impl.subscriptions.Subscription;
|
||||
import io.moquette.spi.impl.subscriptions.SubscriptionsStore;
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
import io.moquette.spi.security.IAuthenticator;
|
||||
import io.moquette.spi.security.IAuthorizator;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
@@ -610,7 +611,7 @@ public class ProtocolProcessor {
|
||||
*/
|
||||
public void internalPublish(MqttPublishMessage msg, final String clientId) {
|
||||
final MqttQoS qos = msg.fixedHeader().qosLevel();
|
||||
final String topic = msg.variableHeader().topicName();
|
||||
final Topic topic = new Topic(msg.variableHeader().topicName());
|
||||
LOG.info("Sending PUBLISH message. Topic = {}, qos = {}.", topic, qos);
|
||||
|
||||
MessageGUID guid = null;
|
||||
@@ -656,7 +657,7 @@ public class ProtocolProcessor {
|
||||
IMessagesStore.StoredMessage tobeStored = asStoredMessage(will);
|
||||
tobeStored.setClientID(clientID);
|
||||
tobeStored.setMessageID(messageId);
|
||||
String topic = tobeStored.getTopic();
|
||||
Topic topic = new Topic(tobeStored.getTopic());
|
||||
List<Subscription> topicMatchingSubscriptions = subscriptions.matches(topic);
|
||||
|
||||
LOG.info("Publishing will message. CId = {}, messageId = {}, topic = {}.", clientID, messageId, topic);
|
||||
@@ -836,8 +837,9 @@ public class ProtocolProcessor {
|
||||
LOG.info("Processing UNSUBSCRIBE message. CId = {}, topics = {}.", clientID, topics);
|
||||
|
||||
ClientSession clientSession = m_sessionsStore.sessionForClient(clientID);
|
||||
for (String topic : topics) {
|
||||
boolean validTopic = SubscriptionsStore.validate(topic);
|
||||
for (String t : topics) {
|
||||
Topic topic = new Topic(t);
|
||||
boolean validTopic = topic.isValid();
|
||||
if (!validTopic) {
|
||||
// close the connection, not valid topicFilter is a protocol violation
|
||||
channel.close();
|
||||
@@ -853,7 +855,7 @@ public class ProtocolProcessor {
|
||||
subscriptions.removeSubscription(topic, clientID);
|
||||
clientSession.unsubscribeFrom(topic);
|
||||
String username = NettyUtils.userName(channel);
|
||||
m_interceptor.notifyTopicUnsubscribed(topic, clientID, username);
|
||||
m_interceptor.notifyTopicUnsubscribed(topic.toString(), clientID, username);
|
||||
}
|
||||
|
||||
// ack the client
|
||||
@@ -926,7 +928,9 @@ public class ProtocolProcessor {
|
||||
if (req.qualityOfService() == FAILURE) {
|
||||
continue;
|
||||
}
|
||||
Subscription newSubscription = new Subscription(clientID, req.topicName(), req.qualityOfService());
|
||||
Subscription newSubscription =
|
||||
new Subscription(clientID, new Topic(req.topicName()), req.qualityOfService());
|
||||
|
||||
clientSession.subscribe(newSubscription);
|
||||
newSubscriptions.add(newSubscription);
|
||||
}
|
||||
@@ -948,7 +952,8 @@ public class ProtocolProcessor {
|
||||
|
||||
final int messageId = messageId(msg);
|
||||
for (MqttTopicSubscription req : msg.payload().topicSubscriptions()) {
|
||||
if (!m_authorizator.canRead(req.topicName(), username, clientSession.clientID)) {
|
||||
Topic topic = new Topic(req.topicName());
|
||||
if (!m_authorizator.canRead(topic, username, clientSession.clientID)) {
|
||||
// send SUBACK with 0x80, the user hasn't credentials to read the topic
|
||||
LOG.error(
|
||||
"The client does not have read permissions on the topic. "
|
||||
@@ -956,18 +961,18 @@ public class ProtocolProcessor {
|
||||
clientID,
|
||||
username,
|
||||
messageId,
|
||||
req.topicName());
|
||||
ackTopics.add(new MqttTopicSubscription(req.topicName(), FAILURE));
|
||||
topic);
|
||||
ackTopics.add(new MqttTopicSubscription(topic.toString(), FAILURE));
|
||||
} else {
|
||||
MqttQoS qos;
|
||||
if (SubscriptionsStore.validate(req.topicName())) {
|
||||
if (topic.isValid()) {
|
||||
LOG.info(
|
||||
"The client will be subscribed to the topic. "
|
||||
+ "CId = {}, username = {}, messageId = {}, topic = {}.",
|
||||
clientID,
|
||||
username,
|
||||
messageId,
|
||||
req.topicName());
|
||||
topic);
|
||||
qos = req.qualityOfService();
|
||||
} else {
|
||||
LOG.error(
|
||||
@@ -975,10 +980,10 @@ public class ProtocolProcessor {
|
||||
clientID,
|
||||
username,
|
||||
messageId,
|
||||
req.topicName());
|
||||
topic);
|
||||
qos = FAILURE;
|
||||
}
|
||||
ackTopics.add(new MqttTopicSubscription(req.topicName(), qos));
|
||||
ackTopics.add(new MqttTopicSubscription(topic.toString(), qos));
|
||||
}
|
||||
}
|
||||
return ackTopics;
|
||||
@@ -1009,8 +1014,8 @@ public class ProtocolProcessor {
|
||||
Collection<IMessagesStore.StoredMessage> messages = m_messagesStore.searchMatching(new IMatchingCondition() {
|
||||
|
||||
@Override
|
||||
public boolean match(String key) {
|
||||
return SubscriptionsStore.matchTopics(key, newSubscription.getTopicFilter());
|
||||
public boolean match(Topic key) {
|
||||
return key.match(newSubscription.getTopicFilter());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.moquette.server.netty.NettyUtils;
|
||||
import io.moquette.spi.IMessagesStore;
|
||||
import io.moquette.spi.impl.subscriptions.Subscription;
|
||||
import io.moquette.spi.impl.subscriptions.SubscriptionsStore;
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
import io.moquette.spi.security.IAuthorizator;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.handler.codec.mqtt.MqttPublishMessage;
|
||||
@@ -33,7 +34,7 @@ class Qos0PublishHandler extends QosPublishHandler {
|
||||
|
||||
void receivedPublishQos0(Channel channel, MqttPublishMessage msg) {
|
||||
// verify if topic can be write
|
||||
final String topic = msg.variableHeader().topicName();
|
||||
final Topic topic = new Topic(msg.variableHeader().topicName());
|
||||
if (checkWriteOnTopic(topic, channel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.moquette.spi.IMessagesStore;
|
||||
import io.moquette.spi.MessageGUID;
|
||||
import io.moquette.spi.impl.subscriptions.Subscription;
|
||||
import io.moquette.spi.impl.subscriptions.SubscriptionsStore;
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
import io.moquette.spi.security.IAuthorizator;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.handler.codec.mqtt.MqttFixedHeader;
|
||||
@@ -43,7 +44,7 @@ class Qos1PublishHandler extends QosPublishHandler {
|
||||
|
||||
void receivedPublishQos1(Channel channel, MqttPublishMessage msg) {
|
||||
// verify if topic can be write
|
||||
final String topic = msg.variableHeader().topicName();
|
||||
final Topic topic = new Topic(msg.variableHeader().topicName());
|
||||
if (checkWriteOnTopic(topic, channel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import io.moquette.spi.ISessionsStore;
|
||||
import io.moquette.spi.MessageGUID;
|
||||
import io.moquette.spi.impl.subscriptions.Subscription;
|
||||
import io.moquette.spi.impl.subscriptions.SubscriptionsStore;
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
import io.moquette.spi.security.IAuthorizator;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.handler.codec.mqtt.*;
|
||||
@@ -45,7 +46,7 @@ class Qos2PublishHandler extends QosPublishHandler {
|
||||
}
|
||||
|
||||
void receivedPublishQos2(Channel channel, MqttPublishMessage msg) {
|
||||
final String topic = msg.variableHeader().topicName();
|
||||
final Topic topic = new Topic(msg.variableHeader().topicName());
|
||||
// check if the topic can be wrote
|
||||
if (checkWriteOnTopic(topic, channel)) {
|
||||
return;
|
||||
@@ -102,7 +103,7 @@ class Qos2PublishHandler extends QosPublishHandler {
|
||||
LOG.info("Processing PUBREL message. MqttClientId = {}, messageId = {}.", clientID, messageID);
|
||||
ClientSession targetSession = m_sessionsStore.sessionForClient(clientID);
|
||||
IMessagesStore.StoredMessage evt = targetSession.storedMessage(messageID);
|
||||
final String topic = evt.getTopic();
|
||||
final Topic topic = new Topic(evt.getTopic());
|
||||
List<Subscription> topicMatchingSubscriptions = subscriptions.matches(topic);
|
||||
|
||||
if (LOG.isDebugEnabled()) {
|
||||
|
||||
@@ -4,6 +4,7 @@ package io.moquette.spi.impl;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.moquette.server.netty.NettyUtils;
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
import io.moquette.spi.security.IAuthorizator;
|
||||
import io.netty.channel.Channel;
|
||||
|
||||
@@ -17,7 +18,7 @@ abstract class QosPublishHandler {
|
||||
this.m_authorizator = m_authorizator;
|
||||
}
|
||||
|
||||
public boolean checkWriteOnTopic(String topic, Channel channel) {
|
||||
public boolean checkWriteOnTopic(Topic topic, Channel channel) {
|
||||
String clientID = NettyUtils.clientID(channel);
|
||||
String username = NettyUtils.userName(channel);
|
||||
if (!m_authorizator.canWrite(topic, username, clientID)) {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package io.moquette.spi.impl.security;
|
||||
|
||||
import static io.moquette.spi.impl.security.Authorization.Permission.READWRITE;
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
|
||||
/**
|
||||
* Carries the read/write authorization to topics for the users.
|
||||
@@ -25,7 +26,7 @@ import static io.moquette.spi.impl.security.Authorization.Permission.READWRITE;
|
||||
*/
|
||||
public class Authorization {
|
||||
|
||||
protected final String topic;
|
||||
protected final Topic topic;
|
||||
protected final Permission permission;
|
||||
|
||||
/**
|
||||
@@ -35,11 +36,11 @@ public class Authorization {
|
||||
READ, WRITE, READWRITE
|
||||
}
|
||||
|
||||
Authorization(String topic) {
|
||||
Authorization(Topic topic) {
|
||||
this(topic, Permission.READWRITE);
|
||||
}
|
||||
|
||||
Authorization(String topic, Permission permission) {
|
||||
Authorization(Topic topic, Permission permission) {
|
||||
this.topic = topic;
|
||||
this.permission = permission;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
package io.moquette.spi.impl.security;
|
||||
|
||||
import io.moquette.spi.impl.subscriptions.SubscriptionsStore;
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
import io.moquette.spi.security.IAuthorizator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -98,37 +98,37 @@ class AuthorizationsCollector implements IAuthorizator {
|
||||
try {
|
||||
Authorization.Permission permission = Authorization.Permission.valueOf(tokens[1].toUpperCase());
|
||||
// bring topic with all original spacing
|
||||
String topic = line.substring(line.indexOf(tokens[2]));
|
||||
Topic topic = new Topic(line.substring(line.indexOf(tokens[2])));
|
||||
|
||||
return new Authorization(topic, permission);
|
||||
} catch (IllegalArgumentException iaex) {
|
||||
throw new ParseException("invalid permission token", 1);
|
||||
}
|
||||
}
|
||||
String topic = tokens[1];
|
||||
Topic topic = new Topic(tokens[1]);
|
||||
return new Authorization(topic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(String topic, String user, String client) {
|
||||
public boolean canWrite(Topic topic, String user, String client) {
|
||||
return canDoOperation(topic, Authorization.Permission.WRITE, user, client);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead(String topic, String user, String client) {
|
||||
public boolean canRead(Topic topic, String user, String client) {
|
||||
return canDoOperation(topic, Authorization.Permission.READ, user, client);
|
||||
}
|
||||
|
||||
private boolean canDoOperation(String topic, Authorization.Permission permission, String username, String client) {
|
||||
private boolean canDoOperation(Topic topic, Authorization.Permission permission, String username, String client) {
|
||||
if (matchACL(m_globalAuthorizations, topic, permission)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isNotEmpty(client) || isNotEmpty(username)) {
|
||||
for (Authorization auth : m_patternAuthorizations) {
|
||||
String substitutedTopic = auth.topic.replace("%c", client).replace("%u", username);
|
||||
Topic substitutedTopic = new Topic(auth.topic.toString().replace("%c", client).replace("%u", username));
|
||||
if (auth.grant(permission)) {
|
||||
if (SubscriptionsStore.matchTopics(topic, substitutedTopic)) {
|
||||
if (topic.match(substitutedTopic)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -146,10 +146,10 @@ class AuthorizationsCollector implements IAuthorizator {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean matchACL(List<Authorization> auths, String topic, Authorization.Permission permission) {
|
||||
private boolean matchACL(List<Authorization> auths, Topic topic, Authorization.Permission permission) {
|
||||
for (Authorization auth : auths) {
|
||||
if (auth.grant(permission)) {
|
||||
if (SubscriptionsStore.matchTopics(topic, auth.topic)) {
|
||||
if (topic.match(auth.topic)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package io.moquette.spi.impl.security;
|
||||
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
import io.moquette.spi.security.IAuthorizator;
|
||||
|
||||
/**
|
||||
@@ -24,12 +25,12 @@ import io.moquette.spi.security.IAuthorizator;
|
||||
public class DenyAllAuthorizator implements IAuthorizator {
|
||||
|
||||
@Override
|
||||
public boolean canWrite(String topic, String user, String client) {
|
||||
public boolean canWrite(Topic topic, String user, String client) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead(String topic, String user, String client) {
|
||||
public boolean canRead(Topic topic, String user, String client) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package io.moquette.spi.impl.security;
|
||||
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
import io.moquette.spi.security.IAuthorizator;
|
||||
|
||||
/**
|
||||
@@ -24,12 +25,12 @@ import io.moquette.spi.security.IAuthorizator;
|
||||
public class PermitAllAuthorizator implements IAuthorizator {
|
||||
|
||||
@Override
|
||||
public boolean canWrite(String topic, String user, String client) {
|
||||
public boolean canWrite(Topic topic, String user, String client) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead(String topic, String user, String client) {
|
||||
public boolean canRead(Topic topic, String user, String client) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@ public final class Subscription implements Serializable {
|
||||
private static final long serialVersionUID = -3383457629635732794L;
|
||||
final MqttQoS requestedQos; // max QoS acceptable
|
||||
final String clientId;
|
||||
final String topicFilter;
|
||||
final Topic topicFilter;
|
||||
final boolean active;
|
||||
|
||||
public Subscription(String clientId, String topicFilter, MqttQoS requestedQos) {
|
||||
public Subscription(String clientId, Topic topicFilter, MqttQoS requestedQos) {
|
||||
this.requestedQos = requestedQos;
|
||||
this.clientId = clientId;
|
||||
this.topicFilter = topicFilter;
|
||||
@@ -56,7 +56,7 @@ public final class Subscription implements Serializable {
|
||||
return requestedQos;
|
||||
}
|
||||
|
||||
public String getTopicFilter() {
|
||||
public Topic getTopicFilter() {
|
||||
return topicFilter;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import io.moquette.spi.ISessionsStore;
|
||||
import io.moquette.spi.ISessionsStore.ClientTopicCouple;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import java.text.ParseException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -43,27 +42,6 @@ public class SubscriptionsStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the topic filter of the subscription is well formed
|
||||
*
|
||||
* @param topicFilter
|
||||
* the filter to validate
|
||||
* @return true if it's correct.
|
||||
*/
|
||||
public static boolean validate(String topicFilter) {
|
||||
try {
|
||||
parseTopic(topicFilter);
|
||||
return true;
|
||||
} catch (ParseException pex) {
|
||||
LOG.warn(
|
||||
"The topic filter is malformed. TopicFilter = {}, cause = {}, errorMessage = {}.",
|
||||
topicFilter,
|
||||
pex.getCause(),
|
||||
pex.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IVisitor<T> {
|
||||
|
||||
void visit(TreeNode node, int deep);
|
||||
@@ -150,24 +128,11 @@ public class SubscriptionsStore {
|
||||
LOG.debug("A subscription has been added. Root = {}, oldRoot = {}.", couple.root, oldRoot);
|
||||
}
|
||||
|
||||
protected NodeCouple recreatePath(String topic, final TreeNode oldRoot) {
|
||||
List<Token> tokens;
|
||||
try {
|
||||
tokens = parseTopic(topic);
|
||||
} catch (ParseException ex) {
|
||||
// TODO handle the parse exception
|
||||
LOG.error(
|
||||
"The topic is malformed. Topic = {}, cause = {}, errorMessage = {}.",
|
||||
topic,
|
||||
ex.getCause(),
|
||||
ex.getMessage());
|
||||
throw new IllegalArgumentException(ex.getMessage());
|
||||
}
|
||||
|
||||
protected NodeCouple recreatePath(Topic topic, final TreeNode oldRoot) {
|
||||
final TreeNode newRoot = oldRoot.copy();
|
||||
TreeNode parent = newRoot;
|
||||
TreeNode current = newRoot;
|
||||
for (Token token : tokens) {
|
||||
for (Token token : topic.getTokens()) {
|
||||
TreeNode matchingChildren;
|
||||
|
||||
// check if a children with the same token already exists
|
||||
@@ -188,7 +153,7 @@ public class SubscriptionsStore {
|
||||
return new NodeCouple(newRoot, current);
|
||||
}
|
||||
|
||||
public void removeSubscription(String topic, String clientID) {
|
||||
public void removeSubscription(Topic topic, String clientID) {
|
||||
/*
|
||||
* The topic filters have already been validated at the ProtocolProcessor. We can assume
|
||||
* they are valid.
|
||||
@@ -230,21 +195,8 @@ public class SubscriptionsStore {
|
||||
* to use fo searching matching subscriptions.
|
||||
* @return the list of matching subscriptions, or empty if not matching.
|
||||
*/
|
||||
public List<Subscription> matches(String topic) {
|
||||
List<Token> tokens;
|
||||
try {
|
||||
tokens = parseTopic(topic);
|
||||
} catch (ParseException ex) {
|
||||
// TODO handle the parse exception
|
||||
LOG.warn(
|
||||
"The topic is malformed. Topic = {}, cause = {}, errorMessage = {}.",
|
||||
topic,
|
||||
ex.getCause(),
|
||||
ex.getMessage());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Queue<Token> tokenQueue = new LinkedBlockingDeque<>(tokens);
|
||||
public List<Subscription> matches(Topic topic) {
|
||||
Queue<Token> tokenQueue = new LinkedBlockingDeque<>(topic.getTokens());
|
||||
List<ClientTopicCouple> matchingSubs = new ArrayList<>();
|
||||
subscriptions.get().matches(tokenQueue, matchingSubs);
|
||||
|
||||
@@ -288,101 +240,4 @@ public class SubscriptionsStore {
|
||||
bfsVisit(child, visitor, ++deep);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if the 2 topics matching respecting the rules of MQTT Appendix A
|
||||
*
|
||||
* @param msgTopic
|
||||
* the topic to match from the message
|
||||
* @param subscriptionTopic
|
||||
* the topic filter of the subscription
|
||||
* @return true if the two topics match.
|
||||
*/
|
||||
// TODO reimplement with iterators or with queues
|
||||
public static boolean matchTopics(String msgTopic, String subscriptionTopic) {
|
||||
try {
|
||||
List<Token> msgTokens = SubscriptionsStore.parseTopic(msgTopic);
|
||||
List<Token> subscriptionTokens = SubscriptionsStore.parseTopic(subscriptionTopic);
|
||||
int i = 0;
|
||||
for (; i < subscriptionTokens.size(); i++) {
|
||||
Token subToken = subscriptionTokens.get(i);
|
||||
if (subToken != Token.MULTI && subToken != Token.SINGLE) {
|
||||
if (i >= msgTokens.size()) {
|
||||
return false;
|
||||
}
|
||||
Token msgToken = msgTokens.get(i);
|
||||
if (!msgToken.equals(subToken)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (subToken == Token.MULTI) {
|
||||
return true;
|
||||
}
|
||||
if (subToken == Token.SINGLE) {
|
||||
// skip a step forward
|
||||
}
|
||||
}
|
||||
}
|
||||
// if last token was a SINGLE then treat it as an empty
|
||||
// if (subToken == Token.SINGLE && (i - msgTokens.size() == 1)) {
|
||||
// i--;
|
||||
// }
|
||||
return i == msgTokens.size();
|
||||
} catch (ParseException ex) {
|
||||
LOG.error(
|
||||
"The message topic, the subscription topic or both are malformed. "
|
||||
+ "MsgTopic = {}, subscriptionTopic = {}, cause = {}, errorMessage = {}.",
|
||||
msgTopic,
|
||||
subscriptionTopic,
|
||||
ex.getCause(),
|
||||
ex.getMessage());
|
||||
throw new IllegalStateException(ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected static List<Token> parseTopic(String topic) throws ParseException {
|
||||
List<Token> res = new ArrayList<>();
|
||||
String[] splitted = topic.split("/");
|
||||
|
||||
if (splitted.length == 0) {
|
||||
res.add(Token.EMPTY);
|
||||
}
|
||||
|
||||
if (topic.endsWith("/")) {
|
||||
// Add a fictious space
|
||||
String[] newSplitted = new String[splitted.length + 1];
|
||||
System.arraycopy(splitted, 0, newSplitted, 0, splitted.length);
|
||||
newSplitted[splitted.length] = "";
|
||||
splitted = newSplitted;
|
||||
}
|
||||
|
||||
for (int i = 0; i < splitted.length; i++) {
|
||||
String s = splitted[i];
|
||||
if (s.isEmpty()) {
|
||||
// if (i != 0) {
|
||||
// throw new ParseException("Bad format of topic, expetec topic name between
|
||||
// separators", i);
|
||||
// }
|
||||
res.add(Token.EMPTY);
|
||||
} else if (s.equals("#")) {
|
||||
// check that multi is the last symbol
|
||||
if (i != splitted.length - 1) {
|
||||
throw new ParseException(
|
||||
"Bad format of topic, the multi symbol (#) has to be the last one after a separator",
|
||||
i);
|
||||
}
|
||||
res.add(Token.MULTI);
|
||||
} else if (s.contains("#")) {
|
||||
throw new ParseException("Bad format of topic, invalid subtopic name: " + s, i);
|
||||
} else if (s.equals("+")) {
|
||||
res.add(Token.SINGLE);
|
||||
} else if (s.contains("+")) {
|
||||
throw new ParseException("Bad format of topic, invalid subtopic name: " + s, i);
|
||||
} else {
|
||||
res.add(new Token(s));
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2017 The original author or authorsgetRockQuestions()
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.spi.impl.subscriptions;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class Topic implements Serializable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Topic.class);
|
||||
|
||||
private static final long serialVersionUID = 2438799283749822L;
|
||||
|
||||
private final String topic;
|
||||
|
||||
private transient List<Token> tokens;
|
||||
|
||||
private transient boolean valid;
|
||||
|
||||
public Topic(String topic) {
|
||||
this.topic = topic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return topic;
|
||||
}
|
||||
|
||||
public List<Token> getTokens() {
|
||||
if (tokens == null) {
|
||||
try {
|
||||
tokens = parseTopic(topic);
|
||||
valid = true;
|
||||
} catch (ParseException e) {
|
||||
valid = false;
|
||||
LOG.error(e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private List<Token> parseTopic(String topic) throws ParseException {
|
||||
List<Token> res = new ArrayList<>();
|
||||
String[] splitted = topic.split("/");
|
||||
|
||||
if (splitted.length == 0) {
|
||||
res.add(Token.EMPTY);
|
||||
}
|
||||
|
||||
if (topic.endsWith("/")) {
|
||||
// Add a fictious space
|
||||
String[] newSplitted = new String[splitted.length + 1];
|
||||
System.arraycopy(splitted, 0, newSplitted, 0, splitted.length);
|
||||
newSplitted[splitted.length] = "";
|
||||
splitted = newSplitted;
|
||||
}
|
||||
|
||||
for (int i = 0; i < splitted.length; i++) {
|
||||
String s = splitted[i];
|
||||
if (s.isEmpty()) {
|
||||
// if (i != 0) {
|
||||
// throw new ParseException("Bad format of topic, expetec topic name between
|
||||
// separators", i);
|
||||
// }
|
||||
res.add(Token.EMPTY);
|
||||
} else if (s.equals("#")) {
|
||||
// check that multi is the last symbol
|
||||
if (i != splitted.length - 1) {
|
||||
throw new ParseException(
|
||||
"Bad format of topic, the multi symbol (#) has to be the last one after a separator",
|
||||
i);
|
||||
}
|
||||
res.add(Token.MULTI);
|
||||
} else if (s.contains("#")) {
|
||||
throw new ParseException("Bad format of topic, invalid subtopic name: " + s, i);
|
||||
} else if (s.equals("+")) {
|
||||
res.add(Token.SINGLE);
|
||||
} else if (s.contains("+")) {
|
||||
throw new ParseException("Bad format of topic, invalid subtopic name: " + s, i);
|
||||
} else {
|
||||
res.add(new Token(s));
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
if (tokens == null)
|
||||
getTokens();
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if the 2 topics matching respecting the rules of MQTT Appendix A
|
||||
*
|
||||
* @param msgTopic
|
||||
* the topic to match from the message
|
||||
* @param subscriptionTopic
|
||||
* the topic filter of the subscription
|
||||
* @return true if the two topics match.
|
||||
*/
|
||||
// TODO reimplement with iterators or with queues
|
||||
public boolean match(Topic subscriptionTopic) {
|
||||
List<Token> msgTokens = getTokens();
|
||||
List<Token> subscriptionTokens = subscriptionTopic.getTokens();
|
||||
int i = 0;
|
||||
for (; i < subscriptionTokens.size(); i++) {
|
||||
Token subToken = subscriptionTokens.get(i);
|
||||
if (subToken != Token.MULTI && subToken != Token.SINGLE) {
|
||||
if (i >= msgTokens.size()) {
|
||||
return false;
|
||||
}
|
||||
Token msgToken = msgTokens.get(i);
|
||||
if (!msgToken.equals(subToken)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (subToken == Token.MULTI) {
|
||||
return true;
|
||||
}
|
||||
if (subToken == Token.SINGLE) {
|
||||
// skip a step forward
|
||||
}
|
||||
}
|
||||
}
|
||||
// if last token was a SINGLE then treat it as an empty
|
||||
// if (subToken == Token.SINGLE && (i - msgTokens.size() == 1)) {
|
||||
// i--;
|
||||
// }
|
||||
return i == msgTokens.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Topic other = (Topic) obj;
|
||||
|
||||
return Objects.equals(this.topic, other.topic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return topic.hashCode();
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package io.moquette.spi.persistence;
|
||||
import io.moquette.spi.IMatchingCondition;
|
||||
import io.moquette.spi.IMessagesStore;
|
||||
import io.moquette.spi.MessageGUID;
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
import org.mapdb.DB;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -37,7 +38,7 @@ class MapDBMessagesStore implements IMessagesStore {
|
||||
private DB m_db;
|
||||
|
||||
// maps clientID -> guid
|
||||
private ConcurrentMap<String, MessageGUID> m_retainedStore;
|
||||
private ConcurrentMap<Topic, MessageGUID> m_retainedStore;
|
||||
// maps guid to message, it's message store
|
||||
private ConcurrentMap<MessageGUID, IMessagesStore.StoredMessage> m_persistentMessageStore;
|
||||
|
||||
@@ -53,7 +54,7 @@ class MapDBMessagesStore implements IMessagesStore {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeRetained(String topic, MessageGUID guid) {
|
||||
public void storeRetained(Topic topic, MessageGUID guid) {
|
||||
LOG.debug("Storing retained messages. Topic = {}, guid = {}.", topic, guid);
|
||||
m_retainedStore.put(topic, guid);
|
||||
}
|
||||
@@ -62,7 +63,7 @@ class MapDBMessagesStore implements IMessagesStore {
|
||||
public Collection<StoredMessage> searchMatching(IMatchingCondition condition) {
|
||||
LOG.debug("Scanning retained messages...");
|
||||
List<StoredMessage> results = new ArrayList<>();
|
||||
for (Map.Entry<String, MessageGUID> entry : m_retainedStore.entrySet()) {
|
||||
for (Map.Entry<Topic, MessageGUID> entry : m_retainedStore.entrySet()) {
|
||||
final MessageGUID guid = entry.getValue();
|
||||
StoredMessage storedMsg = m_persistentMessageStore.get(guid);
|
||||
if (condition.match(entry.getKey())) {
|
||||
@@ -127,7 +128,7 @@ class MapDBMessagesStore implements IMessagesStore {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanRetained(String topic) {
|
||||
public void cleanRetained(Topic topic) {
|
||||
LOG.debug("Cleaning retained messages. Topic = {}.", topic);
|
||||
m_retainedStore.remove(topic);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import io.moquette.spi.ISessionsStore;
|
||||
import io.moquette.spi.MessageGUID;
|
||||
import io.moquette.spi.impl.Utils;
|
||||
import io.moquette.spi.impl.subscriptions.Subscription;
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
import io.moquette.spi.persistence.MapDBPersistentStore.PersistentSession;
|
||||
import org.mapdb.DB;
|
||||
import org.slf4j.Logger;
|
||||
@@ -83,7 +84,7 @@ class MapDBSessionsStore implements ISessionsStore {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSubscription(String topicFilter, String clientID) {
|
||||
public void removeSubscription(Topic topicFilter, String clientID) {
|
||||
LOG.info("Removing subscription. CId= {}, topics = {}.", clientID, topicFilter);
|
||||
if (!m_db.exists("subscriptions_" + clientID)) {
|
||||
return;
|
||||
@@ -115,8 +116,8 @@ class MapDBSessionsStore implements ISessionsStore {
|
||||
LOG.info("Retrieving existing subscriptions...");
|
||||
final List<ClientTopicCouple> allSubscriptions = new ArrayList<>();
|
||||
for (String clientID : m_persistentSessions.keySet()) {
|
||||
ConcurrentMap<String, Subscription> clientSubscriptions = m_db.getHashMap("subscriptions_" + clientID);
|
||||
for (String topicFilter : clientSubscriptions.keySet()) {
|
||||
ConcurrentMap<Topic, Subscription> clientSubscriptions = m_db.getHashMap("subscriptions_" + clientID);
|
||||
for (Topic topicFilter : clientSubscriptions.keySet()) {
|
||||
allSubscriptions.add(new ClientTopicCouple(clientID, topicFilter));
|
||||
}
|
||||
}
|
||||
@@ -128,7 +129,7 @@ class MapDBSessionsStore implements ISessionsStore {
|
||||
|
||||
@Override
|
||||
public Subscription getSubscription(ClientTopicCouple couple) {
|
||||
ConcurrentMap<String, Subscription> clientSubscriptions = m_db.getHashMap("subscriptions_" + couple.clientID);
|
||||
ConcurrentMap<Topic, Subscription> clientSubscriptions = m_db.getHashMap("subscriptions_" + couple.clientID);
|
||||
LOG.info("Retrieving subscriptions. CId= {}, subscriptions = {}.", couple.clientID, clientSubscriptions);
|
||||
return clientSubscriptions.get(couple.topicFilter);
|
||||
}
|
||||
@@ -138,7 +139,7 @@ class MapDBSessionsStore implements ISessionsStore {
|
||||
LOG.info("Retrieving existing subscriptions...");
|
||||
List<Subscription> subscriptions = new ArrayList<>();
|
||||
for (String clientID : m_persistentSessions.keySet()) {
|
||||
ConcurrentMap<String, Subscription> clientSubscriptions = m_db.getHashMap("subscriptions_" + clientID);
|
||||
ConcurrentMap<Topic, Subscription> clientSubscriptions = m_db.getHashMap("subscriptions_" + clientID);
|
||||
subscriptions.addAll(clientSubscriptions.values());
|
||||
}
|
||||
LOG.debug("The existing subscriptions have been retrieved. Result = {}.", subscriptions);
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package io.moquette.spi.security;
|
||||
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
|
||||
/**
|
||||
* ACL checker.
|
||||
*
|
||||
@@ -37,7 +39,7 @@ public interface IAuthorizator {
|
||||
* the client
|
||||
* @return true if the user from client can publish data on topic.
|
||||
*/
|
||||
boolean canWrite(String topic, String user, String client);
|
||||
boolean canWrite(Topic topic, String user, String client);
|
||||
|
||||
boolean canRead(String topic, String user, String client);
|
||||
boolean canRead(Topic topic, String user, String client);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user