Moved project package from org.eclipse.moquette to io.moquette

This commit is contained in:
andsel
2015-11-20 21:30:59 +01:00
parent f438ef7f49
commit 371c740321
175 changed files with 629 additions and 669 deletions
@@ -0,0 +1,109 @@
/*
* Copyright (c) 2012-2015 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.interception.InterceptHandler;
import io.moquette.interception.Interceptor;
import io.moquette.interception.messages.*;
import io.moquette.proto.messages.ConnectMessage;
import io.moquette.spi.impl.subscriptions.Subscription;
import io.moquette.proto.messages.PublishMessage;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* An interceptor that execute the interception tasks asynchronously.
*
* @author Wagner Macedo
*/
final class BrokerInterceptor implements Interceptor {
private final List<InterceptHandler> handlers;
private final ExecutorService executor;
BrokerInterceptor(List<InterceptHandler> handlers) {
this.handlers = handlers;
executor = Executors.newFixedThreadPool(1);
}
/**
* Shutdown graciously the executor service
*/
void stop() {
executor.shutdown();
}
@Override
public void notifyClientConnected(final ConnectMessage msg) {
for (final InterceptHandler handler : this.handlers) {
executor.execute(new Runnable() {
@Override
public void run() {
handler.onConnect(new InterceptConnectMessage(msg));
}
});
}
}
@Override
public void notifyClientDisconnected(final String clientID) {
for (final InterceptHandler handler : this.handlers) {
executor.execute(new Runnable() {
@Override
public void run() {
handler.onDisconnect(new InterceptDisconnectMessage(clientID));
}
});
}
}
@Override
public void notifyTopicPublished(final PublishMessage msg, final String clientID) {
for (final InterceptHandler handler : this.handlers) {
executor.execute(new Runnable() {
@Override
public void run() {
handler.onPublish(new InterceptPublishMessage(msg, clientID));
}
});
}
}
@Override
public void notifyTopicSubscribed(final Subscription sub) {
for (final InterceptHandler handler : this.handlers) {
executor.execute(new Runnable() {
@Override
public void run() {
handler.onSubscribe(new InterceptSubscribeMessage(sub));
}
});
}
}
@Override
public void notifyTopicUnsubscribed(final String topic, final String clientID) {
for (final InterceptHandler handler : this.handlers) {
executor.execute(new Runnable() {
@Override
public void run() {
handler.onUnsubscribe(new InterceptUnsubscribeMessage(topic, clientID));
}
});
}
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2012-2015 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 java.nio.ByteBuffer;
/**
*
* @author andrea
*/
class DebugUtils {
static String payload2Str(ByteBuffer content) {
byte[] b = new byte[content.remaining()];
content.mark();
content.get(b);
content.reset();
return new String(b);
}
}
@@ -0,0 +1,651 @@
/*
* Copyright (c) 2012-2015 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 java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import io.moquette.server.ConnectionDescriptor;
import io.moquette.server.ServerChannel;
import io.moquette.server.netty.NettyChannel;
import io.moquette.spi.ClientSession;
import io.moquette.spi.IMatchingCondition;
import io.moquette.spi.IMessagesStore;
import io.moquette.spi.ISessionsStore;
import io.moquette.spi.impl.security.IAuthenticator;
import io.moquette.spi.impl.security.IAuthorizator;
import io.moquette.spi.impl.subscriptions.SubscriptionsStore;
import io.moquette.spi.impl.subscriptions.Subscription;
import static io.moquette.parser.netty.Utils.VERSION_3_1;
import static io.moquette.parser.netty.Utils.VERSION_3_1_1;
import io.moquette.proto.messages.AbstractMessage;
import io.moquette.proto.messages.AbstractMessage.QOSType;
import io.moquette.proto.messages.ConnAckMessage;
import io.moquette.proto.messages.ConnectMessage;
import io.moquette.proto.messages.PubAckMessage;
import io.moquette.proto.messages.PubCompMessage;
import io.moquette.proto.messages.PubRecMessage;
import io.moquette.proto.messages.PubRelMessage;
import io.moquette.proto.messages.PublishMessage;
import io.moquette.proto.messages.SubAckMessage;
import io.moquette.proto.messages.SubscribeMessage;
import io.moquette.proto.messages.UnsubAckMessage;
import io.moquette.proto.messages.UnsubscribeMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class responsible to handle the logic of MQTT protocol it's the director of
* the protocol execution.
*
* Used by the front facing class SimpleMessaging.
*
* @author andrea
*/
public class ProtocolProcessor {
static final class WillMessage {
private final String topic;
private final ByteBuffer payload;
private final boolean retained;
private final QOSType qos;
public WillMessage(String topic, ByteBuffer payload, boolean retained, QOSType qos) {
this.topic = topic;
this.payload = payload;
this.retained = retained;
this.qos = qos;
}
public String getTopic() {
return topic;
}
public ByteBuffer getPayload() {
return payload;
}
public boolean isRetained() {
return retained;
}
public QOSType getQos() {
return qos;
}
}
private static final Logger LOG = LoggerFactory.getLogger(ProtocolProcessor.class);
private Map<String, ConnectionDescriptor> m_clientIDs;
private SubscriptionsStore subscriptions;
private boolean allowAnonymous;
private IAuthorizator m_authorizator;
private IMessagesStore m_messagesStore;
private ISessionsStore m_sessionsStore;
private IAuthenticator m_authenticator;
private BrokerInterceptor m_interceptor;
//maps clientID to Will testament, if specified on CONNECT
private Map<String, WillMessage> m_willStore = new ConcurrentHashMap<>();
ProtocolProcessor() {}
/**
* @param subscriptions the subscription store where are stored all the existing
* clients subscriptions.
* @param storageService the persistent store to use for save/load of messages
* for QoS1 and QoS2 handling.
* @param sessionsStore the clients sessions store, used to persist subscriptions.
* @param authenticator the authenticator used in connect messages.
* @param allowAnonymous true connection to clients without credentials.
* @param authorizator used to apply ACL policies to publishes and subscriptions.
* @param interceptor to notify events to an intercept handler
*/
void init(SubscriptionsStore subscriptions, IMessagesStore storageService,
ISessionsStore sessionsStore,
IAuthenticator authenticator,
boolean allowAnonymous, IAuthorizator authorizator, BrokerInterceptor interceptor) {
this.m_clientIDs = new ConcurrentHashMap<>();
this.m_interceptor = interceptor;
this.subscriptions = subscriptions;
this.allowAnonymous = allowAnonymous;
m_authorizator = authorizator;
LOG.debug("subscription tree on init {}", subscriptions.dumpTree());
m_authenticator = authenticator;
m_messagesStore = storageService;
m_sessionsStore = sessionsStore;
}
public void processConnect(ServerChannel session, ConnectMessage msg) {
LOG.debug("CONNECT for client <{}>", msg.getClientID());
if (msg.getProtocolVersion() != VERSION_3_1 && msg.getProtocolVersion() != VERSION_3_1_1) {
ConnAckMessage badProto = new ConnAckMessage();
badProto.setReturnCode(ConnAckMessage.UNNACEPTABLE_PROTOCOL_VERSION);
LOG.warn("processConnect sent bad proto ConnAck");
session.write(badProto);
session.close(false);
return;
}
if (msg.getClientID() == null || msg.getClientID().length() == 0) {
ConnAckMessage okResp = new ConnAckMessage();
okResp.setReturnCode(ConnAckMessage.IDENTIFIER_REJECTED);
session.write(okResp);
m_interceptor.notifyClientConnected(msg);
return;
}
//handle user authentication
if (msg.isUserFlag()) {
byte[] pwd = null;
if (msg.isPasswordFlag()) {
pwd = msg.getPassword();
} else if (!this.allowAnonymous) {
failedCredentials(session);
return;
}
if (!m_authenticator.checkValid(msg.getUsername(), pwd)) {
failedCredentials(session);
return;
}
session.setAttribute(NettyChannel.ATTR_KEY_USERNAME, msg.getUsername());
} else if (!this.allowAnonymous) {
failedCredentials(session);
return;
}
//if an old client with the same ID already exists close its session.
if (m_clientIDs.containsKey(msg.getClientID())) {
LOG.info("Found an existing connection with same client ID <{}>, forcing to close", msg.getClientID());
//clean the subscriptions if the old used a cleanSession = true
ServerChannel oldSession = m_clientIDs.get(msg.getClientID()).session;
ClientSession oldClientSession = m_sessionsStore.sessionForClient(msg.getClientID());
oldClientSession.disconnect();
oldSession.setAttribute(NettyChannel.ATTR_KEY_SESSION_STOLEN, true);
oldSession.close(false);
LOG.debug("Existing connection with same client ID <{}>, forced to close", msg.getClientID());
}
ConnectionDescriptor connDescr = new ConnectionDescriptor(msg.getClientID(), session, msg.isCleanSession());
m_clientIDs.put(msg.getClientID(), connDescr);
int keepAlive = msg.getKeepAlive();
LOG.debug("Connect with keepAlive {} s", keepAlive);
session.setAttribute(NettyChannel.ATTR_KEY_KEEPALIVE, keepAlive);
session.setAttribute(NettyChannel.ATTR_KEY_CLEANSESSION, msg.isCleanSession());
//used to track the client in the subscription and publishing phases.
session.setAttribute(NettyChannel.ATTR_KEY_CLIENTID, msg.getClientID());
LOG.debug("Connect create session <{}>", session);
session.setIdleTime(Math.round(keepAlive * 1.5f));
//Handle will flag
if (msg.isWillFlag()) {
AbstractMessage.QOSType willQos = AbstractMessage.QOSType.valueOf(msg.getWillQos());
byte[] willPayload = msg.getWillMessage();
ByteBuffer bb = (ByteBuffer) ByteBuffer.allocate(willPayload.length).put(willPayload).flip();
//save the will testament in the clientID store
WillMessage will = new WillMessage(msg.getWillTopic(), bb, msg.isWillRetain(),willQos );
m_willStore.put(msg.getClientID(), will);
}
ConnAckMessage okResp = new ConnAckMessage();
okResp.setReturnCode(ConnAckMessage.CONNECTION_ACCEPTED);
ClientSession clientSession = m_sessionsStore.sessionForClient(msg.getClientID());
boolean isSessionAlreadyStored = clientSession != null;
if (!msg.isCleanSession() && isSessionAlreadyStored) {
okResp.setSessionPresent(true);
}
session.write(okResp);
m_interceptor.notifyClientConnected(msg);
if (!isSessionAlreadyStored) {
LOG.info("Create persistent session for clientID <{}>", msg.getClientID());
clientSession = m_sessionsStore.createNewSession(msg.getClientID(), msg.isCleanSession());
}
clientSession.activate();
if (msg.isCleanSession()) {
clientSession.cleanSession();
}
LOG.info("Connected client ID <{}> with clean session {}", msg.getClientID(), msg.isCleanSession());
if (!msg.isCleanSession()) {
//force the republish of stored QoS1 and QoS2
republishStoredInSession(clientSession);
}
LOG.info("CONNECT processed");
}
private void failedCredentials(ServerChannel session) {
ConnAckMessage okResp = new ConnAckMessage();
okResp.setReturnCode(ConnAckMessage.BAD_USERNAME_OR_PASSWORD);
session.write(okResp);
session.close(false);
}
/**
* Republish QoS1 and QoS2 messages stored into the session for the clientID.
* */
private void republishStoredInSession(ClientSession clientSession) {
LOG.trace("republishStoredInSession for client <{}>", clientSession);
List<IMessagesStore.StoredMessage> publishedEvents = clientSession.storedMessages();
if (publishedEvents.isEmpty()) {
LOG.info("No stored messages for client <{}>", clientSession.clientID);
return;
}
LOG.info("republishing stored messages to client <{}>", clientSession.clientID);
for (IMessagesStore.StoredMessage pubEvt : publishedEvents) {
//TODO put in flight zone
directSend(clientSession, pubEvt.getTopic(), pubEvt.getQos(),
pubEvt.getMessage(), false, pubEvt.getMessageID());
clientSession.removeEnqueued(pubEvt.getGuid());
}
}
public void processPubAck(ServerChannel session, PubAckMessage msg) {
String clientID = (String) session.getAttribute(NettyChannel.ATTR_KEY_CLIENTID);
int messageID = msg.getMessageID();
//Remove the message from message store
ClientSession targetSession = m_sessionsStore.sessionForClient(clientID);
targetSession.inFlightAcknowledged(messageID);
}
private static IMessagesStore.StoredMessage asStoredMessage(PublishMessage msg) {
IMessagesStore.StoredMessage stored = new IMessagesStore.StoredMessage(msg.getPayload().array(), msg.getQos(), msg.getTopicName());
stored.setRetained(msg.isRetainFlag());
stored.setMessageID(msg.getMessageID());
return stored;
}
private static IMessagesStore.StoredMessage asStoredMessage(WillMessage will) {
IMessagesStore.StoredMessage pub = new IMessagesStore.StoredMessage(will.getPayload().array(), will.getQos(), will.getTopic());
pub.setRetained(will.isRetained());
return pub;
}
public void processPublish(ServerChannel session, PublishMessage msg) {
LOG.trace("PUB --PUBLISH--> SRV executePublish invoked with {}", msg);
String clientID = (String) session.getAttribute(NettyChannel.ATTR_KEY_CLIENTID);
final String topic = msg.getTopicName();
//check if the topic can be wrote
String user = (String) session.getAttribute(NettyChannel.ATTR_KEY_USERNAME);
if (!m_authorizator.canWrite(topic, user, clientID)) {
LOG.debug("topic {} doesn't have write credentials", topic);
return;
}
final AbstractMessage.QOSType qos = msg.getQos();
boolean retain = msg.isRetainFlag();
final Integer messageID = msg.getMessageID();
LOG.info("PUBLISH from clientID <{}> on topic <{}> with QoS {}", clientID, topic, qos);
String guid = null;
IMessagesStore.StoredMessage toStoreMsg = asStoredMessage(msg);
toStoreMsg.setClientID(clientID);
if (qos == AbstractMessage.QOSType.MOST_ONE) { //QoS0
forward2Subscribers(toStoreMsg);
} else if (qos == AbstractMessage.QOSType.LEAST_ONE) { //QoS1
forward2Subscribers(toStoreMsg);
sendPubAck(clientID, messageID);
LOG.debug("replying with PubAck to MSG ID {}", messageID);
} else if (qos == AbstractMessage.QOSType.EXACTLY_ONCE) { //QoS2
guid = m_messagesStore.storePublishForFuture(toStoreMsg);
sendPubRec(clientID, messageID);
//Next the client will send us a pub rel
//NB publish to subscribers for QoS 2 happen upon PUBREL from publisher
}
if (retain) {
if (qos == AbstractMessage.QOSType.MOST_ONE) {
//QoS == 0 && retain => clean old retained
m_messagesStore.cleanRetained(topic);
} else {
if (!msg.getPayload().hasRemaining()) {
m_messagesStore.cleanRetained(topic);
} else {
if (guid == null) {
//before wasn't stored
guid = m_messagesStore.storePublishForFuture(toStoreMsg);
}
m_messagesStore.storeRetained(topic, guid);
}
}
}
m_interceptor.notifyTopicPublished(msg, clientID);
}
/**
* Specialized version to publish will testament message.
*/
private void forwardPublishWill(WillMessage will, String clientID) {
//it has just to publish the message downstream to the subscribers
//NB it's a will publish, it needs a PacketIdentifier for this conn, default to 1
Integer messageId = null;
if (will.getQos() != AbstractMessage.QOSType.MOST_ONE) {
messageId = m_messagesStore.nextPacketID(clientID);
}
IMessagesStore.StoredMessage tobeStored = asStoredMessage(will);
tobeStored.setClientID(clientID);
tobeStored.setMessageID(messageId);
forward2Subscribers(tobeStored);
}
/**
* Flood the subscribers with the message to notify. MessageID is optional and should only used for QoS 1 and 2
* */
//TODO rename to route2Subscribers
void forward2Subscribers(IMessagesStore.StoredMessage pubEvt) {
final String topic = pubEvt.getTopic();
final AbstractMessage.QOSType publishingQos = pubEvt.getQos();
final ByteBuffer origMessage = pubEvt.getMessage();
LOG.debug("forward2Subscribers republishing to existing subscribers that matches the topic {}", topic);
if (LOG.isDebugEnabled()) {
LOG.debug("content <{}>", DebugUtils.payload2Str(origMessage));
LOG.debug("subscription tree {}", subscriptions.dumpTree());
}
//if QoS 1 or 2 store the message
String guid = null;
if (publishingQos == QOSType.EXACTLY_ONCE || publishingQos == QOSType.LEAST_ONE) {
guid = m_messagesStore.storePublishForFuture(pubEvt);
}
for (final Subscription sub : subscriptions.matches(topic)) {
AbstractMessage.QOSType qos = publishingQos;
if (qos.byteValue() > sub.getRequestedQos().byteValue()) {
qos = sub.getRequestedQos();
}
ClientSession targetSession = m_sessionsStore.sessionForClient(sub.getClientId());
LOG.debug("Broker republishing to client <{}> topic <{}> qos <{}>, active {}",
sub.getClientId(), sub.getTopicFilter(), qos, targetSession.isActive());
ByteBuffer message = origMessage.duplicate();
if (qos == AbstractMessage.QOSType.MOST_ONE && targetSession.isActive()) {
//QoS 0
directSend(targetSession, topic, qos, message, false, null);
} else {
//QoS 1 or 2
//if the target subscription is not clean session and is not connected => store it
if (!sub.isCleanSession() && !targetSession.isActive()) {
//store the message in targetSession queue to deliver
targetSession.enqueueToDeliver(guid);
} else {
//publish
if (targetSession.isActive()) {
int messageId = targetSession.nextPacketId();
targetSession.inFlightAckWaiting(guid, messageId);
directSend(targetSession, topic, qos, message, false, messageId);
}
}
}
}
}
protected void directSend(ClientSession clientsession, String topic, AbstractMessage.QOSType qos, ByteBuffer message, boolean retained, Integer messageID) {
String clientId = clientsession.clientID;
LOG.debug("directSend invoked clientId <{}> on topic <{}> QoS {} retained {} messageID {}", clientId, topic, qos, retained, messageID);
PublishMessage pubMessage = new PublishMessage();
pubMessage.setRetainFlag(retained);
pubMessage.setTopicName(topic);
pubMessage.setQos(qos);
pubMessage.setPayload(message);
LOG.info("send publish message to <{}> on topic <{}>", clientId, topic);
if (LOG.isDebugEnabled()) {
LOG.debug("content <{}>", DebugUtils.payload2Str(message));
}
//set the PacketIdentifier only for QoS > 0
if (pubMessage.getQos() != AbstractMessage.QOSType.MOST_ONE) {
pubMessage.setMessageID(messageID);
} else {
if (messageID != null) {
throw new RuntimeException("Internal bad error, trying to forwardPublish a QoS 0 message with PacketIdentifier: " + messageID);
}
}
if (m_clientIDs == null) {
throw new RuntimeException("Internal bad error, found m_clientIDs to null while it should be initialized, somewhere it's overwritten!!");
}
LOG.debug("clientIDs are {}", m_clientIDs);
if (m_clientIDs.get(clientId) == null) {
//TODO while we were publishing to the target client, that client disconnected,
// could happen is not an error HANDLE IT
throw new RuntimeException(String.format("Can't find a ConnectionDescriptor for client <%s> in cache <%s>", clientId, m_clientIDs));
}
ServerChannel session = m_clientIDs.get(clientId).session;
LOG.debug("Session for clientId {} is {}", clientId, session);
String user = (String) session.getAttribute(NettyChannel.ATTR_KEY_USERNAME);
if (!m_authorizator.canRead(topic, user, clientId)) {
LOG.debug("topic {} doesn't have read credentials", topic);
return;
}
session.write(pubMessage);
}
private void sendPubRec(String clientID, int messageID) {
LOG.trace("PUB <--PUBREC-- SRV sendPubRec invoked for clientID {} with messageID {}", clientID, messageID);
PubRecMessage pubRecMessage = new PubRecMessage();
pubRecMessage.setMessageID(messageID);
m_clientIDs.get(clientID).session.write(pubRecMessage);
}
private void sendPubAck(String clientId, int messageID) {
LOG.trace("sendPubAck invoked");
PubAckMessage pubAckMessage = new PubAckMessage();
pubAckMessage.setMessageID(messageID);
try {
if (m_clientIDs == null) {
throw new RuntimeException("Internal bad error, found m_clientIDs to null while it should be initialized, somewhere it's overwritten!!");
}
LOG.debug("clientIDs are {}", m_clientIDs);
if (m_clientIDs.get(clientId) == null) {
throw new RuntimeException(String.format("Can't find a ConnectionDescriptor for client %s in cache %s", clientId, m_clientIDs));
}
m_clientIDs.get(clientId).session.write(pubAckMessage);
} catch(Throwable t) {
LOG.error(null, t);
}
}
/**
* Second phase of a publish QoS2 protocol, sent by publisher to the broker. Search the stored message and publish
* to all interested subscribers.
* */
public void processPubRel(ServerChannel session, PubRelMessage msg) {
String clientID = (String) session.getAttribute(NettyChannel.ATTR_KEY_CLIENTID);
int messageID = msg.getMessageID();
LOG.debug("PUB --PUBREL--> SRV processPubRel invoked for clientID {} ad messageID {}", clientID, messageID);
ClientSession targetSession = m_sessionsStore.sessionForClient(clientID);
IMessagesStore.StoredMessage evt = targetSession.storedMessage(messageID);
forward2Subscribers(evt);
if (evt.isRetained()) {
final String topic = evt.getTopic();
if (!evt.getMessage().hasRemaining()) {
m_messagesStore.cleanRetained(topic);
} else {
m_messagesStore.storeRetained(topic, evt.getGuid());
}
}
sendPubComp(clientID, messageID);
}
private void sendPubComp(String clientID, int messageID) {
LOG.debug("PUB <--PUBCOMP-- SRV sendPubComp invoked for clientID {} ad messageID {}", clientID, messageID);
PubCompMessage pubCompMessage = new PubCompMessage();
pubCompMessage.setMessageID(messageID);
m_clientIDs.get(clientID).session.write(pubCompMessage);
}
public void processPubRec(ServerChannel session, PubRecMessage msg) {
String clientID = (String) session.getAttribute(NettyChannel.ATTR_KEY_CLIENTID);
int messageID = msg.getMessageID();
ClientSession targetSession = m_sessionsStore.sessionForClient(clientID);
//remove from the inflight and move to the QoS2 second phase queue
targetSession.inFlightAcknowledged(messageID);
targetSession.secondPhaseAckWaiting(messageID);
//once received a PUBREC reply with a PUBREL(messageID)
LOG.debug("\t\tSRV <--PUBREC-- SUB processPubRec invoked for clientID {} ad messageID {}", clientID, messageID);
PubRelMessage pubRelMessage = new PubRelMessage();
pubRelMessage.setMessageID(messageID);
pubRelMessage.setQos(AbstractMessage.QOSType.LEAST_ONE);
session.write(pubRelMessage);
}
public void processPubComp(ServerChannel session, PubCompMessage msg) {
String clientID = (String) session.getAttribute(NettyChannel.ATTR_KEY_CLIENTID);
int messageID = msg.getMessageID();
LOG.debug("\t\tSRV <--PUBCOMP-- SUB processPubComp invoked for clientID {} ad messageID {}", clientID, messageID);
//once received the PUBCOMP then remove the message from the temp memory
ClientSession targetSession = m_sessionsStore.sessionForClient(clientID);
targetSession.secondPhaseAcknowledged(messageID);
}
public void processDisconnect(ServerChannel session) throws InterruptedException {
String clientID = (String) session.getAttribute(NettyChannel.ATTR_KEY_CLIENTID);
boolean cleanSession = (Boolean) session.getAttribute(NettyChannel.ATTR_KEY_CLEANSESSION);
LOG.info("DISCONNECT client <{}> with clean session {}", clientID, cleanSession);
ClientSession clientSession = m_sessionsStore.sessionForClient(clientID);
clientSession.disconnect();
m_clientIDs.remove(clientID);
session.close(true);
//cleanup the will store
m_willStore.remove(clientID);
m_interceptor.notifyClientDisconnected(clientID);
LOG.info("DISCONNECT client <{}> finished", clientID, cleanSession);
}
public void processConnectionLost(String clientID, boolean sessionStolen) {
//If already removed a disconnect message was already processed for this clientID
if (sessionStolen && m_clientIDs.remove(clientID) != null) {
//de-activate the subscriptions for this ClientID
ClientSession clientSession = m_sessionsStore.sessionForClient(clientID);
clientSession.deactivate();
LOG.info("Lost connection with client <{}>", clientID);
}
//publish the Will message (if any) for the clientID
if (!sessionStolen && m_willStore.containsKey(clientID)) {
WillMessage will = m_willStore.get(clientID);
forwardPublishWill(will, clientID);
m_willStore.remove(clientID);
}
}
/**
* Remove the clientID from topic subscription, if not previously subscribed,
* doesn't reply any error
*/
public void processUnsubscribe(ServerChannel session, UnsubscribeMessage msg) {
List<String> topics = msg.topicFilters();
int messageID = msg.getMessageID();
String clientID = (String) session.getAttribute(NettyChannel.ATTR_KEY_CLIENTID);
LOG.debug("UNSUBSCRIBE subscription on topics {} for clientID <{}>", topics, clientID);
ClientSession clientSession = m_sessionsStore.sessionForClient(clientID);
for (String topic : topics) {
boolean validTopic = SubscriptionsStore.validate(topic);
if (!validTopic) {
//close the connection, not valid topicFilter is a protocol violation
session.close(true);
LOG.warn("UNSUBSCRIBE found an invalid topic filter <{}> for clientID <{}>", topic, clientID);
return;
}
subscriptions.removeSubscription(topic, clientID);
clientSession.unsubscribeFrom(topic);
m_interceptor.notifyTopicUnsubscribed(topic, clientID);
}
//ack the client
UnsubAckMessage ackMessage = new UnsubAckMessage();
ackMessage.setMessageID(messageID);
LOG.info("replying with UnsubAck to MSG ID {}", messageID);
session.write(ackMessage);
}
public void processSubscribe(ServerChannel session, SubscribeMessage msg) {
String clientID = (String) session.getAttribute(NettyChannel.ATTR_KEY_CLIENTID);
boolean cleanSession = (Boolean) session.getAttribute(NettyChannel.ATTR_KEY_CLEANSESSION);
LOG.debug("SUBSCRIBE client <{}> packetID {}", clientID, msg.getMessageID());
ClientSession clientSession = m_sessionsStore.sessionForClient(clientID);
//ack the client
SubAckMessage ackMessage = new SubAckMessage();
ackMessage.setMessageID(msg.getMessageID());
List<Subscription> newSubscriptions = new ArrayList<>();
for (SubscribeMessage.Couple req : msg.subscriptions()) {
AbstractMessage.QOSType qos = AbstractMessage.QOSType.valueOf(req.getQos());
Subscription newSubscription = new Subscription(clientID, req.getTopicFilter(), qos, cleanSession);
//boolean valid = subscribeSingleTopic(newSubscription, req.getTopicFilter());
boolean valid = clientSession.subscribe(req.getTopicFilter(), newSubscription);
ackMessage.addType(valid ? qos : AbstractMessage.QOSType.FAILURE);
if (valid) {
newSubscriptions.add(newSubscription);
}
}
//save session, persist subscriptions from session
LOG.debug("SUBACK for packetID {}", msg.getMessageID());
session.write(ackMessage);
//fire the publish
for(Subscription subscription : newSubscriptions) {
subscribeSingleTopic(subscription, subscription.getTopicFilter());
}
}
private boolean subscribeSingleTopic(Subscription newSubscription, final String topic) {
subscriptions.add(newSubscription);
//scans retained messages to be published to the new subscription
//TODO this is ugly, it does a linear scan on potential big dataset
Collection<IMessagesStore.StoredMessage> messages = m_messagesStore.searchMatching(new IMatchingCondition() {
public boolean match(String key) {
return SubscriptionsStore.matchTopics(key, topic);
}
});
ClientSession targetSession = m_sessionsStore.sessionForClient(newSubscription.getClientId());
for (IMessagesStore.StoredMessage storedMsg : messages) {
//fire the as retained the message
LOG.debug("send publish message for topic {}", topic);
//forwardPublishQoS0(newSubscription.getClientId(), storedMsg.getTopic(), storedMsg.getQos(), storedMsg.getPayload(), true);
Integer packetID = storedMsg.getQos() == QOSType.MOST_ONE ? null :
m_messagesStore.nextPacketID(newSubscription.getClientId());
directSend(targetSession, storedMsg.getTopic(), storedMsg.getQos(), storedMsg.getPayload(), true, packetID);
}
//notify the Observables
m_interceptor.notifyTopicSubscribed(newSubscription);
return true;
}
}
@@ -0,0 +1,178 @@
/*
* Copyright (c) 2012-2015 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.spi.IMessagesStore;
import io.moquette.commons.Constants;
import io.moquette.interception.InterceptHandler;
import io.moquette.server.config.IConfig;
import io.moquette.spi.ISessionsStore;
import io.moquette.spi.impl.security.*;
import io.moquette.spi.impl.subscriptions.SubscriptionsStore;
import io.moquette.spi.persistence.MapDBPersistentStore;
import static io.moquette.commons.Constants.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
/**
*
* Singleton class that orchestrate the execution of the protocol.
*
* It's main responsibility is instantiate the ProtocolProcessor.
*
* @author andrea
*/
public class SimpleMessaging {
private static final Logger LOG = LoggerFactory.getLogger(SimpleMessaging.class);
private SubscriptionsStore subscriptions;
private MapDBPersistentStore m_mapStorage;
private BrokerInterceptor m_interceptor;
private static SimpleMessaging INSTANCE;
private final ProtocolProcessor m_processor = new ProtocolProcessor();
private SimpleMessaging() {
}
public static SimpleMessaging getInstance() {
if (INSTANCE == null) {
INSTANCE = new SimpleMessaging();
}
return INSTANCE;
}
public ProtocolProcessor init(IConfig props) {
subscriptions = new SubscriptionsStore();
//TODO use a property to select the storage path
m_mapStorage = new MapDBPersistentStore(props.getProperty(PERSISTENT_STORE_PROPERTY_NAME, ""));
m_mapStorage.initStore();
IMessagesStore messagesStore = m_mapStorage.messagesStore();
ISessionsStore sessionsStore = m_mapStorage.sessionsStore(messagesStore);
List<InterceptHandler> observers = new ArrayList<>();
String interceptorClassName = props.getProperty("intercept.handler");
if (interceptorClassName != null && !interceptorClassName.isEmpty()) {
try {
InterceptHandler handler = Class.forName(interceptorClassName).asSubclass(InterceptHandler.class).newInstance();
observers.add(handler);
} catch (Throwable ex) {
LOG.error("Can't load the intercept handler {}", ex);
}
}
m_interceptor = new BrokerInterceptor(observers);
subscriptions.init(sessionsStore);
String configPath = System.getProperty("moquette.path", null);
String authenticatorClassName = props.getProperty(Constants.AUTHENTICATOR_CLASS_NAME, "");
IAuthenticator authenticator = null;
if (!authenticatorClassName.isEmpty()) {
authenticator = (IAuthenticator)loadClass(authenticatorClassName, IAuthenticator.class);
LOG.info("Loaded custom authenticator {}", authenticatorClassName);
}
if (authenticator == null) {
String passwdPath = props.getProperty(PASSWORD_FILE_PROPERTY_NAME, "");
if (passwdPath.isEmpty()) {
authenticator = new AcceptAllAuthenticator();
} else {
authenticator = new FileAuthenticator(configPath, passwdPath);
}
}
IAuthorizator authorizator = null;
String authorizatorClassName = props.getProperty(Constants.AUTHORIZATOR_CLASS_NAME, "");
if (!authorizatorClassName.isEmpty()) {
authorizator = (IAuthorizator)loadClass(authorizatorClassName, IAuthorizator.class);
LOG.info("Loaded custom authorizator {}", authorizatorClassName);
}
if (authorizator == null) {
String aclFilePath = props.getProperty(ACL_FILE_PROPERTY_NAME, "");
if (aclFilePath != null && !aclFilePath.isEmpty()) {
authorizator = new DenyAllAuthorizator();
File aclFile = new File(configPath, aclFilePath);
try {
authorizator = ACLFileParser.parse(aclFile);
} catch (ParseException pex) {
LOG.error(String.format("Format error in parsing acl file %s", aclFile), pex);
}
LOG.info("Using acl file defined at path {}", aclFilePath);
} else {
authorizator = new PermitAllAuthorizator();
LOG.info("Starting without ACL definition");
}
}
boolean allowAnonymous = Boolean.parseBoolean(props.getProperty(ALLOW_ANONYMOUS_PROPERTY_NAME, "true"));
m_processor.init(subscriptions, messagesStore, sessionsStore, authenticator, allowAnonymous, authorizator, m_interceptor);
return m_processor;
}
private Object loadClass(String className, Class<?> cls) {
Object instance = null;
try {
Class<?> clazz = Class.forName(className);
// check if method getInstance exists
Method method = clazz.getMethod("getInstance", new Class[] {});
try {
instance = method.invoke(null, new Object[] {});
} catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException ex) {
LOG.error(null, ex);
throw new RuntimeException("Cannot call method "+ className +".getInstance", ex);
}
}
catch (NoSuchMethodException nsmex) {
try {
instance = this.getClass().getClassLoader()
.loadClass(className)
.asSubclass(cls)
.newInstance();
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException ex) {
LOG.error(null, ex);
throw new RuntimeException("Cannot load custom authenticator class " + className, ex);
}
} catch (ClassNotFoundException ex) {
LOG.error(null, ex);
throw new RuntimeException("Class " + className + " not found", ex);
} catch (SecurityException ex) {
LOG.error(null, ex);
throw new RuntimeException("Cannot call method "+ className +".getInstance", ex);
}
return instance;
}
public void shutdown() {
this.m_mapStorage.close();
}
}
@@ -0,0 +1,16 @@
package io.moquette.spi.impl;
import java.util.Map;
/**
* Utility static methods, like Map get with default value, or elvis operator.
*/
public 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;
}
}
@@ -0,0 +1,96 @@
/*
* Copyright (c) 2012-2015 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.*;
import java.text.ParseException;
/**
* 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>"
*
* @author andrea
* */
public class ACLFileParser {
private static final Logger LOG = LoggerFactory.getLogger(ACLFileParser.class);
/**
* Parse the configuration from file.
*/
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 {
FileReader reader = new FileReader(file);
return parse(reader);
} catch (FileNotFoundException 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
*
* @throws java.text.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();
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;
}
collector.parse(line);
}
}
} catch (IOException ex) {
throw new ParseException("Failed to read", 1);
}
return collector;
}
}
@@ -0,0 +1,10 @@
package io.moquette.spi.impl.security;
/**
* Created by andrea on 8/23/14.
*/
public class AcceptAllAuthenticator implements IAuthenticator {
public boolean checkValid(String username, byte[] password) {
return true;
}
}
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2012-2015 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;
/**
* Carries the read/write authorization to topics for the users.
*
* @author andrea
*/
public class Authorization {
protected final String topic;
protected final Permission permission;
/**
* Access rights
* */
enum Permission {
READ, WRITE, READWRITE
}
Authorization(String topic) {
this(topic, Permission.READWRITE);
}
Authorization(String 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;
}
}
@@ -0,0 +1,165 @@
/*
* Copyright (c) 2012-2015 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.SubscriptionsStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.
*
* @author andrea
*/
class AuthorizationsCollector implements IAuthorizator {
private static final Logger LOG = LoggerFactory.getLogger(AuthorizationsCollector.class);
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 = false;
private boolean m_parsingPatternSpecificSection = false;
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());
}
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
String 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];
return new Authorization(topic);
}
@Override
public boolean canWrite(String topic, String user, String client) {
return canDoOperation(topic, Authorization.Permission.WRITE, user, client);
}
@Override
public boolean canRead(String 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) {
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);
if (auth.grant(permission)) {
if (SubscriptionsStore.matchTopics(topic, 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, String topic, Authorization.Permission permission) {
for (Authorization auth : auths) {
if (auth.grant(permission)) {
if (SubscriptionsStore.matchTopics(topic, auth.topic)) {
return true;
}
}
}
return false;
}
private boolean isNotEmpty(String client) {
return client != null && !client.isEmpty();
}
public boolean isEmpty() {
return m_globalAuthorizations.isEmpty();
}
}
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2012-2015 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;
/**
* @author andrea
*/
public class DenyAllAuthorizator implements IAuthorizator {
@Override
public boolean canWrite(String topic, String user, String client) {
return true;
}
@Override
public boolean canRead(String topic, String user, String client) {
return true;
}
}
@@ -0,0 +1,124 @@
/*
* Copyright (c) 2012-2015 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.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
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 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.
*
* @author andrea
*/
public class FileAuthenticator implements IAuthenticator {
private static final Logger LOG = LoggerFactory.getLogger(FileAuthenticator.class);
private Map<String, String> m_identities = new HashMap<>();
private MessageDigest m_digest;
public FileAuthenticator(String parent, String filePath) {
File file = new File(parent, filePath);
LOG.info("Loading password file: " + file);
if (file.isDirectory()) {
LOG.warn(String.format("Bad file reference %s is a directory", file));
return;
}
try {
this.m_digest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException nsaex) {
LOG.error("Can't find SHA-256 for password encoding", nsaex);
throw new RuntimeException(nsaex);
}
try {
FileReader reader = new FileReader(file);
parse(reader);
} catch (FileNotFoundException fex) {
LOG.warn(String.format("Parsing not existing file %s", file), fex);
} catch (ParseException pex) {
LOG.warn(String.format("Format error in parsing password file %s", file), 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);
}
}
public boolean checkValid(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;
}
m_digest.update(password);
byte[] digest = m_digest.digest();
String encodedPasswd = new String(Hex.encodeHex(digest));
return foundPwq.equals(encodedPasswd);
}
}
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2012-2015 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;
/**
* username and password checker
*
* @author andrea
*/
public interface IAuthenticator {
boolean checkValid(String username, byte[] password);
}
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2012-2015 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;
/**
* ACL checker.
*
* Create an authorizator that matches topic names with same grammar of subscriptions.
* The # is always a terminator and its the multilevel matcher.
* The + sign is the single level matcher.
*
* @author andrea
*/
public interface IAuthorizator {
/**
* Ask the implementation of the authorizator if the topic can be used in a publish.
* */
boolean canWrite(String topic, String user, String client);
boolean canRead(String topic, String user, String client);
}
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2012-2015 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;
/**
* @author andrea
*/
public class PermitAllAuthorizator implements IAuthorizator {
@Override
public boolean canWrite(String topic, String user, String client) {
return true;
}
@Override
public boolean canRead(String topic, String user, String client) {
return true;
}
}
@@ -0,0 +1,104 @@
/*
* Copyright (c) 2012-2015 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 io.moquette.proto.messages.AbstractMessage.QOSType;
/**
* Maintain the information about which Topic a certain ClientID is subscribed
* and at which QoS
*
*
* @author andrea
*/
public class Subscription implements Serializable {
QOSType requestedQos; //max QoS acceptable
String clientId;
String topicFilter;
boolean cleanSession;
boolean active = true;
public Subscription(String clientId, String topicFilter, QOSType requestedQos, boolean cleanSession) {
this.requestedQos = requestedQos;
this.clientId = clientId;
this.topicFilter = topicFilter;
this.cleanSession = cleanSession;
}
public Subscription(Subscription orig) {
this.requestedQos = orig.requestedQos;
this.clientId = orig.clientId;
this.topicFilter = orig.topicFilter;
this.cleanSession = orig.cleanSession;
this.active = orig.active;
}
public String getClientId() {
return clientId;
}
public QOSType getRequestedQos() {
return requestedQos;
}
public String getTopicFilter() {
return topicFilter;
}
public boolean isCleanSession() {
return this.cleanSession;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Subscription that = (Subscription) o;
if (cleanSession != that.cleanSession) return false;
if (clientId != null ? !clientId.equals(that.clientId) : that.clientId != null) return false;
if (requestedQos != that.requestedQos) return false;
if (topicFilter != null ? !topicFilter.equals(that.topicFilter) : that.topicFilter != null) return false;
return true;
}
@Override
public int hashCode() {
int result = requestedQos.hashCode();
result = 31 * result + (clientId != null ? clientId.hashCode() : 0);
result = 31 * result + (topicFilter != null ? topicFilter.hashCode() : 0);
result = 31 * result + (cleanSession ? 1 : 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;
}
}
}
@@ -0,0 +1,339 @@
/*
* Copyright (c) 2012-2015 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.text.ParseException;
import java.util.*;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicReference;
import io.moquette.spi.ISessionsStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Represents a tree of topics subscriptions.
*
* @author andrea
*/
public class SubscriptionsStore {
public static class NodeCouple {
final TreeNode root;
final TreeNode createdNode;
public NodeCouple(TreeNode root, TreeNode createdNode) {
this.root = root;
this.createdNode = createdNode;
}
}
/**
* Check if the topic filter of the subscription is well formed
* */
public static boolean validate(String topicFilter) {
try {
parseTopic(topicFilter);
return true;
} catch (ParseException pex) {
LOG.info("Bad matching topic filter <{}>", topicFilter);
return false;
}
}
public interface IVisitor<T> {
void visit(TreeNode node, int deep);
T getResult();
}
private class DumpTreeVisitor implements IVisitor<String> {
String s = "";
@Override
public void visit(TreeNode node, int deep) {
String subScriptionsStr = "";
String indentTabs = indentTabs(deep);
for (Subscription sub : node.m_subscriptions) {
subScriptionsStr += indentTabs + sub.toString() + "\n";
}
s += node.getToken() == null ? "" : node.getToken().toString();
s += "\n" + (node.m_subscriptions.isEmpty() ? indentTabs : "") + subScriptionsStr /*+ "\n"*/;
}
private String indentTabs(int deep) {
String s = "";
for (int i=0; i < deep; i++) {
s += "\t";
// s += "--";
}
return s;
}
@Override
public String getResult() {
return s;
}
}
private AtomicReference<TreeNode> subscriptions = new AtomicReference<>(new TreeNode(null));
private static final Logger LOG = LoggerFactory.getLogger(SubscriptionsStore.class);
/**
* Initialize the subscription tree with the list of subscriptions.
* Maintained for compatibility reasons.
*/
public void init(ISessionsStore sessionsStore) {
LOG.debug("init invoked");
List<Subscription> subscriptions = sessionsStore.listAllSubscriptions();
//reload any subscriptions persisted
if (LOG.isDebugEnabled()) {
LOG.debug("Reloading all stored subscriptions...subscription tree before {}", dumpTree());
}
for (Subscription subscription : subscriptions) {
LOG.debug("Re-subscribing {} to topic {}", subscription.getClientId(), subscription.getTopicFilter());
add(subscription);
}
if (LOG.isDebugEnabled()) {
LOG.debug("Finished loading. Subscription tree after {}", dumpTree());
}
}
public void add(Subscription newSubscription) {
TreeNode oldRoot;
NodeCouple couple;
do {
oldRoot = subscriptions.get();
couple = recreatePath(newSubscription.topicFilter, oldRoot);
couple.createdNode.addSubscription(newSubscription); //createdNode could be null?
//spin lock repeating till we can, swap root, if can't swap just re-do the operation
} while(!subscriptions.compareAndSet(oldRoot, couple.root));
LOG.debug("root ref {}, original root was {}", couple.root, oldRoot);
}
protected NodeCouple recreatePath(String topic, final TreeNode oldRoot) {
List<Token> tokens = new ArrayList<>();
try {
tokens = parseTopic(topic);
} catch (ParseException ex) {
//TODO handle the parse exception
LOG.error(null, ex);
}
final TreeNode newRoot = oldRoot.copy();
TreeNode parent = newRoot;
TreeNode current = newRoot;
for (Token token : tokens) {
TreeNode matchingChildren;
//check if a children with the same token already exists
if ((matchingChildren = current.childWithToken(token)) != null) {
//copy the traversed node
current = matchingChildren.copy();
current.m_parent = parent;
//update the child just added in the children list
parent.updateChild(matchingChildren, current);
parent = current;
} else {
//create a new node for the newly inserted token
matchingChildren = new TreeNode(current);
matchingChildren.setToken(token);
current.addChild(matchingChildren);
current = matchingChildren;
}
}
return new NodeCouple(newRoot, current);
}
public void removeSubscription(String topic, String clientID) {
TreeNode oldRoot;
NodeCouple couple;
do {
oldRoot = subscriptions.get();
couple = recreatePath(topic, oldRoot);
//do the job
//search for the subscription to remove
Subscription toBeRemoved = null;
for (Subscription sub : couple.createdNode.subscriptions()) {
if (sub.topicFilter.equals(topic) && sub.getClientId().equals(clientID)) {
toBeRemoved = sub;
break;
}
}
if (toBeRemoved != null) {
couple.createdNode.subscriptions().remove(toBeRemoved);
}
//spin lock repeating till we can, swap root, if can't swap just re-do the operation
} while(!subscriptions.compareAndSet(oldRoot, couple.root));
}
/**
* Visit the topics tree to remove matching subscriptions with clientID.
* It's a mutating structure operation so create a new subscription tree (partial or total).
*/
public void removeForClient(String clientID) {
TreeNode oldRoot;
TreeNode newRoot;
do {
oldRoot = subscriptions.get();
newRoot = oldRoot.removeClientSubscriptions(clientID);
//spin lock repeating till we can, swap root, if can't swap just re-do the operation
} while(!subscriptions.compareAndSet(oldRoot, newRoot));
}
/**
* 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.
*/
public List<Subscription> matches(String topic) {
List<Token> tokens;
try {
tokens = parseTopic(topic);
} catch (ParseException ex) {
//TODO handle the parse exception
LOG.error(null, ex);
return Collections.emptyList();
}
Queue<Token> tokenQueue = new LinkedBlockingDeque<>(tokens);
List<Subscription> matchingSubs = new ArrayList<>();
subscriptions.get().matches(tokenQueue, matchingSubs);
//remove the overlapping subscriptions, selecting ones with greatest qos
Map<String, Subscription> subsForClient = new HashMap<>();
for (Subscription sub : matchingSubs) {
Subscription existingSub = subsForClient.get(sub.getClientId());
//update the selected subscriptions if not present or if has a greater qos
if (existingSub == null || existingSub.getRequestedQos().byteValue() < sub.getRequestedQos().byteValue()) {
subsForClient.put(sub.getClientId(), sub);
}
}
return new ArrayList<>(subsForClient.values());
}
public boolean contains(Subscription sub) {
return !matches(sub.topicFilter).isEmpty();
}
public int size() {
return subscriptions.get().size();
}
public String dumpTree() {
DumpTreeVisitor visitor = new DumpTreeVisitor();
bfsVisit(subscriptions.get(), visitor, 0);
return visitor.getResult();
}
private void bfsVisit(TreeNode node, IVisitor visitor, int deep) {
if (node == null) {
return;
}
visitor.visit(node, deep);
for (TreeNode child : node.m_children) {
bfsVisit(child, visitor, ++deep);
}
}
/**
* Verify if the 2 topics matching respecting the rules of MQTT Appendix A
*/
//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(null, ex);
throw new RuntimeException(ex);
}
}
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,74 @@
/*
* Copyright (c) 2012-2015 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 (t == MULTI || t == SINGLE) {
return false;
}
if (this == MULTI || this == SINGLE) {
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;
}
}
@@ -0,0 +1,192 @@
/*
* Copyright (c) 2012-2015 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.*;
import java.util.concurrent.LinkedBlockingQueue;
class TreeNode {
private class ClientIDComparator implements Comparator<Subscription> {
public int compare(Subscription o1, Subscription o2) {
return o1.getClientId().compareTo(o2.getClientId());
}
}
TreeNode m_parent;
Token m_token;
List<TreeNode> m_children = new ArrayList<>();
List<Subscription> m_subscriptions = new ArrayList<>();
TreeNode(TreeNode parent) {
this.m_parent = parent;
}
Token getToken() {
return m_token;
}
void setToken(Token topic) {
this.m_token = topic;
}
void addSubscription(Subscription s) {
//avoid double registering for same clientID, topic and QoS
if (m_subscriptions.contains(s)) {
return;
}
//remove existing subscription for same client and topic but different QoS
Comparator<Subscription> comparator = new ClientIDComparator();
Collections.sort(m_subscriptions, comparator);
int existingSubIdx = Collections.binarySearch(m_subscriptions, s, comparator);
if (existingSubIdx >= 0) {
m_subscriptions.remove(existingSubIdx);
}
m_subscriptions.add(s);
}
void addChild(TreeNode child) {
m_children.add(child);
}
/**
* Creates a shallow copy of the current node.
* Copy the token and the children.
* */
TreeNode copy() {
final TreeNode copy = new TreeNode(this);
copy.m_parent = m_parent;
copy.m_children = new ArrayList<>(m_children);
copy.m_subscriptions = new ArrayList<>(m_subscriptions.size());
for (Subscription sub : m_subscriptions) {
copy.m_subscriptions.add(new Subscription(sub));
}
copy.m_token = m_token;
return copy;
}
/**
* Search for children that has the specified token, if not found return
* null;
*/
TreeNode childWithToken(Token token) {
for (TreeNode child : m_children) {
if (child.getToken().equals(token)) {
return child;
}
}
return null;
}
void updateChild(TreeNode oldChild, TreeNode newChild) {
m_children.remove(oldChild);
m_children.add(newChild);
}
List<Subscription> subscriptions() {
return m_subscriptions;
}
void matches(Queue<Token> tokens, List<Subscription> matchingSubs) {
Token t = tokens.poll();
//check if t is null <=> tokens finished
if (t == null) {
matchingSubs.addAll(m_subscriptions);
//check if it has got a MULTI child and add its subscriptions
for (TreeNode n : m_children) {
if (n.getToken() == Token.MULTI || n.getToken() == Token.SINGLE) {
matchingSubs.addAll(n.subscriptions());
}
}
return;
}
//we are on MULTI, than add subscriptions and return
if (m_token == Token.MULTI) {
matchingSubs.addAll(m_subscriptions);
return;
}
for (TreeNode n : m_children) {
if (n.getToken().match(t)) {
//Create a copy of token, else if navigate 2 sibling it
//consumes 2 elements on the queue instead of one
n.matches(new LinkedBlockingQueue<>(tokens), matchingSubs);
//TODO don't create a copy n.matches(tokens, matchingSubs);
}
}
}
/**
* Return the number of registered subscriptions
*/
int size() {
int res = m_subscriptions.size();
for (TreeNode child : m_children) {
res += child.size();
}
return res;
}
/**
* Create a copied subtree rooted on this node but purged of clientID's subscriptions.
* */
TreeNode removeClientSubscriptions(String clientID) {
//collect what to delete and then delete to avoid ConcurrentModification
TreeNode newSubRoot = this.copy();
List<Subscription> subsToRemove = new ArrayList<>();
for (Subscription s : newSubRoot.m_subscriptions) {
if (s.clientId.equals(clientID)) {
subsToRemove.add(s);
}
}
for (Subscription s : subsToRemove) {
newSubRoot.m_subscriptions.remove(s);
}
//go deep
List<TreeNode> newChildren = new ArrayList<>(newSubRoot.m_children.size());
for (TreeNode child : newSubRoot.m_children) {
newChildren.add(child.removeClientSubscriptions(clientID));
}
newSubRoot.m_children = newChildren;
return newSubRoot;
}
/**
* @return the set of subscriptions for the given client.
* */
Set<Subscription> findAllByClientID(String clientID) {
Set<Subscription> subs = new HashSet<>();
for (Subscription s : m_subscriptions) {
if (s.clientId.equals(clientID)) {
subs.add(s);
}
}
//go deep
for (TreeNode child : m_children) {
subs.addAll(child.findAllByClientID(clientID));
}
return subs;
}
}