Moved packages from old io.moquette.server to new io.moquette.broker
This commit is contained in:
@@ -1,197 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.spi.impl;
|
||||
|
||||
import io.moquette.BrokerConstants;
|
||||
import io.moquette.interception.InterceptHandler;
|
||||
import io.moquette.interception.Interceptor;
|
||||
import io.moquette.interception.messages.*;
|
||||
import io.moquette.server.config.IConfig;
|
||||
import io.moquette.spi.impl.subscriptions.Subscription;
|
||||
import io.netty.handler.codec.mqtt.MqttConnectMessage;
|
||||
import io.netty.handler.codec.mqtt.MqttPublishMessage;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import static io.moquette.logging.LoggingUtils.getInterceptorIds;
|
||||
|
||||
/**
|
||||
* An interceptor that execute the interception tasks asynchronously.
|
||||
*/
|
||||
public final class BrokerInterceptor implements Interceptor {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BrokerInterceptor.class);
|
||||
private final Map<Class<?>, List<InterceptHandler>> handlers;
|
||||
private final ExecutorService executor;
|
||||
|
||||
private BrokerInterceptor(int poolSize, List<InterceptHandler> handlers) {
|
||||
LOG.info("Initializing broker interceptor. InterceptorIds={}", getInterceptorIds(handlers));
|
||||
this.handlers = new HashMap<>();
|
||||
for (Class<?> messageType : InterceptHandler.ALL_MESSAGE_TYPES) {
|
||||
this.handlers.put(messageType, new CopyOnWriteArrayList<InterceptHandler>());
|
||||
}
|
||||
for (InterceptHandler handler : handlers) {
|
||||
this.addInterceptHandler(handler);
|
||||
}
|
||||
executor = Executors.newFixedThreadPool(poolSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures a broker interceptor, with a thread pool of one thread.
|
||||
*
|
||||
* @param handlers InterceptHandlers listeners.
|
||||
*/
|
||||
public BrokerInterceptor(List<InterceptHandler> handlers) {
|
||||
this(1, handlers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures a broker interceptor using the pool size specified in the IConfig argument.
|
||||
* @param props configuration properties.
|
||||
* @param handlers InterceptHandlers listeners.
|
||||
*
|
||||
*/
|
||||
public BrokerInterceptor(IConfig props, List<InterceptHandler> handlers) {
|
||||
this(Integer.parseInt(props.getProperty(BrokerConstants.BROKER_INTERCEPTOR_THREAD_POOL_SIZE, "1")), handlers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown graciously the executor service
|
||||
*/
|
||||
void stop() {
|
||||
LOG.info("Shutting down interceptor thread pool...");
|
||||
executor.shutdown();
|
||||
try {
|
||||
LOG.info("Waiting for thread pool tasks to terminate...");
|
||||
executor.awaitTermination(10L, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
if (!executor.isTerminated()) {
|
||||
LOG.warn("Forcing shutdown of interceptor thread pool...");
|
||||
executor.shutdownNow();
|
||||
}
|
||||
LOG.info("interceptors stopped");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyClientConnected(final MqttConnectMessage msg) {
|
||||
for (final InterceptHandler handler : this.handlers.get(InterceptConnectMessage.class)) {
|
||||
LOG.debug("Sending MQTT CONNECT message to interceptor. CId={}, interceptorId={}",
|
||||
msg.payload().clientIdentifier(), handler.getID());
|
||||
executor.execute(() -> handler.onConnect(new InterceptConnectMessage(msg)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyClientDisconnected(final String clientID, final String username) {
|
||||
for (final InterceptHandler handler : this.handlers.get(InterceptDisconnectMessage.class)) {
|
||||
LOG.debug("Notifying MQTT client disconnection to interceptor. CId={}, username={}, interceptorId={}",
|
||||
clientID, username, handler.getID());
|
||||
executor.execute(() -> handler.onDisconnect(new InterceptDisconnectMessage(clientID, username)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyClientConnectionLost(final String clientID, final String username) {
|
||||
for (final InterceptHandler handler : this.handlers.get(InterceptConnectionLostMessage.class)) {
|
||||
LOG.debug("Notifying unexpected MQTT client disconnection to interceptor CId={}, username={}, " +
|
||||
"interceptorId={}", clientID, username, handler.getID());
|
||||
executor.execute(() -> handler.onConnectionLost(new InterceptConnectionLostMessage(clientID, username)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyTopicPublished(final MqttPublishMessage msg, final String clientID, final String username) {
|
||||
msg.retain();
|
||||
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
int messageId = msg.variableHeader().messageId();
|
||||
String topic = msg.variableHeader().topicName();
|
||||
for (InterceptHandler handler : handlers.get(InterceptPublishMessage.class)) {
|
||||
LOG.debug("Notifying MQTT PUBLISH message to interceptor. CId={}, messageId={}, topic={}, "
|
||||
+ "interceptorId={}", clientID, messageId, topic, handler.getID());
|
||||
handler.onPublish(new InterceptPublishMessage(msg, clientID, username));
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyTopicSubscribed(final Subscription sub, final String username) {
|
||||
for (final InterceptHandler handler : this.handlers.get(InterceptSubscribeMessage.class)) {
|
||||
LOG.debug("Notifying MQTT SUBSCRIBE message to interceptor. CId={}, topicFilter={}, interceptorId={}",
|
||||
sub.getClientId(), sub.getTopicFilter(), handler.getID());
|
||||
executor.execute(() -> handler.onSubscribe(new InterceptSubscribeMessage(sub, username)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyTopicUnsubscribed(final String topic, final String clientID, final String username) {
|
||||
for (final InterceptHandler handler : this.handlers.get(InterceptUnsubscribeMessage.class)) {
|
||||
LOG.debug("Notifying MQTT UNSUBSCRIBE message to interceptor. CId={}, topic={}, interceptorId={}", clientID,
|
||||
topic, handler.getID());
|
||||
executor.execute(() -> handler.onUnsubscribe(new InterceptUnsubscribeMessage(topic, clientID, username)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyMessageAcknowledged(final InterceptAcknowledgedMessage msg) {
|
||||
for (final InterceptHandler handler : this.handlers.get(InterceptAcknowledgedMessage.class)) {
|
||||
LOG.debug("Notifying MQTT ACK message to interceptor. CId={}, messageId={}, topic={}, interceptorId={}",
|
||||
msg.getMsg().getClientID(), msg.getPacketID(), msg.getTopic(), handler.getID());
|
||||
executor.execute(() -> handler.onMessageAcknowledged(msg));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptHandler(InterceptHandler interceptHandler) {
|
||||
Class<?>[] interceptedMessageTypes = getInterceptedMessageTypes(interceptHandler);
|
||||
LOG.info("Adding MQTT message interceptor. InterceptorId={}, handledMessageTypes={}",
|
||||
interceptHandler.getID(), interceptedMessageTypes);
|
||||
for (Class<?> interceptMessageType : interceptedMessageTypes) {
|
||||
this.handlers.get(interceptMessageType).add(interceptHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeInterceptHandler(InterceptHandler interceptHandler) {
|
||||
Class<?>[] interceptedMessageTypes = getInterceptedMessageTypes(interceptHandler);
|
||||
LOG.info("Removing MQTT message interceptor. InterceptorId={}, handledMessageTypes={}",
|
||||
interceptHandler.getID(), interceptedMessageTypes);
|
||||
for (Class<?> interceptMessageType : interceptedMessageTypes) {
|
||||
this.handlers.get(interceptMessageType).remove(interceptHandler);
|
||||
}
|
||||
}
|
||||
|
||||
private static Class<?>[] getInterceptedMessageTypes(InterceptHandler interceptHandler) {
|
||||
Class<?>[] interceptedMessageTypes = interceptHandler.getInterceptedMessageTypes();
|
||||
if (interceptedMessageTypes == null) {
|
||||
return InterceptHandler.ALL_MESSAGE_TYPES;
|
||||
}
|
||||
return interceptedMessageTypes;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.spi.impl;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public final class DebugUtils {
|
||||
|
||||
public static String payload2Str(ByteBuf content) {
|
||||
final ByteBuf copy = content.copy();
|
||||
final byte[] bytesContent;
|
||||
if (copy.isDirect()) {
|
||||
final int size = copy.readableBytes();
|
||||
bytesContent = new byte[size];
|
||||
copy.readBytes(bytesContent);
|
||||
} else {
|
||||
bytesContent = copy.array();
|
||||
}
|
||||
return new String(bytesContent, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private DebugUtils() {
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.spi.impl;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.mqtt.MqttMessage;
|
||||
import io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility static methods, like Map get with default value, or elvis operator.
|
||||
*/
|
||||
public final class Utils {
|
||||
|
||||
public static <T, K> T defaultGet(Map<K, T> map, K key, T defaultValue) {
|
||||
T value = map.get(key);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public static int messageId(MqttMessage msg) {
|
||||
return ((MqttMessageIdVariableHeader) msg.variableHeader()).messageId();
|
||||
}
|
||||
|
||||
public static byte[] readBytesAndRewind(ByteBuf payload) {
|
||||
byte[] payloadContent = new byte[payload.readableBytes()];
|
||||
int mark = payload.readerIndex();
|
||||
payload.readBytes(payloadContent);
|
||||
payload.readerIndex(mark);
|
||||
return payloadContent;
|
||||
}
|
||||
|
||||
private Utils() {
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.spi.impl.security;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.nio.file.Files;
|
||||
import java.text.ParseException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
/**
|
||||
* Parses the acl configuration file. If a line starts with # it's comment. Blank lines are skipped.
|
||||
* The format is "topic [read|write|readwrite] {topic name}"
|
||||
*/
|
||||
public final class ACLFileParser {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ACLFileParser.class);
|
||||
|
||||
/**
|
||||
* Parse the configuration from file.
|
||||
*
|
||||
* @param file
|
||||
* to parse
|
||||
* @return the collector of authorizations form reader passed into.
|
||||
* @throws ParseException
|
||||
* if the format is not compliant.
|
||||
*/
|
||||
public static AuthorizationsCollector parse(File file) throws ParseException {
|
||||
if (file == null) {
|
||||
LOG.warn("parsing NULL file, so fallback on default configuration!");
|
||||
return AuthorizationsCollector.emptyImmutableCollector();
|
||||
}
|
||||
if (!file.exists()) {
|
||||
LOG.warn(
|
||||
String.format(
|
||||
"parsing not existing file %s, so fallback on default configuration!",
|
||||
file.getAbsolutePath()));
|
||||
return AuthorizationsCollector.emptyImmutableCollector();
|
||||
}
|
||||
try {
|
||||
Reader reader = Files.newBufferedReader(file.toPath(), UTF_8);
|
||||
return parse(reader);
|
||||
} catch (IOException fex) {
|
||||
LOG.warn(
|
||||
String.format(
|
||||
"parsing not existing file %s, so fallback on default configuration!",
|
||||
file.getAbsolutePath()),
|
||||
fex);
|
||||
return AuthorizationsCollector.emptyImmutableCollector();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the ACL configuration file
|
||||
*
|
||||
* @param reader
|
||||
* to parse
|
||||
* @return the collector of authorizations form reader passed into.
|
||||
* @throws ParseException
|
||||
* if the format is not compliant.
|
||||
*/
|
||||
public static AuthorizationsCollector parse(Reader reader) throws ParseException {
|
||||
if (reader == null) {
|
||||
// just log and return default properties
|
||||
LOG.warn("parsing NULL reader, so fallback on default configuration!");
|
||||
return AuthorizationsCollector.emptyImmutableCollector();
|
||||
}
|
||||
|
||||
BufferedReader br = new BufferedReader(reader);
|
||||
String line;
|
||||
AuthorizationsCollector collector = new AuthorizationsCollector();
|
||||
|
||||
Pattern emptyLine = Pattern.compile("^\\s*$");
|
||||
Pattern commentLine = Pattern.compile("^#.*"); // As spec, comment lines should start with '#'
|
||||
Pattern invalidCommentLine = Pattern.compile("^\\s*#.*");
|
||||
// This pattern has a dependency on filtering `commentLine`.
|
||||
Pattern endLineComment = Pattern.compile("^([\\w\\s\\/\\+]+#?)(\\s*#.*)$");
|
||||
Matcher endLineCommentMatcher;
|
||||
|
||||
try {
|
||||
while ((line = br.readLine()) != null) {
|
||||
if (line.isEmpty() || emptyLine.matcher(line).matches() || commentLine.matcher(line).matches()) {
|
||||
// skip it's a black line or comment
|
||||
continue;
|
||||
} else if (invalidCommentLine.matcher(line).matches()) {
|
||||
// it's a malformed comment
|
||||
int commentMarker = line.indexOf('#');
|
||||
throw new ParseException(line, commentMarker);
|
||||
}
|
||||
|
||||
endLineCommentMatcher = endLineComment.matcher(line);
|
||||
if (endLineCommentMatcher.matches()) {
|
||||
line = endLineCommentMatcher.group(1);
|
||||
}
|
||||
|
||||
collector.parse(line);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new ParseException("Failed to read", 1);
|
||||
}
|
||||
return collector;
|
||||
}
|
||||
|
||||
private ACLFileParser() {
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.spi.impl.security;
|
||||
|
||||
import io.moquette.spi.security.IAuthenticator;
|
||||
|
||||
public class AcceptAllAuthenticator implements IAuthenticator {
|
||||
|
||||
@Override
|
||||
public boolean checkValid(String clientId, String username, byte[] password) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.spi.impl.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.
|
||||
*/
|
||||
public class Authorization {
|
||||
|
||||
protected final Topic topic;
|
||||
protected final Permission permission;
|
||||
|
||||
/**
|
||||
* Access rights
|
||||
*/
|
||||
enum Permission {
|
||||
READ, WRITE, READWRITE
|
||||
}
|
||||
|
||||
Authorization(Topic topic) {
|
||||
this(topic, Permission.READWRITE);
|
||||
}
|
||||
|
||||
Authorization(Topic topic, Permission permission) {
|
||||
this.topic = topic;
|
||||
this.permission = permission;
|
||||
}
|
||||
|
||||
public boolean grant(Permission desiredPermission) {
|
||||
return permission == desiredPermission || permission == READWRITE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
|
||||
Authorization that = (Authorization) o;
|
||||
|
||||
if (permission != that.permission)
|
||||
return false;
|
||||
if (!topic.equals(that.topic))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = topic.hashCode();
|
||||
result = 31 * result + permission.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.spi.impl.security;
|
||||
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
import io.moquette.spi.security.IAuthorizatorPolicy;
|
||||
import java.text.ParseException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Used by the ACLFileParser to push all authorizations it finds. ACLAuthorizator uses it in read
|
||||
* mode to check it topics matches the ACLs.
|
||||
*
|
||||
* Not thread safe.
|
||||
*/
|
||||
class AuthorizationsCollector implements IAuthorizatorPolicy {
|
||||
|
||||
private List<Authorization> m_globalAuthorizations = new ArrayList<>();
|
||||
private List<Authorization> m_patternAuthorizations = new ArrayList<>();
|
||||
private Map<String, List<Authorization>> m_userAuthorizations = new HashMap<>();
|
||||
private boolean m_parsingUsersSpecificSection;
|
||||
private boolean m_parsingPatternSpecificSection;
|
||||
private String m_currentUser = "";
|
||||
|
||||
static final AuthorizationsCollector emptyImmutableCollector() {
|
||||
AuthorizationsCollector coll = new AuthorizationsCollector();
|
||||
coll.m_globalAuthorizations = Collections.emptyList();
|
||||
coll.m_patternAuthorizations = Collections.emptyList();
|
||||
coll.m_userAuthorizations = Collections.emptyMap();
|
||||
return coll;
|
||||
}
|
||||
|
||||
void parse(String line) throws ParseException {
|
||||
Authorization acl = parseAuthLine(line);
|
||||
if (acl == null) {
|
||||
// skip it's a user
|
||||
return;
|
||||
}
|
||||
if (m_parsingUsersSpecificSection) {
|
||||
// TODO in java 8 switch to m_userAuthorizations.putIfAbsent(m_currentUser, new
|
||||
// ArrayList());
|
||||
if (!m_userAuthorizations.containsKey(m_currentUser)) {
|
||||
m_userAuthorizations.put(m_currentUser, new ArrayList<Authorization>());
|
||||
}
|
||||
List<Authorization> userAuths = m_userAuthorizations.get(m_currentUser);
|
||||
userAuths.add(acl);
|
||||
} else if (m_parsingPatternSpecificSection) {
|
||||
m_patternAuthorizations.add(acl);
|
||||
} else {
|
||||
m_globalAuthorizations.add(acl);
|
||||
}
|
||||
}
|
||||
|
||||
protected Authorization parseAuthLine(String line) throws ParseException {
|
||||
String[] tokens = line.split("\\s+");
|
||||
String keyword = tokens[0].toLowerCase();
|
||||
switch (keyword) {
|
||||
case "topic":
|
||||
return createAuthorization(line, tokens);
|
||||
case "user":
|
||||
m_parsingUsersSpecificSection = true;
|
||||
m_currentUser = tokens[1];
|
||||
m_parsingPatternSpecificSection = false;
|
||||
return null;
|
||||
case "pattern":
|
||||
m_parsingUsersSpecificSection = false;
|
||||
m_currentUser = "";
|
||||
m_parsingPatternSpecificSection = true;
|
||||
return createAuthorization(line, tokens);
|
||||
default:
|
||||
throw new ParseException(String.format("invalid line definition found %s", line), 1);
|
||||
}
|
||||
}
|
||||
|
||||
private Authorization createAuthorization(String line, String[] tokens) throws ParseException {
|
||||
if (tokens.length > 2) {
|
||||
// if the tokenized lines has 3 token the second must be the permission
|
||||
try {
|
||||
Authorization.Permission permission = Authorization.Permission.valueOf(tokens[1].toUpperCase());
|
||||
// bring topic with all original spacing
|
||||
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);
|
||||
}
|
||||
}
|
||||
Topic topic = new Topic(tokens[1]);
|
||||
return new Authorization(topic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(Topic topic, String user, String client) {
|
||||
return canDoOperation(topic, Authorization.Permission.WRITE, user, client);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead(Topic topic, String user, String client) {
|
||||
return canDoOperation(topic, Authorization.Permission.READ, user, 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) {
|
||||
Topic substitutedTopic = new Topic(auth.topic.toString().replace("%c", client).replace("%u", username));
|
||||
if (auth.grant(permission)) {
|
||||
if (topic.match(substitutedTopic)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isNotEmpty(username)) {
|
||||
if (m_userAuthorizations.containsKey(username)) {
|
||||
List<Authorization> auths = m_userAuthorizations.get(username);
|
||||
if (matchACL(auths, topic, permission)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean matchACL(List<Authorization> auths, Topic topic, Authorization.Permission permission) {
|
||||
for (Authorization auth : auths) {
|
||||
if (auth.grant(permission)) {
|
||||
if (topic.match(auth.topic)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isNotEmpty(String client) {
|
||||
return client != null && !client.isEmpty();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return m_globalAuthorizations.isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.spi.impl.security;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import io.moquette.BrokerConstants;
|
||||
import io.moquette.server.config.IConfig;
|
||||
import io.moquette.spi.security.IAuthenticator;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Load user credentials from a SQL database. sql driver must be provided at runtime
|
||||
*/
|
||||
public class DBAuthenticator implements IAuthenticator {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DBAuthenticator.class);
|
||||
|
||||
private final MessageDigest messageDigest;
|
||||
private HikariDataSource dataSource;
|
||||
private String sqlQuery;
|
||||
|
||||
public DBAuthenticator(IConfig conf) {
|
||||
this(conf.getProperty(BrokerConstants.DB_AUTHENTICATOR_DRIVER, ""),
|
||||
conf.getProperty(BrokerConstants.DB_AUTHENTICATOR_URL, ""),
|
||||
conf.getProperty(BrokerConstants.DB_AUTHENTICATOR_QUERY, ""),
|
||||
conf.getProperty(BrokerConstants.DB_AUTHENTICATOR_DIGEST, ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* provide authenticator from SQL database
|
||||
*
|
||||
* @param driver
|
||||
* : jdbc driver class like : "org.postgresql.Driver"
|
||||
* @param jdbcUrl
|
||||
* : jdbc url like : "jdbc:postgresql://host:port/dbname"
|
||||
* @param sqlQuery
|
||||
* : sql query like : "SELECT PASSWORD FROM USER WHERE LOGIN=?"
|
||||
* @param digestMethod
|
||||
* : password encoding algorithm : "MD5", "SHA-1", "SHA-256"
|
||||
*/
|
||||
public DBAuthenticator(String driver, String jdbcUrl, String sqlQuery, String digestMethod) {
|
||||
this.sqlQuery = sqlQuery;
|
||||
this.dataSource = new HikariDataSource();
|
||||
this.dataSource.setJdbcUrl(jdbcUrl);
|
||||
|
||||
try {
|
||||
this.messageDigest = MessageDigest.getInstance(digestMethod);
|
||||
} catch (NoSuchAlgorithmException nsaex) {
|
||||
LOG.error(String.format("Can't find %s for password encoding", digestMethod), nsaex);
|
||||
throw new RuntimeException(nsaex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean checkValid(String clientId, String username, byte[] password) {
|
||||
// Check Username / Password in DB using sqlQuery
|
||||
if (username == null || password == null) {
|
||||
LOG.info("username or password was null");
|
||||
return false;
|
||||
}
|
||||
|
||||
ResultSet resultSet = null;
|
||||
PreparedStatement preparedStatement = null;
|
||||
Connection conn = null;
|
||||
try {
|
||||
conn = this.dataSource.getConnection();
|
||||
|
||||
preparedStatement = conn.prepareStatement(this.sqlQuery);
|
||||
preparedStatement.setString(1, username);
|
||||
resultSet = preparedStatement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final String foundPwq = resultSet.getString(1);
|
||||
messageDigest.update(password);
|
||||
byte[] digest = messageDigest.digest();
|
||||
String encodedPasswd = new String(Hex.encodeHex(digest));
|
||||
return foundPwq.equals(encodedPasswd);
|
||||
}
|
||||
} catch (SQLException sqlex) {
|
||||
LOG.error("Error quering DB for username: {}", username, sqlex);
|
||||
} finally {
|
||||
try {
|
||||
if (resultSet != null) {
|
||||
resultSet.close();
|
||||
}
|
||||
if (preparedStatement != null) {
|
||||
preparedStatement.close();
|
||||
}
|
||||
if (conn != null) {
|
||||
conn.close();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOG.error("Error releasing connection to the datasource", username, e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.spi.impl.security;
|
||||
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
import io.moquette.spi.security.IAuthorizatorPolicy;
|
||||
|
||||
public class DenyAllAuthorizatorPolicy implements IAuthorizatorPolicy {
|
||||
|
||||
@Override
|
||||
public boolean canWrite(Topic topic, String user, String client) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead(Topic topic, String user, String client) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.spi.impl.security;
|
||||
|
||||
import io.moquette.server.config.FileResourceLoader;
|
||||
|
||||
/**
|
||||
* Load user credentials from a text file. Each line of the file is formatted as
|
||||
* "[username]:[sha256(password)]". The username mustn't contains : char.
|
||||
*
|
||||
* To encode your password from command line on Linux systems, you could use:
|
||||
*
|
||||
* <pre>
|
||||
* echo -n "yourpassword" | sha256sum
|
||||
* </pre>
|
||||
*
|
||||
* NB -n is important because echo append a newline by default at the of string. -n avoid this
|
||||
* behaviour.
|
||||
*
|
||||
* @deprecated user {@link ResourceAuthenticator} instead
|
||||
*/
|
||||
public class FileAuthenticator extends ResourceAuthenticator {
|
||||
|
||||
public FileAuthenticator(String parent, String filePath) {
|
||||
super(new FileResourceLoader(parent), filePath);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.spi.impl.security;
|
||||
|
||||
import io.moquette.spi.impl.subscriptions.Topic;
|
||||
import io.moquette.spi.security.IAuthorizatorPolicy;
|
||||
|
||||
public class PermitAllAuthorizatorPolicy implements IAuthorizatorPolicy {
|
||||
|
||||
@Override
|
||||
public boolean canWrite(Topic topic, String user, String client) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead(Topic topic, String user, String client) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.spi.impl.security;
|
||||
|
||||
import io.moquette.server.config.IResourceLoader;
|
||||
import io.moquette.spi.security.IAuthenticator;
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.ParseException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Load user credentials from a text resource. Each line of the file is formatted as
|
||||
* "[username]:[sha256(password)]". The username mustn't contains : char.
|
||||
*
|
||||
* To encode your password from command line on Linux systems, you could use:
|
||||
*
|
||||
* <pre>
|
||||
* echo -n "yourpassword" | sha256sum
|
||||
* </pre>
|
||||
*
|
||||
* NB -n is important because echo append a newline by default at the of string. -n avoid this
|
||||
* behaviour.
|
||||
*/
|
||||
public class ResourceAuthenticator implements IAuthenticator {
|
||||
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(ResourceAuthenticator.class);
|
||||
|
||||
private Map<String, String> m_identities = new HashMap<>();
|
||||
|
||||
public ResourceAuthenticator(IResourceLoader resourceLoader, String resourceName) {
|
||||
try {
|
||||
MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException nsaex) {
|
||||
LOG.error("Can't find SHA-256 for password encoding", nsaex);
|
||||
throw new RuntimeException(nsaex);
|
||||
}
|
||||
|
||||
LOG.info(String.format("Loading password %s %s", resourceLoader.getName(), resourceName));
|
||||
Reader reader = null;
|
||||
try {
|
||||
reader = resourceLoader.loadResource(resourceName);
|
||||
if (reader == null) {
|
||||
LOG.warn(String.format("Parsing not existing %s %s", resourceLoader.getName(), resourceName));
|
||||
} else {
|
||||
parse(reader);
|
||||
}
|
||||
} catch (IResourceLoader.ResourceIsDirectoryException e) {
|
||||
LOG.warn(String.format("Trying to parse directory %s", resourceName));
|
||||
} catch (ParseException pex) {
|
||||
LOG.warn(
|
||||
String.format("Format error in parsing password %s %s", resourceLoader.getName(), resourceName),
|
||||
pex);
|
||||
}
|
||||
}
|
||||
|
||||
private void parse(Reader reader) throws ParseException {
|
||||
if (reader == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BufferedReader br = new BufferedReader(reader);
|
||||
String line;
|
||||
try {
|
||||
while ((line = br.readLine()) != null) {
|
||||
int commentMarker = line.indexOf('#');
|
||||
if (commentMarker != -1) {
|
||||
if (commentMarker == 0) {
|
||||
// skip its a comment
|
||||
continue;
|
||||
} else {
|
||||
// it's a malformed comment
|
||||
throw new ParseException(line, commentMarker);
|
||||
}
|
||||
} else {
|
||||
if (line.isEmpty() || line.matches("^\\s*$")) {
|
||||
// skip it's a black line
|
||||
continue;
|
||||
}
|
||||
|
||||
// split till the first space
|
||||
int delimiterIdx = line.indexOf(':');
|
||||
String username = line.substring(0, delimiterIdx).trim();
|
||||
String password = line.substring(delimiterIdx + 1).trim();
|
||||
|
||||
m_identities.put(username, password);
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new ParseException("Failed to read", 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkValid(String clientId, String username, byte[] password) {
|
||||
if (username == null || password == null) {
|
||||
LOG.info("username or password was null");
|
||||
return false;
|
||||
}
|
||||
String foundPwq = m_identities.get(username);
|
||||
if (foundPwq == null) {
|
||||
return false;
|
||||
}
|
||||
String encodedPasswd = DigestUtils.sha256Hex(password);
|
||||
return foundPwq.equals(encodedPasswd);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.spi.impl.subscriptions;
|
||||
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
class CNode {
|
||||
|
||||
Token token;
|
||||
private List<INode> children;
|
||||
Set<Subscription> subscriptions;
|
||||
|
||||
CNode() {
|
||||
this.children = new ArrayList<>();
|
||||
this.subscriptions = new HashSet<>();
|
||||
}
|
||||
|
||||
//Copy constructor
|
||||
private CNode(Token token, List<INode> children, Set<Subscription> subscriptions) {
|
||||
this.token = token; // keep reference, root comparison in directory logic relies on it for now.
|
||||
this.subscriptions = new HashSet<>(subscriptions);
|
||||
this.children = new ArrayList<>(children);
|
||||
}
|
||||
|
||||
boolean anyChildrenMatch(Token token) {
|
||||
for (INode iNode : children) {
|
||||
final CNode child = iNode.mainNode();
|
||||
if (child.equalsToken(token)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<INode> allChildren() {
|
||||
return this.children;
|
||||
}
|
||||
|
||||
INode childOf(Token token) {
|
||||
for (INode iNode : children) {
|
||||
final CNode child = iNode.mainNode();
|
||||
if (child.equalsToken(token)) {
|
||||
return iNode;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Asked for a token that doesn't exists in any child [" + token + "]");
|
||||
}
|
||||
|
||||
private boolean equalsToken(Token token) {
|
||||
return token != null && this.token != null && this.token.equals(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(token);
|
||||
}
|
||||
|
||||
CNode copy() {
|
||||
return new CNode(this.token, this.children, this.subscriptions);
|
||||
}
|
||||
|
||||
public void add(INode newINode) {
|
||||
this.children.add(newINode);
|
||||
}
|
||||
public void remove(INode node) {
|
||||
this.children.remove(node);
|
||||
}
|
||||
|
||||
CNode addSubscription(Subscription newSubscription) {
|
||||
// if already contains one with same topic and same client, keep that with higher QoS
|
||||
if (subscriptions.contains(newSubscription)) {
|
||||
final Subscription existing = subscriptions.stream()
|
||||
.filter(s -> s.equals(newSubscription))
|
||||
.findFirst().get();
|
||||
if (existing.getRequestedQos().value() < newSubscription.getRequestedQos().value()) {
|
||||
subscriptions.remove(existing);
|
||||
subscriptions.add(new Subscription(newSubscription));
|
||||
}
|
||||
} else {
|
||||
this.subscriptions.add(new Subscription(newSubscription));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true iff the subscriptions contained in this node are owned by clientId
|
||||
* AND at least one subscription is actually present for that clientId
|
||||
* */
|
||||
boolean containsOnly(String clientId) {
|
||||
for (Subscription sub : this.subscriptions) {
|
||||
if (!sub.clientId.equals(clientId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !this.subscriptions.isEmpty();
|
||||
}
|
||||
|
||||
//TODO this is equivalent to negate(containsOnly(clientId))
|
||||
public boolean contains(String clientId) {
|
||||
for (Subscription sub : this.subscriptions) {
|
||||
if (sub.clientId.equals(clientId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void removeSubscriptionsFor(String clientId) {
|
||||
Set<Subscription> toRemove = new HashSet<>();
|
||||
for (Subscription sub : this.subscriptions) {
|
||||
if (sub.clientId.equals(clientId)) {
|
||||
toRemove.add(sub);
|
||||
}
|
||||
}
|
||||
this.subscriptions.removeAll(toRemove);
|
||||
}
|
||||
}
|
||||
-296
@@ -1,296 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.spi.impl.subscriptions;
|
||||
|
||||
import io.moquette.broker.ISubscriptionsRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CTrieSubscriptionDirectory.class);
|
||||
|
||||
private static final Token ROOT = new Token("root");
|
||||
private static final INode NO_PARENT = null;
|
||||
|
||||
INode root;
|
||||
private volatile ISubscriptionsRepository sessionsRepository;
|
||||
|
||||
interface IVisitor<T> {
|
||||
|
||||
void visit(CNode node, int deep);
|
||||
|
||||
T getResult();
|
||||
}
|
||||
|
||||
private enum Action {
|
||||
OK, REPEAT
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ISubscriptionsRepository sessionsRepository) {
|
||||
LOG.info("Initializing CTrie");
|
||||
final CNode mainNode = new CNode();
|
||||
mainNode.token = ROOT;
|
||||
this.root = new INode(mainNode);
|
||||
|
||||
LOG.info("Initializing subscriptions store...");
|
||||
this.sessionsRepository = sessionsRepository;
|
||||
// reload any subscriptions persisted
|
||||
if (LOG.isTraceEnabled()) {
|
||||
LOG.trace("Reloading all stored subscriptions. SubscriptionTree = {}", dumpTree());
|
||||
}
|
||||
|
||||
for (Subscription subscription : this.sessionsRepository.listAllSubscriptions()) {
|
||||
LOG.info("Re-subscribing client to topic CId={}, topicFilter={}", subscription.clientId,
|
||||
subscription.topicFilter);
|
||||
add(subscription);
|
||||
}
|
||||
if (LOG.isTraceEnabled()) {
|
||||
LOG.trace("Stored subscriptions have been reloaded. SubscriptionTree = {}", dumpTree());
|
||||
}
|
||||
}
|
||||
|
||||
Optional<CNode> lookup(Topic topic) {
|
||||
INode inode = this.root;
|
||||
Token token = topic.headToken();
|
||||
while (!topic.isEmpty() && (inode.mainNode().anyChildrenMatch(token))) {
|
||||
topic = topic.exceptHeadToken();
|
||||
inode = inode.mainNode().childOf(token);
|
||||
token = topic.headToken();
|
||||
}
|
||||
if (inode == null || !topic.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(inode.mainNode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Subscription> matches(Topic topic) {
|
||||
return new ArrayList<>(matchQosSharpening(topic));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a topic string return the clients subscriptions that matches it. Topic string can't
|
||||
* contain character # and + because they are reserved to listeners subscriptions, and not topic
|
||||
* publishing.
|
||||
*
|
||||
* @param topic
|
||||
* to use fo searching matching subscriptions.
|
||||
* @return the list of matching subscriptions, or empty if not matching.
|
||||
*/
|
||||
@Override
|
||||
public Set<Subscription> matchWithoutQosSharpening(Topic topic) {
|
||||
return recursiveMatch(topic, this.root);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Subscription> matchQosSharpening(Topic topic) {
|
||||
final Set<Subscription> subscriptions = matchWithoutQosSharpening(topic);
|
||||
|
||||
Map<String, Subscription> subsGroupedByClient = new HashMap<>();
|
||||
for (Subscription sub : subscriptions) {
|
||||
Subscription existingSub = subsGroupedByClient.get(sub.clientId);
|
||||
// update the selected subscriptions if not present or if has a greater qos
|
||||
if (existingSub == null || existingSub.qosLessThan(sub)) {
|
||||
subsGroupedByClient.put(sub.clientId, sub);
|
||||
}
|
||||
}
|
||||
return new HashSet<>(subsGroupedByClient.values());
|
||||
}
|
||||
|
||||
Set<Subscription> recursiveMatch(Topic topic, INode inode) {
|
||||
CNode cnode = inode.mainNode();
|
||||
if (Token.MULTI.equals(cnode.token)) {
|
||||
return cnode.subscriptions;
|
||||
}
|
||||
if (topic.isEmpty()) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
if (cnode instanceof TNode) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
final Token token = topic.headToken();
|
||||
if (!(Token.SINGLE.equals(cnode.token) || cnode.token.equals(token) || ROOT.equals(cnode.token))) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Topic remainingTopic = (ROOT.equals(cnode.token)) ? topic : topic.exceptHeadToken();
|
||||
Set<Subscription> subscriptions = new HashSet<>();
|
||||
if (remainingTopic.isEmpty()) {
|
||||
subscriptions.addAll(cnode.subscriptions);
|
||||
}
|
||||
for (INode subInode : cnode.allChildren()) {
|
||||
subscriptions.addAll(recursiveMatch(remainingTopic, subInode));
|
||||
}
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Cleans Disposes of TNode in separate Atomic CAS operation per
|
||||
* http://bravenewgeek.com/breaking-and-entering-lose-the-lock-while-embracing-concurrency/
|
||||
*
|
||||
* We roughly follow this theory above, but we allow CNode with no Subscriptions to linger (for now).
|
||||
*
|
||||
*
|
||||
* @param inode inode that handle to the tomb node.
|
||||
* @param iParent inode parent.
|
||||
* @return REPEAT if the this methods wasn't successful or OK.
|
||||
*/
|
||||
public Action cleanTomb(INode inode, INode iParent) {
|
||||
CNode updatedCnode = iParent.mainNode().copy();
|
||||
updatedCnode.remove(inode);
|
||||
return iParent.compareAndSet(iParent.mainNode(), updatedCnode) ? Action.OK : Action.REPEAT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(Subscription newSubscription) {
|
||||
Action res;
|
||||
do {
|
||||
res = insert(newSubscription.topicFilter, this.root, newSubscription);
|
||||
} while (res == Action.REPEAT);
|
||||
}
|
||||
|
||||
private Action insert(Topic topic, final INode inode, Subscription newSubscription) {
|
||||
Token token = topic.headToken();
|
||||
if (!topic.isEmpty() && inode.mainNode().anyChildrenMatch(token)) {
|
||||
Topic remainingTopic = topic.exceptHeadToken();
|
||||
INode nextInode = inode.mainNode().childOf(token);
|
||||
return insert(remainingTopic, nextInode, newSubscription);
|
||||
} else {
|
||||
if (topic.isEmpty()) {
|
||||
return insertSubscription(inode, newSubscription);
|
||||
} else {
|
||||
return createNodeAndInsertSubscription(topic, inode, newSubscription);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Action insertSubscription(INode inode, Subscription newSubscription) {
|
||||
CNode cnode = inode.mainNode();
|
||||
CNode updatedCnode = cnode.copy().addSubscription(newSubscription);
|
||||
if (inode.compareAndSet(cnode, updatedCnode)) {
|
||||
return Action.OK;
|
||||
} else {
|
||||
return Action.REPEAT;
|
||||
}
|
||||
}
|
||||
|
||||
private Action createNodeAndInsertSubscription(Topic topic, INode inode, Subscription newSubscription) {
|
||||
INode newInode = createPathRec(topic, newSubscription);
|
||||
CNode cnode = inode.mainNode();
|
||||
CNode updatedCnode = cnode.copy();
|
||||
updatedCnode.add(newInode);
|
||||
|
||||
return inode.compareAndSet(cnode, updatedCnode) ? Action.OK : Action.REPEAT;
|
||||
}
|
||||
|
||||
private INode createLeafNodes(Token token, Subscription newSubscription) {
|
||||
CNode newLeafCnode = new CNode();
|
||||
newLeafCnode.token = token;
|
||||
newLeafCnode.addSubscription(newSubscription);
|
||||
|
||||
return new INode(newLeafCnode);
|
||||
}
|
||||
|
||||
private INode createPathRec(Topic topic, Subscription newSubscription) {
|
||||
Topic remainingTopic = topic.exceptHeadToken();
|
||||
if (!remainingTopic.isEmpty()) {
|
||||
INode inode = createPathRec(remainingTopic, newSubscription);
|
||||
CNode cnode = new CNode();
|
||||
cnode.token = topic.headToken();
|
||||
cnode.add(inode);
|
||||
return new INode(cnode);
|
||||
} else {
|
||||
return createLeafNodes(topic.headToken(), newSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes subscription from CTrie, adds TNode when the last client unsubscribes, then calls for cleanTomb in a
|
||||
* separate atomic CAS operation.
|
||||
*
|
||||
* @param topic the subscription's topic to remove.
|
||||
* @param clientID the Id of client owning the subscription.
|
||||
*/
|
||||
@Override
|
||||
public void removeSubscription(Topic topic, String clientID) {
|
||||
Action res;
|
||||
do {
|
||||
res = remove(clientID, topic, this.root, NO_PARENT);
|
||||
} while (res == Action.REPEAT);
|
||||
}
|
||||
|
||||
private Action remove(String clientId, Topic topic, INode inode, INode iParent) {
|
||||
Token token = topic.headToken();
|
||||
if (!topic.isEmpty() && (inode.mainNode().anyChildrenMatch(token))) {
|
||||
Topic remainingTopic = topic.exceptHeadToken();
|
||||
INode nextInode = inode.mainNode().childOf(token);
|
||||
return remove(clientId, remainingTopic, nextInode, inode);
|
||||
} else {
|
||||
final CNode cnode = inode.mainNode();
|
||||
if (cnode instanceof TNode) {
|
||||
// this inode is a tomb, has no clients and should be cleaned up
|
||||
// Because we implemented cleanTomb below, this should be rare, but possible
|
||||
// Consider calling cleanTomb here too
|
||||
return Action.OK;
|
||||
}
|
||||
if (cnode.containsOnly(clientId) && topic.isEmpty() && cnode.allChildren().isEmpty()) {
|
||||
// last client to leave this node, AND there are no downstream children, remove via TNode tomb
|
||||
if (inode == this.root) {
|
||||
return inode.compareAndSet(cnode, inode.mainNode().copy()) ? Action.OK : Action.REPEAT;
|
||||
}
|
||||
TNode tnode = new TNode();
|
||||
return inode.compareAndSet(cnode, tnode) ? cleanTomb(inode, iParent) : Action.REPEAT;
|
||||
} else if (cnode.contains(clientId) && topic.isEmpty()) {
|
||||
CNode updatedCnode = cnode.copy();
|
||||
updatedCnode.removeSubscriptionsFor(clientId);
|
||||
return inode.compareAndSet(cnode, updatedCnode) ? Action.OK : Action.REPEAT;
|
||||
} else {
|
||||
//someone else already removed
|
||||
return Action.OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
SubscriptionCounterVisitor visitor = new SubscriptionCounterVisitor();
|
||||
dfsVisit(this.root, visitor, 0);
|
||||
return visitor.getResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String dumpTree() {
|
||||
DumpTreeVisitor visitor = new DumpTreeVisitor();
|
||||
dfsVisit(this.root, visitor, 0);
|
||||
return visitor.getResult();
|
||||
}
|
||||
|
||||
private void dfsVisit(INode node, IVisitor<?> visitor, int deep) {
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
visitor.visit(node.mainNode(), deep);
|
||||
++deep;
|
||||
for (INode child : node.mainNode().allChildren()) {
|
||||
dfsVisit(child, visitor, deep);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.spi.impl.subscriptions;
|
||||
|
||||
import io.netty.util.internal.StringUtil;
|
||||
|
||||
class DumpTreeVisitor implements CTrieSubscriptionDirectory.IVisitor<String> {
|
||||
|
||||
String s = "";
|
||||
|
||||
@Override
|
||||
public void visit(CNode node, int deep) {
|
||||
String indentTabs = indentTabs(deep);
|
||||
s += indentTabs + (node.token == null ? "''" : node.token.toString()) + prettySubscriptions(node) + "\n";
|
||||
}
|
||||
|
||||
private String prettySubscriptions(CNode node) {
|
||||
if (node instanceof TNode) {
|
||||
return "TNode";
|
||||
}
|
||||
if (node.subscriptions.isEmpty()) {
|
||||
return StringUtil.EMPTY_STRING;
|
||||
}
|
||||
StringBuilder subScriptionsStr = new StringBuilder(" ~~[");
|
||||
int counter = 0;
|
||||
for (Subscription couple : node.subscriptions) {
|
||||
subScriptionsStr
|
||||
.append("{filter=").append(couple.topicFilter).append(", ")
|
||||
.append("client='").append(couple.clientId).append("'}");
|
||||
counter++;
|
||||
if (counter < node.subscriptions.size()) {
|
||||
subScriptionsStr.append(";");
|
||||
}
|
||||
}
|
||||
return subScriptionsStr.append("]").toString();
|
||||
}
|
||||
|
||||
private String indentTabs(int deep) {
|
||||
StringBuilder s = new StringBuilder();
|
||||
if (deep > 0) {
|
||||
s.append(" ");
|
||||
for (int i = 0; i < deep - 1; i++) {
|
||||
s.append("| ");
|
||||
}
|
||||
s.append("|-");
|
||||
}
|
||||
return s.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResult() {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.spi.impl.subscriptions;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
class INode {
|
||||
private AtomicReference<CNode> mainNode = new AtomicReference<>();
|
||||
|
||||
INode(CNode mainNode) {
|
||||
this.mainNode.set(mainNode);
|
||||
if (mainNode instanceof TNode) { // this should never happen
|
||||
throw new IllegalStateException("TNode should not be set on mainNnode");
|
||||
}
|
||||
}
|
||||
|
||||
boolean compareAndSet(CNode old, CNode newNode) {
|
||||
return mainNode.compareAndSet(old, newNode);
|
||||
}
|
||||
|
||||
boolean compareAndSet(CNode old, TNode newNode) {
|
||||
return mainNode.compareAndSet(old, newNode);
|
||||
}
|
||||
|
||||
CNode mainNode() {
|
||||
return this.mainNode.get();
|
||||
}
|
||||
|
||||
boolean isTombed() {
|
||||
return this.mainNode() instanceof TNode;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.spi.impl.subscriptions;
|
||||
|
||||
import io.moquette.broker.ISubscriptionsRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public interface ISubscriptionsDirectory {
|
||||
|
||||
void init(ISubscriptionsRepository sessionsRepository);
|
||||
|
||||
Set<Subscription> matchWithoutQosSharpening(Topic topic);
|
||||
|
||||
Set<Subscription> matchQosSharpening(Topic topic);
|
||||
|
||||
void add(Subscription newSubscription);
|
||||
|
||||
void removeSubscription(Topic topic, String clientID);
|
||||
|
||||
List<Subscription> matches(Topic topic);
|
||||
|
||||
int size();
|
||||
|
||||
String dumpTree();
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.spi.impl.subscriptions;
|
||||
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Maintain the information about which Topic a certain ClientID is subscribed and at which QoS
|
||||
*/
|
||||
public final class Subscription implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -3383457629635732794L;
|
||||
private final MqttQoS requestedQos; // max QoS acceptable
|
||||
final String clientId;
|
||||
final Topic topicFilter;
|
||||
private final boolean active;
|
||||
|
||||
public Subscription(String clientId, Topic topicFilter, MqttQoS requestedQos) {
|
||||
this.requestedQos = requestedQos;
|
||||
this.clientId = clientId;
|
||||
this.topicFilter = topicFilter;
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
public Subscription(Subscription orig) {
|
||||
this.requestedQos = orig.requestedQos;
|
||||
this.clientId = orig.clientId;
|
||||
this.topicFilter = orig.topicFilter;
|
||||
this.active = orig.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with undefined maximum QoS
|
||||
*
|
||||
* @param clientId id of client owning this subscription.
|
||||
* @param topicFilter the topic to subscribe to.
|
||||
* */
|
||||
public Subscription(String clientId, Topic topicFilter) {
|
||||
this.requestedQos = null;
|
||||
this.clientId = clientId;
|
||||
this.topicFilter = topicFilter;
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public MqttQoS getRequestedQos() {
|
||||
return requestedQos;
|
||||
}
|
||||
|
||||
public Topic getTopicFilter() {
|
||||
return topicFilter;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public boolean qosLessThan(Subscription sub) {
|
||||
return requestedQos.value() < sub.requestedQos.value();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
|
||||
Subscription that = (Subscription) o;
|
||||
|
||||
if (clientId != null ? !clientId.equals(that.clientId) : that.clientId != null)
|
||||
return false;
|
||||
return !(topicFilter != null ? !topicFilter.equals(that.topicFilter) : that.topicFilter != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = clientId != null ? clientId.hashCode() : 0;
|
||||
result = 31 * result + (topicFilter != null ? topicFilter.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"[filter:%s, cliID: %s, qos: %s, active: %s]",
|
||||
this.topicFilter,
|
||||
this.clientId,
|
||||
this.requestedQos,
|
||||
this.active);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Subscription clone() {
|
||||
try {
|
||||
return (Subscription) super.clone();
|
||||
} catch (CloneNotSupportedException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.spi.impl.subscriptions;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
class SubscriptionCounterVisitor implements CTrieSubscriptionDirectory.IVisitor<Integer> {
|
||||
|
||||
private AtomicInteger accumulator = new AtomicInteger(0);
|
||||
|
||||
@Override
|
||||
public void visit(CNode node, int deep) {
|
||||
accumulator.addAndGet(node.subscriptions.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getResult() {
|
||||
return accumulator.get();
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.spi.impl.subscriptions;
|
||||
|
||||
class TNode extends CNode {
|
||||
|
||||
@Override
|
||||
INode childOf(Token token) {
|
||||
throw new IllegalStateException("Can't be invoked on TNode");
|
||||
}
|
||||
|
||||
@Override
|
||||
CNode copy() {
|
||||
throw new IllegalStateException("Can't be invoked on TNode");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(INode newINode) {
|
||||
throw new IllegalStateException("Can't be invoked on TNode");
|
||||
}
|
||||
|
||||
@Override
|
||||
CNode addSubscription(Subscription newSubscription) {
|
||||
throw new IllegalStateException("Can't be invoked on TNode");
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean containsOnly(String clientId) {
|
||||
throw new IllegalStateException("Can't be invoked on TNode");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String clientId) {
|
||||
throw new IllegalStateException("Can't be invoked on TNode");
|
||||
}
|
||||
|
||||
@Override
|
||||
void removeSubscriptionsFor(String clientId) {
|
||||
throw new IllegalStateException("Can't be invoked on TNode");
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean anyChildrenMatch(Token token) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.spi.impl.subscriptions;
|
||||
|
||||
/**
|
||||
* Internal use only class.
|
||||
*/
|
||||
public class Token {
|
||||
|
||||
static final Token EMPTY = new Token("");
|
||||
static final Token MULTI = new Token("#");
|
||||
static final Token SINGLE = new Token("+");
|
||||
final String name;
|
||||
|
||||
protected Token(String s) {
|
||||
name = s;
|
||||
}
|
||||
|
||||
protected String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
protected boolean match(Token t) {
|
||||
if (MULTI.equals(t) || SINGLE.equals(t)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (MULTI.equals(this) || SINGLE.equals(this)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return equals(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = 7;
|
||||
hash = 29 * hash + (this.name != null ? this.name.hashCode() : 0);
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
final Token other = (Token) obj;
|
||||
if ((this.name == null) ? (other.name != null) : !this.name.equals(other.name)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.spi.impl.subscriptions;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Factory method
|
||||
*
|
||||
* @param s the topic string (es "/a/b").
|
||||
* @return the created Topic instance.
|
||||
* */
|
||||
public static Topic asTopic(String s) {
|
||||
return new Topic(s);
|
||||
}
|
||||
|
||||
public Topic(String topic) {
|
||||
this.topic = topic;
|
||||
}
|
||||
|
||||
Topic(List<Token> tokens) {
|
||||
this.tokens = tokens;
|
||||
List<String> strTokens = tokens.stream().map(Token::toString).collect(Collectors.toList());
|
||||
this.topic = String.join("/", strTokens);
|
||||
this.valid = true;
|
||||
}
|
||||
|
||||
public List<Token> getTokens() {
|
||||
if (tokens == null) {
|
||||
try {
|
||||
tokens = parseTopic(topic);
|
||||
valid = true;
|
||||
} catch (ParseException e) {
|
||||
valid = false;
|
||||
LOG.error("Error parsing the topic: {}, message: {}", topic, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private List<Token> parseTopic(String topic) throws ParseException {
|
||||
if (topic.length() == 0) {
|
||||
throw new ParseException("Bad format of topic, topic MUST be at least 1 character [MQTT-4.7.3-1] and " +
|
||||
"this was empty", 0);
|
||||
}
|
||||
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 Token headToken() {
|
||||
final List<Token> tokens = getTokens();
|
||||
if (tokens.isEmpty()) {
|
||||
//TODO UGLY use Optional
|
||||
return null;
|
||||
}
|
||||
return tokens.get(0);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
final List<Token> tokens = getTokens();
|
||||
return tokens == null || tokens.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a new Topic corresponding to this less than the head token
|
||||
* */
|
||||
public Topic exceptHeadToken() {
|
||||
List<Token> tokens = getTokens();
|
||||
if (tokens.isEmpty()) {
|
||||
return new Topic(Collections.emptyList());
|
||||
}
|
||||
List<Token> tokensCopy = new ArrayList<>(tokens);
|
||||
tokensCopy.remove(0);
|
||||
return new Topic(tokensCopy);
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
if (tokens == null)
|
||||
getTokens();
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if the 2 topics matching respecting the rules of MQTT Appendix A
|
||||
*
|
||||
* @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 (!Token.MULTI.equals(subToken) && !Token.SINGLE.equals(subToken)) {
|
||||
if (i >= msgTokens.size()) {
|
||||
return false;
|
||||
}
|
||||
Token msgToken = msgTokens.get(i);
|
||||
if (!msgToken.equals(subToken)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (Token.MULTI.equals(subToken)) {
|
||||
return true;
|
||||
}
|
||||
// if (Token.SINGLE.equals(subToken)) {
|
||||
// // skip a step forward
|
||||
// }
|
||||
}
|
||||
}
|
||||
return i == msgTokens.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return topic;
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user