wildfire chat server source initial submit

This commit is contained in:
heavyrain2012
2019-03-16 17:04:23 +08:00
parent e59217bd10
commit cab3ca0916
1466 changed files with 528862 additions and 15919 deletions
@@ -0,0 +1,154 @@
/*
* Copyright (c) 2012-2017 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.netty.handler.codec.mqtt.MqttConnectMessage;
import io.netty.handler.codec.mqtt.MqttPublishMessage;
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.
*/
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
*/
BrokerInterceptor(List<InterceptHandler> handlers) {
this(1, handlers);
}
/**
* Configures a broker interceptor using the pool size specified in the IConfig argument.
*/
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();
}
}
@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 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;
}
}
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2012-2017 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;
final class DebugUtils {
static String payload2Str(ByteBuf content) {
return new String(content.copy().array());
}
private DebugUtils() {
}
}
+109
View File
@@ -0,0 +1,109 @@
package io.moquette.spi.impl;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
public class DesUtil {
private final static String Key = "abcdefgh";
private final static String DES = "AES";
public static void main(String[] args) throws Exception {
String data = "123 456";
System.err.println(encrypt(data));
System.err.println(decrypt(encrypt(data)));
System.out.println(decrypt("JF5dX/TlOg529KAhh+vywjzIp5Msktmf"));
}
/**
* Description 根据键值进行加密
* @param data
* @return
* @throws Exception
*/
public static String encrypt(String data) throws Exception {
byte[] bt = encrypt(data.getBytes(), Key.getBytes());
String strs = new String(Base64.getEncoder().encode(bt));
return strs;
}
/**
* Description 根据键值进行解密
* @param data
* @return
* @throws IOException
* @throws Exception
*/
public static String decrypt(String data) throws IOException,
Exception {
if (data == null)
return null;
byte[] buf = Base64.getDecoder().decode(data);
byte[] bt = decrypt(buf,Key.getBytes());
return new String(bt);
}
/**
* Description 根据键值进行加密
* @param data
* @param key 加密键byte数组
* @return
* @throws Exception
*/
private static byte[] encrypt(byte[] data, byte[] key) throws Exception {
// 生成一个可信任的随机数源
SecureRandom sr = new SecureRandom();
// 从原始密钥数据创建DESKeySpec对象
DESKeySpec dks = new DESKeySpec(key);
// 创建一个密钥工厂,然后用它把DESKeySpec转换成SecretKey对象
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES);
SecretKey securekey = keyFactory.generateSecret(dks);
// Cipher对象实际完成加密操作
Cipher cipher = Cipher.getInstance(DES);
// 用密钥初始化Cipher对象
cipher.init(Cipher.ENCRYPT_MODE, securekey, sr);
return cipher.doFinal(data);
}
/**
* Description 根据键值进行解密
* @param data
* @param key 加密键byte数组
* @return
* @throws Exception
*/
private static byte[] decrypt(byte[] data, byte[] key) throws Exception {
// 生成一个可信任的随机数源
SecureRandom sr = new SecureRandom();
// 从原始密钥数据创建DESKeySpec对象
DESKeySpec dks = new DESKeySpec(key);
// 创建一个密钥工厂,然后用它把DESKeySpec转换成SecretKey对象
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES);
SecretKey securekey = keyFactory.generateSecret(dks);
// Cipher对象实际完成解密操作
Cipher cipher = Cipher.getInstance(DES);
// 用密钥初始化Cipher对象
cipher.init(Cipher.DECRYPT_MODE, securekey, sr);
return cipher.doFinal(data);
}
}
@@ -0,0 +1,98 @@
/*
* Copyright (c) 2012-2017 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.ClientSession;
import io.moquette.spi.IMessagesStore;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.mqtt.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Queue;
class InternalRepublisher {
private static final Logger LOG = LoggerFactory.getLogger(InternalRepublisher.class);
private final PersistentQueueMessageSender messageSender;
InternalRepublisher(PersistentQueueMessageSender messageSender) {
this.messageSender = messageSender;
}
void publishRetained(ClientSession targetSession, Collection<IMessagesStore.StoredMessage> messages) {
for (IMessagesStore.StoredMessage storedMsg : messages) {
// fire as retained the message
MqttPublishMessage publishMsg = retainedPublish(storedMsg);
if (storedMsg.getQos() != MqttQoS.AT_MOST_ONCE) {
LOG.debug("Adding message to inflight zone. ClientId={}, topic={}", targetSession.clientID,
storedMsg.getTopic());
int packetID = targetSession.inFlightAckWaiting(storedMsg);
// set the PacketIdentifier only for QoS > 0
publishMsg = retainedPublish(storedMsg, packetID);
}
this.messageSender.sendPublish(targetSession, publishMsg);
}
}
void publishStored(ClientSession clientSession, Queue<IMessagesStore.StoredMessage> publishedEvents) {
IMessagesStore.StoredMessage pubEvt;
while ((pubEvt = publishedEvents.poll()) != null) {
// put in flight zone
LOG.debug("Adding message ot inflight zone. ClientId={}, guid={}, topic={}", clientSession.clientID,
pubEvt.getGuid(), pubEvt.getTopic());
int messageId = clientSession.inFlightAckWaiting(pubEvt);
MqttPublishMessage publishMsg = notRetainedPublish(pubEvt);
// set the PacketIdentifier only for QoS > 0
if (publishMsg.fixedHeader().qosLevel() != MqttQoS.AT_MOST_ONCE) {
publishMsg = notRetainedPublish(pubEvt, messageId);
}
this.messageSender.sendPublish(clientSession, publishMsg);
}
}
private MqttPublishMessage notRetainedPublish(IMessagesStore.StoredMessage storedMessage, Integer messageID) {
return createPublishForQos(storedMessage.getTopic(), storedMessage.getQos(), storedMessage.getPayload(), false,
messageID);
}
private MqttPublishMessage notRetainedPublish(IMessagesStore.StoredMessage storedMessage) {
return createPublishForQos(storedMessage.getTopic(), storedMessage.getQos(), storedMessage.getPayload(), false,
0);
}
private MqttPublishMessage retainedPublish(IMessagesStore.StoredMessage storedMessage) {
return createPublishForQos(storedMessage.getTopic(), storedMessage.getQos(), storedMessage.getPayload(), true,
0);
}
private MqttPublishMessage retainedPublish(IMessagesStore.StoredMessage storedMessage, Integer packetID) {
return createPublishForQos(storedMessage.getTopic(), storedMessage.getQos(), storedMessage.getPayload(), true,
packetID);
}
public static MqttPublishMessage createPublishForQos(String topic, MqttQoS qos, ByteBuf message, boolean retained,
int messageId) {
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, false, qos, retained, 0);
MqttPublishVariableHeader varHeader = new MqttPublishVariableHeader(topic, messageId);
return new MqttPublishMessage(fixedHeader, varHeader, message);
}
}
@@ -0,0 +1,500 @@
/*
* Copyright (c) 2012-2017 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 cn.wildfirechat.proto.ProtoConstants;
import cn.wildfirechat.proto.WFCMessage;
import com.google.gson.Gson;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.util.StringUtil;
import com.xiaoleilu.loServer.pojos.OutputNotifyChannelSubscribeStatus;
import com.xiaoleilu.loServer.pojos.SendMessageData;
import io.moquette.persistence.*;
import io.moquette.persistence.MemorySessionStore.Session;
import io.moquette.server.ConnectionDescriptorStore;
import io.moquette.spi.IMessagesStore;
import io.moquette.spi.ISessionsStore;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.mqtt.*;
import win.liyufan.im.HttpUtils;
import win.liyufan.im.IMTopic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import win.liyufan.im.Utility;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.BiFunction;
import static cn.wildfirechat.proto.ProtoConstants.PersistFlag.Transparent;
public class MessagesPublisher {
private static final Logger LOG = LoggerFactory.getLogger(MessagesPublisher.class);
private final ConnectionDescriptorStore connectionDescriptors;
private final ISessionsStore m_sessionsStore;
private final IMessagesStore m_messagesStore;
private final PersistentQueueMessageSender messageSender;
private ConcurrentHashMap<UserClientEntry, Long> chatRoomHeaders = new ConcurrentHashMap<>();
private ExecutorService chatroomScheduler = Executors.newFixedThreadPool(1);
private boolean schedulerStarted = false;
private static ExecutorService executorCallback = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
public void startChatroomScheduler() {
schedulerStarted = true;
chatroomScheduler.execute(() -> {
while (schedulerStarted) {
try {
if (chatRoomHeaders.size() < 100) {
Thread.sleep(500);
} else if(chatRoomHeaders.size() < 500) {
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
Utility.printExecption(LOG, e);
}
chatRoomHeaders.forEach(100, (s, aLong) -> {
chatRoomHeaders.remove(s, aLong);
publish2ChatroomReceivers(s.userId, s.clientId, aLong);
});
}
});
}
public void stopChatroomScheduler() {
schedulerStarted = false;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
chatroomScheduler.shutdown();
}
public MessagesPublisher(ConnectionDescriptorStore connectionDescriptors, ISessionsStore sessionsStore,
PersistentQueueMessageSender messageSender, HazelcastInstance hz, IMessagesStore messagesStore) {
this.connectionDescriptors = connectionDescriptors;
this.m_sessionsStore = sessionsStore;
this.messageSender = messageSender;
this.m_messagesStore = messagesStore;
this.startChatroomScheduler();
}
static MqttPublishMessage notRetainedPublish(String topic, MqttQoS qos, ByteBuf message) {
return notRetainedPublishWithMessageId(topic, qos, message, 0);
}
private static MqttPublishMessage notRetainedPublishWithMessageId(String topic, MqttQoS qos, ByteBuf message,
int messageId) {
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, false, qos, false, 0);
MqttPublishVariableHeader varHeader = new MqttPublishVariableHeader(topic, messageId);
return new MqttPublishMessage(fixedHeader, varHeader, message);
}
private void publish2ChatroomReceivers(String user, String clientId, long messageHead) {
publish2ChatroomReceiversDirectly(user, clientId, messageHead);
}
public void publish2ChatroomReceiversDirectly(String user, String clientId, long messageHead) {
try {
Session session = m_sessionsStore.getSession(clientId);
if (session != null) {
LOG.warn("session for {} not exit", clientId);
return;
}
if(!session.getUsername().equals(user)) {
LOG.warn("session {} user is not {}", clientId, user);
return;
}
if (!this.connectionDescriptors.isConnected(clientId)) {
LOG.warn("session {} not connected", clientId);
return;
}
WFCMessage.NotifyMessage notifyMessage = WFCMessage.NotifyMessage
.newBuilder()
.setType(ProtoConstants.PullType.Pull_ChatRoom)
.setHead(messageHead)
.build();
ByteBuf payload = Unpooled.buffer();
byte[] byteData = notifyMessage.toByteArray();
payload.ensureWritable(byteData.length).writeBytes(byteData);
MqttPublishMessage publishMsg;
publishMsg = notRetainedPublish(IMTopic.NotifyMessageTopic, MqttQoS.AT_MOST_ONCE, payload);
boolean result = !this.messageSender.sendPublish(session.getClientSession(), publishMsg);
if (!result) {
LOG.warn("send publish to {} failure", clientId);
}
} catch (Exception e) {
e.printStackTrace();
Utility.printExecption(LOG, e);
}
}
private void publish2Receivers(String sender, int conversationType, String target, int line, long messageHead, Collection<String> receivers, String pushContent, String exceptClientId, int pullType, int messageContentType, long serverTime, int mentionType, List<String> mentionTargets, int persistFlag) {
if (persistFlag == Transparent) {
publishTransparentMessage2Receivers(messageHead, receivers, pullType);
return;
}
WFCMessage.Message message = null;
for (String user : receivers) {
if (!user.equals(sender)) {
WFCMessage.User userInfo = m_messagesStore.getUserInfo(user);
if (userInfo != null && userInfo.getType() == ProtoConstants.UserType.UserType_Robot) {
WFCMessage.Robot robot = m_messagesStore.getRobot(user);
if (robot != null && !StringUtil.isNullOrEmpty(robot.getCallback())) {
if (message == null) {
message = m_messagesStore.getMessage(messageHead);
}
final WFCMessage.Message finalMsg = message;
executorCallback.execute(() -> HttpUtils.httpJsonPost(robot.getCallback(), new Gson().toJson(SendMessageData.fromProtoMessage(finalMsg), SendMessageData.class)));
continue;
}
}
}
long messageSeq;
if (pullType != ProtoConstants.PullType.Pull_ChatRoom) {
messageSeq = m_messagesStore.insertUserMessages(sender, conversationType, target, line, messageContentType, user, messageHead);
} else {
messageSeq = m_messagesStore.insertChatroomMessages(user, line, messageHead);
}
Collection<Session> sessions = m_sessionsStore.sessionForUser(user);
String senderName = null;
String targetName = null;
boolean nameLoaded = false;
Collection<String> targetClients = null;
if (pullType == ProtoConstants.PullType.Pull_ChatRoom) {
targetClients = m_messagesStore.getChatroomMemberClient(user);
}
for (Session targetSession : sessions) {
if(System.currentTimeMillis() - targetSession.getLastActiveTime() > 7 * 24 * 60 * 60 * 1000) {
continue;
}
if (exceptClientId != null && exceptClientId.equals(targetSession.getClientSession().clientID)) {
continue;
}
if (targetSession.getClientID() == null) {
continue;
}
if (pullType == ProtoConstants.PullType.Pull_ChatRoom && !targetClients.contains(targetSession.getClientID())) {
continue;
}
if (pullType == ProtoConstants.PullType.Pull_ChatRoom) {
if (exceptClientId.equals(targetSession.getClientID())) {
targetSession.refreshLastChatroomActiveTime();
}
if(System.currentTimeMillis() - targetSession.getLastChatroomActiveTime() > 5*60*1000) {
m_messagesStore.handleQuitChatroom(user, targetSession.getClientID(), target);
continue;
}
}
boolean isSlient;
String slientKey = null;
if (pullType == ProtoConstants.PullType.Pull_ChatRoom) {
isSlient = true;
} else {
isSlient = false;
if (conversationType == ProtoConstants.ConversationType.ConversationType_Private) {
slientKey = conversationType + "-" + line + "-" + sender;
} else {
slientKey = conversationType + "-" + line + "-" + target;
}
if (targetSession.getSlientConvs().containsKey(slientKey)) {
LOG.info("The conversation {} is slient", slientKey);
isSlient = true;
}
if (targetSession.isGlobalSlient()) {
LOG.info("The user is global sliented");
isSlient = true;
}
if (!StringUtil.isNullOrEmpty(pushContent) || messageContentType == 400 || messageContentType == 402) {
if (!isSlient) {
targetSession.setUnReceivedMsgs(targetSession.getUnReceivedMsgs() + 1);
}
}
if (isSlient) {
if (mentionType == 2 || (mentionType == 1 && mentionTargets.contains(user))) {
isSlient = false;
}
}
}
boolean needPush = !user.equals(sender);
boolean targetIsActive = this.connectionDescriptors.isConnected(targetSession.getClientSession().clientID);
if (targetIsActive) {
WFCMessage.NotifyMessage notifyMessage = WFCMessage.NotifyMessage
.newBuilder()
.setType(pullType)
.setHead(messageSeq)
.build();
ByteBuf payload = Unpooled.buffer();
byte[] byteData = notifyMessage.toByteArray();
payload.ensureWritable(byteData.length).writeBytes(byteData);
MqttPublishMessage publishMsg;
publishMsg = notRetainedPublish(IMTopic.NotifyMessageTopic, MqttQoS.AT_MOST_ONCE, payload);
boolean sent = this.messageSender.sendPublish(targetSession.getClientSession(), publishMsg);
if (sent) {
needPush = false;
}
} else {
LOG.info("the target {} of user {} is not active", targetSession.getClientID(), targetSession.getUsername());
}
if (needPush && pullType != ProtoConstants.PullType.Pull_ChatRoom) {
int curMentionType = 0;
if (mentionType == 2) {
curMentionType = 2;
isSlient = false;
} else if (mentionType == 1){
if (mentionTargets != null && mentionTargets.contains(user)) {
curMentionType = 1;
isSlient = false;
}
}
if ((StringUtil.isNullOrEmpty(pushContent) && messageContentType != 402 && messageContentType != 400)) {
LOG.info("push content is empty and contenttype is {}", messageContentType);
continue;
}
if (StringUtil.isNullOrEmpty(targetSession.getDeviceToken())) {
LOG.info("device token not exist");
continue;
}
if (isSlient) {
LOG.info("The conversation {} is slient", slientKey);
continue;
}
boolean isHiddenDetail = targetSession.isHiddenNotifyDetail();
if(!nameLoaded) {
senderName = getUserDisplayName(sender);
targetName = getTargetName(target, conversationType);
nameLoaded = true;
}
this.messageSender.sendPush(sender, conversationType, target, line, messageHead, targetSession.getClientID(), pushContent, messageContentType, serverTime, senderName, targetName, targetSession.getUnReceivedMsgs(), curMentionType, isHiddenDetail, targetSession.getLanguage());
}
}
}
}
private void publishTransparentMessage2Receivers(long messageHead, Collection<String> receivers, int pullType) {
WFCMessage.Message message = m_messagesStore.getMessage(messageHead);
if (message != null) {
for (String user : receivers) {
Collection<Session> sessions = m_sessionsStore.sessionForUser(user);
for (Session targetSession : sessions) {
if (targetSession.getClientID() == null) {
continue;
}
boolean targetIsActive = this.connectionDescriptors.isConnected(targetSession.getClientSession().clientID);
if (targetIsActive) {
ByteBuf payload = Unpooled.buffer();
byte[] byteData = message.toByteArray();
payload.ensureWritable(byteData.length).writeBytes(byteData);
MqttPublishMessage publishMsg;
publishMsg = notRetainedPublish(IMTopic.SendMessageTopic, MqttQoS.AT_MOST_ONCE, payload);
this.messageSender.sendPublish(targetSession.getClientSession(), publishMsg);
} else {
LOG.info("the target {} of user {} is not active", targetSession.getClientID(), targetSession.getUsername());
}
}
}
}
}
String getUserDisplayName(String userId) {
WFCMessage.User user = m_messagesStore.getUserInfo(userId);
if(user != null) {
return user.getDisplayName();
}
return null;
}
String getTargetName(String targetId, int cnvType) {
if (cnvType == ProtoConstants.ConversationType.ConversationType_Private) {
return getUserDisplayName(targetId);
} else if(cnvType == ProtoConstants.ConversationType.ConversationType_Group) {
WFCMessage.GroupInfo group = m_messagesStore.getGroupInfo(targetId);
if(group != null) {
return group.getName();
}
}
return null;
}
public void publishNotification(String topic, String receiver, long head) {
publishNotificationLocal(topic, receiver, head);
}
void publishNotificationLocal(String topic, String receiver, long head) {
Collection<Session> sessions = m_sessionsStore.sessionForUser(receiver);
for (Session targetSession : sessions) {
boolean targetIsActive = this.connectionDescriptors.isConnected(targetSession.getClientSession().clientID);
if (targetIsActive) {
ByteBuf payload = Unpooled.buffer();
payload.writeLong(head);
MqttPublishMessage publishMsg;
publishMsg = notRetainedPublish(topic, MqttQoS.AT_MOST_ONCE, payload);
boolean result = this.messageSender.sendPublish(targetSession.getClientSession(), publishMsg);
if (!result) {
LOG.warn("Publish friend request failure");
}
}
}
}
public void updateChatroomMembersQueue(String chatroomId, int line, long messageId) {
final long messageSeq = m_messagesStore.insertChatroomMessages(chatroomId, line, messageId);
Collection<UserClientEntry> members = m_messagesStore.getChatroomMembers(chatroomId);
for (UserClientEntry member : members
) {
chatRoomHeaders.compute(member, new BiFunction<UserClientEntry, Long, Long>() {
@Override
public Long apply(UserClientEntry s, Long aLong) {
if (aLong == null)
return messageSeq;
if (messageSeq > aLong)
return messageSeq;
return aLong;
}
});
}
}
public void publishRecall2ReceiversLocal(long messageUid, String operatorId, Collection<String> receivers, String exceptClientId) {
for (String user : receivers) {
Collection<Session> sessions = m_sessionsStore.sessionForUser(user);
for (Session targetSession : sessions) {
if (exceptClientId != null && exceptClientId.equals(targetSession.getClientSession().clientID)) {
continue;
}
if (targetSession.getClientID() == null) {
continue;
}
boolean targetIsActive = this.connectionDescriptors.isConnected(targetSession.getClientSession().clientID);
if (targetIsActive) {
WFCMessage.NotifyRecallMessage notifyMessage = WFCMessage.NotifyRecallMessage
.newBuilder()
.setFromUser(operatorId)
.setId(messageUid)
.build();
ByteBuf payload = Unpooled.buffer();
byte[] byteData = notifyMessage.toByteArray();
payload.ensureWritable(byteData.length).writeBytes(byteData);
MqttPublishMessage publishMsg;
publishMsg = notRetainedPublish(IMTopic.NotifyRecallMessageTopic, MqttQoS.AT_MOST_ONCE, payload);
this.messageSender.sendPublish(targetSession.getClientSession(), publishMsg);
} else {
LOG.info("the target {} of user {} is not active", targetSession.getClientID(), targetSession.getUsername());
}
}
}
}
public void publishRecall2Receivers(long messageUid, String operatorId, Set<String> receivers, String exceptClientId) {
publishRecall2ReceiversLocal(messageUid, operatorId, receivers, exceptClientId);
}
public void publish2Receivers(WFCMessage.Message message, Set<String> receivers, String exceptClientId, int pullType) {
if (message.getConversation().getType() == ProtoConstants.ConversationType.ConversationType_Channel) {
WFCMessage.ChannelInfo channelInfo = m_messagesStore.getChannelInfo(message.getConversation().getTarget());
if (channelInfo != null && !StringUtil.isNullOrEmpty(channelInfo.getCallback())) {
executorCallback.execute(() -> HttpUtils.httpJsonPost(channelInfo.getCallback() + "/message", new Gson().toJson(SendMessageData.fromProtoMessage(message), SendMessageData.class)));
}
}
long messageId = message.getMessageId();
String pushContent = message.getContent().getPushContent();
if (StringUtil.isNullOrEmpty(pushContent)) {
int type = message.getContent().getType();
if (type == ProtoConstants.ContentType.Image) {
pushContent = "[图片]";
} else if(type == ProtoConstants.ContentType.Location) {
pushContent = "[位置]";
} else if(type == ProtoConstants.ContentType.Text) {
pushContent = message.getContent().getSearchableContent();
} else if(type == ProtoConstants.ContentType.Voice) {
pushContent = "[语音]";
} else if(type == ProtoConstants.ContentType.Video) {
pushContent = "[视频]";
} else if(type == ProtoConstants.ContentType.RichMedia) {
pushContent = "[图文]";
}
}
if (message.getContent().getPersistFlag() == Transparent) {
pushContent = null;
}
publish2Receivers(message.getFromUser(),
message.getConversation().getType(), message.getConversation().getTarget(), message.getConversation().getLine(),
messageId,
receivers,
pushContent, exceptClientId, pullType, message.getContent().getType(), message.getServerTimestamp(), message.getContent().getMentionedType(), message.getContent().getMentionedTargetList(), message.getContent().getPersistFlag());
}
public void forwardMessage(final WFCMessage.Message message, String forwardUrl) {
executorCallback.execute(() -> HttpUtils.httpJsonPost(forwardUrl, new Gson().toJson(SendMessageData.fromProtoMessage(message), SendMessageData.class)));
}
public void notifyChannelListenStatusChanged(WFCMessage.ChannelInfo channelInfo, String user, boolean listen) {
if (channelInfo == null || StringUtil.isNullOrEmpty(channelInfo.getCallback())) {
return;
}
executorCallback.execute(() -> HttpUtils.httpJsonPost(channelInfo.getCallback() + "/subscribe", new Gson().toJson(new OutputNotifyChannelSubscribeStatus(user, channelInfo.getTargetId(), listen), OutputNotifyChannelSubscribeStatus.class)));
}
}
@@ -0,0 +1,65 @@
/*
* Copyright (c) 2012-2017 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 cn.wildfirechat.proto.WFCMessage;
import cn.wildfirechat.push.PushMessage;
import cn.wildfirechat.push.PushServer;
import io.moquette.server.ConnectionDescriptorStore;
import io.moquette.spi.ClientSession;
import io.netty.handler.codec.mqtt.MqttPublishMessage;
import io.netty.handler.codec.mqtt.MqttQoS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static io.moquette.spi.impl.ProtocolProcessor.asStoredMessage;
import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE;
class PersistentQueueMessageSender {
private static final Logger LOG = LoggerFactory.getLogger(PersistentQueueMessageSender.class);
private final ConnectionDescriptorStore connectionDescriptorStore;
PersistentQueueMessageSender(ConnectionDescriptorStore connectionDescriptorStore) {
this.connectionDescriptorStore = connectionDescriptorStore;
}
void sendPush(String sender, int conversationType, String target, int line, long messageHead, String deviceId, String pushContent, int messageContentType, long serverTime, String senderName, String targetName, int unReceivedMsg, int mentionType, boolean isHiddenDetail, String language) {
LOG.info("Send push to {}, message from {}", deviceId, sender);
PushMessage pushMessage = new PushMessage(sender, conversationType, target, line, messageContentType, serverTime, senderName, targetName, unReceivedMsg, mentionType, isHiddenDetail, language);
PushServer.getServer().pushMessage(pushMessage, deviceId, pushContent);
}
boolean sendPublish(ClientSession clientsession, MqttPublishMessage pubMessage) {
String clientId = clientsession.clientID;
final int messageId = pubMessage.variableHeader().packetId();
final String topicName = pubMessage.variableHeader().topicName();
if (LOG.isDebugEnabled()) {
LOG.debug("Sending PUBLISH message. MessageId={}, CId={}, topic={}, qos={}, payload={}", messageId,
clientId, topicName, DebugUtils.payload2Str(pubMessage.payload()));
} else {
LOG.info("Sending PUBLISH message. MessageId={}, CId={}, topic={}", messageId, clientId, topicName);
}
boolean messageDelivered = connectionDescriptorStore.sendMessage(pubMessage, messageId, clientId, null);
if(!messageDelivered) {
LOG.warn("PUBLISH message could not be delivered. MessageId={}, CId={}, topic={}", messageId, clientId, topicName);
}
return messageDelivered;
}
}
@@ -0,0 +1,621 @@
/*
* Copyright (c) 2012-2017 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 cn.wildfirechat.proto.WFCMessage;
import io.moquette.persistence.RPCCenter;
import io.moquette.interception.InterceptHandler;
import io.moquette.interception.messages.InterceptAcknowledgedMessage;
import io.moquette.persistence.MemorySessionStore;
import io.moquette.persistence.TargetEntry;
import io.moquette.server.ConnectionDescriptor;
import io.moquette.server.ConnectionDescriptorStore;
import io.moquette.server.Server;
import io.moquette.server.netty.NettyUtils;
import io.moquette.spi.*;
import io.moquette.spi.IMessagesStore.StoredMessage;
import io.moquette.spi.impl.security.AES;
import io.moquette.spi.security.IAuthenticator;
import io.moquette.spi.security.IAuthorizator;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.mqtt.*;
import io.netty.handler.timeout.IdleStateHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import win.liyufan.im.Utility;
import java.util.*;
import static io.moquette.spi.impl.InternalRepublisher.createPublishForQos;
import static io.moquette.spi.impl.Utils.readBytesAndRewind;
import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.*;
import static io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader.from;
import static io.netty.handler.codec.mqtt.MqttQoS.*;
import static io.moquette.server.ConnectionDescriptor.ConnectionState.*;
/**
* Class responsible to handle the logic of MQTT protocol it's the director of the protocol
* execution.
*
* Used by the front facing class ProtocolProcessorBootstrapper.
*/
public class ProtocolProcessor {
private void handleTargetRemovedFromCurrentNode(TargetEntry target) {
System.out.println("kickof user " + target);
if (target.type == TargetEntry.Type.TARGET_TYPE_USER) {
Collection<MemorySessionStore.Session> sessions = m_sessionsStore.sessionForUser(target.target);
for (MemorySessionStore.Session session : sessions) {
ConnectionDescriptor descriptor = connectionDescriptors.getConnection(session.getClientID());
try {
if (descriptor != null) {
processDisconnect(descriptor.getChannel(), true);
}
} catch (InterruptedException e) {
e.printStackTrace();
Utility.printExecption(LOG, e);
}
}
} else if(target.type == TargetEntry.Type.TARGET_TYPE_CHATROOM) {
}
}
private static final Logger LOG = LoggerFactory.getLogger(ProtocolProcessor.class);
protected ConnectionDescriptorStore connectionDescriptors;
private boolean allowAnonymous;
private boolean allowZeroByteClientId;
private IAuthorizator m_authorizator;
private IMessagesStore m_messagesStore;
private ISessionsStore m_sessionsStore;
private IAuthenticator m_authenticator;
private BrokerInterceptor m_interceptor;
public Qos1PublishHandler qos1PublishHandler;
private MessagesPublisher messagesPublisher;
private Server mServer;
ProtocolProcessor() {
}
/**
* @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
* true to allow clients connect without a clientid
* @param authorizator
* used to apply ACL policies to publishes and subscriptions.
* @param interceptor
* to notify events to an intercept handler
*/
void init(ConnectionDescriptorStore connectionDescriptors,
IMessagesStore storageService, ISessionsStore sessionsStore, IAuthenticator authenticator, IAuthorizator authorizator,
BrokerInterceptor interceptor, Server server) {
LOG.info("Initializing MQTT protocol processor...");
this.connectionDescriptors = connectionDescriptors;
this.m_interceptor = interceptor;
this.allowAnonymous = false;
this.allowZeroByteClientId = false;
m_authorizator = authorizator;
m_authenticator = authenticator;
m_messagesStore = storageService;
m_sessionsStore = sessionsStore;
LOG.info("Initializing messages publisher...");
final PersistentQueueMessageSender messageSender = new PersistentQueueMessageSender(this.connectionDescriptors);
this.messagesPublisher = new MessagesPublisher(connectionDescriptors, sessionsStore, messageSender, server.getHazelcastInstance(), m_messagesStore);
LOG.info("Initializing QoS publish handlers...");
this.qos1PublishHandler = new Qos1PublishHandler(m_authorizator, m_messagesStore, m_interceptor,
this.connectionDescriptors, this.messagesPublisher, sessionsStore, server.getImBusinessScheduler(), server);
mServer = server;
}
public void processConnect(Channel channel, MqttConnectMessage msg) {
MqttConnectPayload payload = msg.payload();
String clientId = payload.clientIdentifier();
LOG.info("Processing CONNECT message. CId={}, username={}", clientId, payload.userName());
if (msg.variableHeader().version() != MqttVersion.MQTT_3_1.protocolLevel()
&& msg.variableHeader().version() != MqttVersion.MQTT_3_1_1.protocolLevel()) {
MqttConnAckMessage badProto = connAck(CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION);
LOG.error("MQTT protocol version is not valid. CId={}", clientId);
channel.writeAndFlush(badProto);
channel.close();
return;
}
if (clientId == null || clientId.length() == 0) {
MqttConnAckMessage badId = connAck(CONNECTION_REFUSED_IDENTIFIER_REJECTED);
channel.writeAndFlush(badId);
channel.close();
LOG.error("The MQTT client ID cannot be empty. Username={}", payload.userName());
return;
}
if (!login(channel, msg, clientId)) {
channel.close();
return;
}
if (!mServer.m_initialized) {
channel.close();
return;
}
ConnectionDescriptor descriptor = new ConnectionDescriptor(clientId, channel);
ConnectionDescriptor existing = this.connectionDescriptors.addConnection(descriptor);
if (existing != null) {
LOG.info("The client ID is being used in an existing connection. It will be closed. CId={}", clientId);
this.connectionDescriptors.removeConnection(existing);
existing.abort();
this.connectionDescriptors.addConnection(descriptor);
}
initializeKeepAliveTimeout(channel, msg, clientId);
if (!sendAck(descriptor, msg, clientId)) {
channel.close();
return;
}
m_interceptor.notifyClientConnected(msg);
final ClientSession clientSession = createOrLoadClientSession(payload.userName(), descriptor, msg, clientId);
if (clientSession == null) {
MqttConnAckMessage badId = connAck(CONNECTION_REFUSED_SESSION_NOT_EXIST);
channel.writeAndFlush(badId);
channel.close();
return;
}
int flushIntervalMs = 500/* (keepAlive * 1000) / 2 */;
descriptor.setupAutoFlusher(flushIntervalMs);
final boolean success = descriptor.assignState(SESSION_CREATED, ESTABLISHED);
if (!success) {
channel.close();
return;
}
MemorySessionStore.Session session = m_sessionsStore.getSession(clientId);
if(session != null) {
session.refreshLastActiveTime();
}
LOG.info("The CONNECT message has been processed. CId={}, username={}", clientId, payload.userName());
}
private MqttConnAckMessage connAck(MqttConnectReturnCode returnCode) {
return connAck(returnCode, false);
}
private MqttConnAckMessage connAck(MqttConnectReturnCode returnCode, boolean sessionPresent) {
return connAck(returnCode, sessionPresent, null);
}
private MqttConnAckMessage connAck(MqttConnectReturnCode returnCode, byte[] data) {
return connAck(returnCode, false, data);
}
private MqttConnAckMessage connAckWithSessionPresent(MqttConnectReturnCode returnCode, byte[] data) {
return connAck(returnCode, true, data);
}
private MqttConnAckMessage connAck(MqttConnectReturnCode returnCode, boolean sessionPresent, byte[] data) {
MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE,
false, 0);
MqttConnAckVariableHeader mqttConnAckVariableHeader = new MqttConnAckVariableHeader(returnCode, sessionPresent);
return new MqttConnAckMessage(mqttFixedHeader, mqttConnAckVariableHeader, new MqttConnectAckPayload(data));
}
private boolean login(Channel channel, MqttConnectMessage msg, final String clientId) {
// handle user authentication
if (msg.variableHeader().hasUserName()) {
int status = m_messagesStore.getUserStatus(msg.payload().userName());
if (status == 2) {
failedBlocked(channel);
return false;
}
byte[] pwd = null;
if (msg.variableHeader().hasPassword()) {
pwd = msg.payload().password();
MemorySessionStore.Session session = m_sessionsStore.getSession(clientId);
if (session != null && session.getUsername().equals(msg.payload().userName())) {
pwd = AES.AESDecrypt(pwd, session.getSecret(), true);
} else {
LOG.error("Password decrypt failed of client {}", clientId);
failedCredentials(channel);
return false;
}
if (pwd == null) {
LOG.error("Password decrypt failed of client {}", clientId);
failedCredentials(channel);
return false;
}
} else if (!this.allowAnonymous) {
LOG.error("Client didn't supply any password and MQTT anonymous mode is disabled CId={}", clientId);
failedCredentials(channel);
return false;
}
if (!m_authenticator.checkValid(clientId, msg.payload().userName(), pwd)) {
LOG.error("Authenticator has rejected the MQTT credentials CId={}, username={}, password={}",
clientId, msg.payload().userName(), pwd);
failedCredentials(channel);
return false;
}
NettyUtils.userName(channel, msg.payload().userName());
} else if (!this.allowAnonymous) {
LOG.error("Client didn't supply any credentials and MQTT anonymous mode is disabled. CId={}", clientId);
failedCredentials(channel);
return false;
}
return true;
}
private boolean sendAck(ConnectionDescriptor descriptor, MqttConnectMessage msg, final String clientId) {
LOG.info("Sending connect ACK. CId={}", clientId);
final boolean success = descriptor.assignState(DISCONNECTED, SENDACK);
if (!success) {
return false;
}
MqttConnAckMessage okResp;
ClientSession clientSession = m_sessionsStore.sessionForClient(clientId);
boolean isSessionAlreadyStored = clientSession != null;
String user = msg.payload().userName();
long messageHead = m_messagesStore.getMessageHead(user);
long friendHead = m_messagesStore.getFriendHead(user);
long friendRqHead = m_messagesStore.getFriendRqHead(user);
long settingHead = m_messagesStore.getSettingHead(user);
WFCMessage.ConnectAckPayload payload = WFCMessage.ConnectAckPayload.newBuilder()
.setMsgHead(messageHead)
.setFriendHead(friendHead)
.setFriendRqHead(friendRqHead)
.setSettingHead(settingHead)
.setServerTime(System.currentTimeMillis())
.build();
if (!msg.variableHeader().isCleanSession() && isSessionAlreadyStored) {
okResp = connAckWithSessionPresent(CONNECTION_ACCEPTED, payload.toByteArray());
} else {
okResp = connAck(CONNECTION_ACCEPTED, payload.toByteArray());
}
descriptor.writeAndFlush(okResp);
LOG.info("The connect ACK has been sent. CId={}", clientId);
return true;
}
private void initializeKeepAliveTimeout(Channel channel, MqttConnectMessage msg, final String clientId) {
int keepAlive = msg.variableHeader().keepAliveTimeSeconds();
LOG.info("Configuring connection. CId={}", clientId);
NettyUtils.keepAlive(channel, keepAlive);
// session.attr(NettyUtils.ATTR_KEY_CLEANSESSION).set(msg.variableHeader().isCleanSession());
NettyUtils.cleanSession(channel, msg.variableHeader().isCleanSession());
// used to track the client in the subscription and publishing phases.
// session.attr(NettyUtils.ATTR_KEY_CLIENTID).set(msg.getClientID());
NettyUtils.clientID(channel, clientId);
int idleTime = Math.round(keepAlive * 1.5f);
setIdleTime(channel.pipeline(), idleTime);
LOG.debug("The connection has been configured CId={}, keepAlive={}, cleanSession={}, idleTime={}",
clientId, keepAlive, msg.variableHeader().isCleanSession(), idleTime);
}
private ClientSession createOrLoadClientSession(String username, ConnectionDescriptor descriptor, MqttConnectMessage msg,
String clientId) {
final boolean success = descriptor.assignState(SENDACK, SESSION_CREATED);
if (!success) {
return null;
}
ClientSession clientSession = m_sessionsStore.sessionForClient(clientId);
boolean isSessionAlreadyStored = clientSession != null;
if (isSessionAlreadyStored) {
clientSession = m_sessionsStore.updateExistSession(username, clientId, null, msg.variableHeader().isCleanSession());
} else {
return null;
}
return clientSession;
}
private void failedBlocked(Channel session) {
session.writeAndFlush(connAck(CONNECTION_REFUSED_IDENTIFIER_REJECTED));
LOG.info("Client {} failed to connect, use is blocked.", session);
}
private void failedCredentials(Channel session) {
session.writeAndFlush(connAck(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD));
LOG.info("Client {} failed to connect with bad username or password.", session);
}
private void setIdleTime(ChannelPipeline pipeline, int idleTime) {
if (pipeline.names().contains("idleStateHandler")) {
pipeline.remove("idleStateHandler");
}
pipeline.addFirst("idleStateHandler", new IdleStateHandler(idleTime, 0, 0));
}
public void processPubAck(Channel channel, MqttPubAckMessage msg) {
String clientID = NettyUtils.clientID(channel);
int messageID = msg.variableHeader().messageId();
String username = NettyUtils.userName(channel);
LOG.trace("retrieving inflight for messageID <{}>", messageID);
ClientSession targetSession = m_sessionsStore.sessionForClient(clientID);
StoredMessage inflightMsg = targetSession.inFlightAcknowledged(messageID);
String topic = inflightMsg.getTopic();
InterceptAcknowledgedMessage wrapped = new InterceptAcknowledgedMessage(inflightMsg, topic, username, messageID);
m_interceptor.notifyMessageAcknowledged(wrapped);
}
public static IMessagesStore.StoredMessage asStoredMessage(MqttPublishMessage msg) {
// TODO ugly, too much array copy
ByteBuf payload = msg.payload();
byte[] payloadContent = readBytesAndRewind(payload);
IMessagesStore.StoredMessage stored = new IMessagesStore.StoredMessage(payloadContent,
msg.fixedHeader().qosLevel(), msg.variableHeader().topicName());
stored.setRetained(msg.fixedHeader().isRetain());
return stored;
}
public void processPublish(Channel channel, MqttPublishMessage msg) {
final MqttQoS qos = msg.fixedHeader().qosLevel();
final String clientId = NettyUtils.clientID(channel);
LOG.info("Processing PUBLISH message. CId={}, topic={}, messageId={}, qos={}", clientId,
msg.variableHeader().topicName(), msg.variableHeader().packetId(), qos);
switch (qos) {
case AT_MOST_ONCE:
//not support
break;
case AT_LEAST_ONCE:
this.qos1PublishHandler.receivedPublishQos1(channel, msg);
break;
case EXACTLY_ONCE:
//not use
break;
default:
LOG.error("Unknown QoS-Type:{}", qos);
break;
}
}
/**
* Second phase of a publish QoS2 protocol, sent by publisher to the broker. Search the stored
* message and publish to all interested subscribers.
*
* @param channel
* the channel of the incoming message.
* @param msg
* the decoded pubrel message.
*/
public void processPubRel(Channel channel, MqttMessage msg) {
//not use
}
public void processPubRec(Channel channel, MqttMessage msg) {
//not use
}
public void processPubComp(Channel channel, MqttMessage msg) {
//not use
}
public void processDisconnect(Channel channel, boolean clearSession) throws InterruptedException {
final String clientID = NettyUtils.clientID(channel);
LOG.info("Processing DISCONNECT message. CId={}, clearSession={}", clientID, clearSession);
channel.flush();
if (clientID == null) {
LOG.error("Error. Cid not exist!!!", clientID, clearSession);
channel.close();
return;
}
if (!clearSession) {
processConnectionLost(clientID, channel);
return;
}
final ConnectionDescriptor existingDescriptor = this.connectionDescriptors.getConnection(clientID);
if (existingDescriptor == null) {
// another client with same ID removed the descriptor, we must exit
channel.close();
return;
}
if (existingDescriptor.doesNotUseChannel(channel)) {
// another client saved it's descriptor, exit
LOG.warn("Another client is using the connection descriptor. Closing connection. CId={}", clientID);
existingDescriptor.abort();
return;
}
if (!dropStoredMessages(existingDescriptor, clientID)) {
LOG.warn("Unable to drop stored messages. Closing connection. CId={}", clientID);
existingDescriptor.abort();
return;
}
if (!notifyInterceptorDisconnected(existingDescriptor, clientID)) {
LOG.warn("Unable to drop will message. Closing connection. CId={}", clientID);
existingDescriptor.abort();
return;
}
if (!existingDescriptor.close()) {
LOG.info("The connection has been closed. CId={}", clientID);
return;
}
boolean stillPresent = this.connectionDescriptors.removeConnection(existingDescriptor);
if (!stillPresent) {
// another descriptor was inserted
LOG.warn("Another descriptor has been inserted. CId={}", clientID);
return;
}
LOG.info("The DISCONNECT message has been processed. CId={}", clientID);
//disconnect the session
m_sessionsStore.sessionForClient(clientID).disconnect(clearSession);
}
private boolean dropStoredMessages(ConnectionDescriptor descriptor, String clientID) {
final boolean success = descriptor.assignState(ESTABLISHED, MESSAGES_DROPPED);
if (!success) {
return false;
}
LOG.debug("Removing messages of session. CId={}", descriptor.clientID);
this.m_sessionsStore.dropQueue(clientID);
LOG.debug("The messages of the session have been removed. CId={}", descriptor.clientID);
return true;
}
private boolean notifyInterceptorDisconnected(ConnectionDescriptor descriptor, String clientID) {
final boolean success = descriptor.assignState(MESSAGES_DROPPED, INTERCEPTORS_NOTIFIED);
if (!success) {
return false;
}
LOG.info("Removing will message. ClientId={}", descriptor.clientID);
// cleanup the will store
String username = descriptor.getUsername();
m_interceptor.notifyClientDisconnected(clientID, username);
return true;
}
public void processConnectionLost(String clientID, Channel channel) {
LOG.info("Processing connection lost event. CId={}", clientID);
ConnectionDescriptor oldConnDescr = new ConnectionDescriptor(clientID, channel);
if(connectionDescriptors.removeConnection(oldConnDescr)) {
MemorySessionStore.Session session = m_sessionsStore.getSession(clientID);
if(session != null) {
session.refreshLastActiveTime();
}
String username = NettyUtils.userName(channel);
m_interceptor.notifyClientConnectionLost(clientID, username);
}
}
/**
* Remove the clientID from topic subscription, if not previously subscribed, doesn't reply any
* error.
*
* @param channel
* the channel of the incoming message.
* @param msg
* the decoded unsubscribe message.
*/
public void processUnsubscribe(Channel channel, MqttUnsubscribeMessage msg) {
//Not use
}
public void processSubscribe(Channel channel, MqttSubscribeMessage msg) {
//not use
}
/**
* Create the SUBACK response from a list of topicFilters
*/
private MqttSubAckMessage doAckMessageFromValidateFilters(List<MqttTopicSubscription> topicFilters, int messageId) {
List<Integer> grantedQoSLevels = new ArrayList<>();
for (MqttTopicSubscription req : topicFilters) {
grantedQoSLevels.add(req.qualityOfService().value());
}
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.SUBACK, false, AT_LEAST_ONCE, false, 0);
MqttSubAckPayload payload = new MqttSubAckPayload(grantedQoSLevels);
return new MqttSubAckMessage(fixedHeader, from(messageId), payload);
}
public void notifyChannelWritable(Channel channel) {
String clientID = NettyUtils.clientID(channel);
ClientSession clientSession = m_sessionsStore.sessionForClient(clientID);
boolean emptyQueue = false;
while (channel.isWritable() && !emptyQueue) {
StoredMessage msg = clientSession.queue().poll();
if (msg == null) {
emptyQueue = true;
} else {
// recreate a publish from stored publish in queue
MqttPublishMessage pubMsg = createPublishForQos( msg.getTopic(), msg.getQos(), msg.getPayload(),
msg.isRetained(), 0);
channel.write(pubMsg);
}
}
channel.flush();
}
public void addInterceptHandler(InterceptHandler interceptHandler) {
this.m_interceptor.addInterceptHandler(interceptHandler);
}
public void removeInterceptHandler(InterceptHandler interceptHandler) {
this.m_interceptor.removeInterceptHandler(interceptHandler);
}
public IMessagesStore getMessagesStore() {
return m_messagesStore;
}
public ISessionsStore getSessionsStore() {
return m_sessionsStore;
}
public void onRpcMsg(String fromUser, String clientId, byte[] message, int messageId, String from, String request) {
if(request.equals(RPCCenter.KICKOFF_USER_REQUEST)) {
mServer.getImBusinessScheduler().execute(()->handleTargetRemovedFromCurrentNode(new TargetEntry(TargetEntry.Type.TARGET_TYPE_USER, from)));
return;
}
qos1PublishHandler.onRpcMsg(fromUser, clientId, message, messageId, from, request);
}
public void shutdown() {
messagesPublisher.stopChatroomScheduler();
}
}
@@ -0,0 +1,175 @@
/*
* Copyright (c) 2012-2017 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.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.moquette.BrokerConstants;
import io.moquette.interception.InterceptHandler;
import io.moquette.server.ConnectionDescriptorStore;
import io.moquette.server.Server;
import io.moquette.server.config.IConfig;
import io.moquette.spi.IMessagesStore;
import io.moquette.spi.ISessionsStore;
import io.moquette.spi.IStore;
import io.moquette.spi.impl.security.PermitAllAuthorizator;
import io.moquette.spi.impl.security.TokenAuthenticator;
import io.moquette.spi.security.IAuthenticator;
import io.moquette.spi.security.IAuthorizator;
/**
* It's main responsibility is bootstrap the ProtocolProcessor.
*/
public class ProtocolProcessorBootstrapper {
private static final Logger LOG = LoggerFactory.getLogger(ProtocolProcessorBootstrapper.class);
public static final String INMEMDB_STORE_CLASS = "io.moquette.persistence.MemoryStorageService";
private ISessionsStore m_sessionsStore;
private Runnable storeShutdown;
private final ProtocolProcessor m_processor = new ProtocolProcessor();
private ConnectionDescriptorStore connectionDescriptors;
public ProtocolProcessorBootstrapper() {
}
/**
* Initialize the processing part of the broker.
*
* @param props
* the properties carrier where some props like port end host could be loaded. For
* the full list check of configurable properties check wildfirechat.conf file.
* @param embeddedObservers
* a list of callbacks to be notified of certain events inside the broker. Could be
* empty list of null.
* @param authenticator
* an implementation of the authenticator to be used, if null load that specified in
* config and fallback on the default one (permit all).
* @param authorizator
* an implementation of the authorizator to be used, if null load that specified in
* config and fallback on the default one (permit all).
* @param server
* the server to init.
* @return the processor created for the broker.
*/
public ProtocolProcessor init(IConfig props, List<? extends InterceptHandler> embeddedObservers,
IAuthenticator authenticator, IAuthorizator authorizator, Server server, IStore store) {
IMessagesStore messagesStore;
messagesStore = store.messagesStore();
m_sessionsStore = store.sessionsStore();
storeShutdown = new Runnable() {
@Override
public void run() {
store.close();
}
};
LOG.info("Configuring message interceptors...");
List<InterceptHandler> observers = new ArrayList<>(embeddedObservers);
String interceptorClassName = props.getProperty(BrokerConstants.INTERCEPT_HANDLER_PROPERTY_NAME);
if (interceptorClassName != null && !interceptorClassName.isEmpty()) {
InterceptHandler handler = loadClass(interceptorClassName, InterceptHandler.class, Server.class, server);
if (handler != null) {
observers.add(handler);
}
}
BrokerInterceptor interceptor = new BrokerInterceptor(props, observers);
LOG.info("Configuring MQTT authenticator...");
authenticator = new TokenAuthenticator();
LOG.info("Configuring MQTT authorizator...");
String authorizatorClassName = props.getProperty(BrokerConstants.AUTHORIZATOR_CLASS_NAME, "");
if (authorizator == null && !authorizatorClassName.isEmpty()) {
authorizator = loadClass(authorizatorClassName, IAuthorizator.class, IConfig.class, props);
}
if (authorizator == null) {
authorizator = new PermitAllAuthorizator();
LOG.info("An {} authorizator instance will be used", authorizator.getClass().getName());
}
LOG.info("Initializing connection descriptor store...");
connectionDescriptors = new ConnectionDescriptorStore(m_sessionsStore);
LOG.info("Initializing MQTT protocol processor...");
m_processor.init(connectionDescriptors, messagesStore, m_sessionsStore, authenticator, authorizator, interceptor, server);
return m_processor;
}
@SuppressWarnings("unchecked")
private <T, U> T loadClass(String className, Class<T> intrface, Class<U> constructorArgClass, U props) {
T instance = null;
try {
// check if constructor with constructor arg class parameter
// exists
LOG.info("Invoking constructor with {} argument. ClassName={}, interfaceName={}",
constructorArgClass.getName(), className, intrface.getName());
instance = this.getClass().getClassLoader()
.loadClass(className)
.asSubclass(intrface)
.getConstructor(constructorArgClass)
.newInstance(props);
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException ex) {
LOG.warn("Unable to invoke constructor with {} argument. ClassName={}, interfaceName={}, cause={}, errorMessage={}",
constructorArgClass.getName(), className, intrface.getName(), ex.getCause(), ex.getMessage());
return null;
} catch (NoSuchMethodException | InvocationTargetException e) {
try {
LOG.info("Invoking default constructor. ClassName={}, interfaceName={}",
className, intrface.getName());
// fallback to default constructor
instance = this.getClass().getClassLoader()
.loadClass(className)
.asSubclass(intrface)
.newInstance();
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException ex) {
LOG.error("Unable to invoke default constructor. ClassName={}, interfaceName={}, cause={}, errorMessage={}",
className, intrface.getName(), ex.getCause(), ex.getMessage());
return null;
}
}
return instance;
}
public ISessionsStore getSessionsStore() {
return m_sessionsStore;
}
public void shutdown() {
if (storeShutdown != null)
storeShutdown.run();
if (m_processor != null)
m_processor.shutdown();
// if (m_interceptor != null)
// m_interceptor.stop();
}
public ConnectionDescriptorStore getConnectionDescriptors() {
return connectionDescriptors;
}
}
@@ -0,0 +1,264 @@
/*
* Copyright (c) 2012-2017 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 static io.moquette.spi.impl.Utils.readBytesAndRewind;
import static io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader.from;
import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE;
import static win.liyufan.im.ErrorCode.ERROR_CODE_NOT_IMPLEMENT;
import static win.liyufan.im.ErrorCode.ERROR_CODE_OVER_FREQUENCY;
import static win.liyufan.im.ErrorCode.ERROR_CODE_SUCCESS;
import java.io.IOException;
import java.util.*;
import cn.wildfirechat.server.ThreadPoolExecutorWrapper;
import com.google.gson.Gson;
import com.xiaoleilu.loServer.RestResult;
import com.xiaoleilu.loServer.action.ClassUtil;
import com.xiaoleilu.loServer.pojos.OutputCheckUserOnline;
import io.moquette.persistence.RPCCenter;
import io.moquette.imhandler.Handler;
import io.moquette.imhandler.IMHandler;
import io.moquette.persistence.MemorySessionStore;
import io.moquette.server.ConnectionDescriptor;
import io.moquette.server.Server;
import io.moquette.spi.ClientSession;
import io.moquette.spi.impl.security.AES;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.internal.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.moquette.server.ConnectionDescriptorStore;
import io.moquette.server.netty.NettyUtils;
import io.moquette.spi.IMessagesStore;
import io.moquette.spi.ISessionsStore;
import io.moquette.spi.impl.subscriptions.Topic;
import io.moquette.spi.security.IAuthorizator;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.handler.codec.mqtt.MqttFixedHeader;
import io.netty.handler.codec.mqtt.MqttMessageType;
import io.netty.handler.codec.mqtt.MqttPublishMessage;
import win.liyufan.im.ErrorCode;
import win.liyufan.im.IMTopic;
import win.liyufan.im.RateLimiter;
import win.liyufan.im.Utility;
import win.liyufan.im.extended.mqttmessage.ModifiedMqttPubAckMessage;
public class Qos1PublishHandler extends QosPublishHandler {
private static final Logger LOG = LoggerFactory.getLogger(Qos1PublishHandler.class);
private final IMessagesStore m_messagesStore;
private final ConnectionDescriptorStore connectionDescriptors;
private final MessagesPublisher publisher;
private final ISessionsStore m_sessionStore;
private final ThreadPoolExecutorWrapper m_imBusinessExecutor;
private final RateLimiter mLimitCounter = new RateLimiter(5, 100);
private HashMap<String, IMHandler> m_imHandlers = new HashMap<>();
public Qos1PublishHandler(IAuthorizator authorizator, IMessagesStore messagesStore, BrokerInterceptor interceptor,
ConnectionDescriptorStore connectionDescriptors, MessagesPublisher messagesPublisher,
ISessionsStore sessionStore, ThreadPoolExecutorWrapper executorService, Server server) {
super(authorizator);
this.m_messagesStore = messagesStore;
this.connectionDescriptors = connectionDescriptors;
this.publisher = messagesPublisher;
this.m_sessionStore = sessionStore;
this.m_imBusinessExecutor = executorService;
IMHandler.init(m_messagesStore, m_sessionStore, publisher, m_imBusinessExecutor, server);
registerAllAction();
}
private void registerAllAction() {
try {
for (Class cls:ClassUtil.getAllAssignedClass(IMHandler.class)) {
Handler annotation = (Handler)cls.getAnnotation(Handler.class);
if(annotation != null) {
IMHandler handler = (IMHandler) com.xiaoleilu.hutool.util.ClassUtil.newInstance(cls);
m_imHandlers.put(annotation.value(), handler);
}
}
} catch (IOException e) {
e.printStackTrace();
Utility.printExecption(LOG, e);
} catch (ClassNotFoundException e) {
e.printStackTrace();
Utility.printExecption(LOG, e);
}
}
public void onRpcMsg(String fromUser, String clientId, byte[] message, int requestId, String from, String request) {
if (request.equals(RPCCenter.CHECK_USER_ONLINE_REQUEST)) {
checkUserOnlineHandler(message, ackPayload -> RPCCenter.getInstance().sendResponse(ERROR_CODE_SUCCESS.getCode(), ackPayload, from, requestId));
} else {
imHandler(clientId, fromUser, request, message, (errorCode, ackPayload) -> {
if (requestId > 0) {
byte[] response = new byte[ackPayload.readableBytes()];
ackPayload.readBytes(response);
ReferenceCountUtil.release(ackPayload);
RPCCenter.getInstance().sendResponse(ERROR_CODE_SUCCESS.getCode(), response, from, requestId);
}
});
}
}
void checkUserOnlineHandler(byte[] payloadContent, RouteCallback callback) {
m_imBusinessExecutor.execute(() -> {
String userId = new String(payloadContent);
int status;
ClientSession clientSession = m_sessionStore.sessionForClient(userId);
if (clientSession == null) {
status = 2;
} else {
ConnectionDescriptor descriptor = connectionDescriptors.getConnection(clientSession.clientID);
if (descriptor == null) {
status = 1;
} else {
status = 0;
}
}
OutputCheckUserOnline out = new OutputCheckUserOnline();
out.setStatus(status);
RestResult result = RestResult.ok(out);
callback.onRouteHandled(new Gson().toJson(result).getBytes());
});
}
void imHandler(String clientID, String fromUser, String topic, byte[] payloadContent, IMCallback callback) {
LOG.info("imHandler fromUser={}, topic={}", fromUser, topic);
if(!mLimitCounter.isGranted(clientID + fromUser + topic)) {
ByteBuf ackPayload = Unpooled.buffer();
ackPayload.ensureWritable(1).writeByte(ERROR_CODE_OVER_FREQUENCY.getCode());
try {
callback.onIMHandled(ERROR_CODE_OVER_FREQUENCY, ackPayload);
} catch (Exception e) {
e.printStackTrace();
Utility.printExecption(LOG, e);
}
LOG.warn("user {} request over frequency", fromUser);
return;
}
IMCallback wrapper = (errorcode, ackPayload) -> {
ackPayload.resetReaderIndex();
byte code = ackPayload.readByte();
if(ackPayload.readableBytes() > 0) {
byte[] data = new byte[ackPayload.readableBytes()];
ackPayload.getBytes(1, data);
try {
//clientID 为空的是server api请求。客户端不允许clientID为空
if (!StringUtil.isNullOrEmpty(clientID)) {
//在route时,使用系统根密钥。当route成功后,用户都使用用户密钥
if (topic.equals(IMTopic.GetTokenTopic)) {
data = AES.AESEncrypt(data, "");
} else {
MemorySessionStore.Session session = m_sessionStore.getSession(clientID);
if (session != null && session.getUsername().equals(fromUser))
data = AES.AESEncrypt(data, session.getSecret());
}
}
ackPayload.clear();
ackPayload.resetWriterIndex();
ackPayload.writeByte(code);
ackPayload.writeBytes(data);
} catch (Exception e) {
e.printStackTrace();
Utility.printExecption(LOG, e);
}
}
ackPayload.resetReaderIndex();
try {
callback.onIMHandled(errorcode, ackPayload);
} catch (Exception e) {
e.printStackTrace();
Utility.printExecption(LOG, e);
}
};
IMHandler handler = m_imHandlers.get(topic);
if (handler != null) {
handler.doHandler(clientID, fromUser, topic, payloadContent, wrapper);
} else {
LOG.error("imHandler unknown topic={}", topic);
ByteBuf ackPayload = Unpooled.buffer();
ackPayload.ensureWritable(1).writeByte(ERROR_CODE_NOT_IMPLEMENT.getCode());
try {
wrapper.onIMHandled(ERROR_CODE_NOT_IMPLEMENT, ackPayload);
} catch (Exception e) {
e.printStackTrace();
Utility.printExecption(LOG, e);
}
}
}
public interface IMCallback {
void onIMHandled(ErrorCode errorCode, ByteBuf ackPayload);
}
interface RouteCallback {
void onRouteHandled(byte[] ackPayload);
}
void receivedPublishQos1(Channel channel, MqttPublishMessage msg) {
// verify if topic can be write
final Topic topic = new Topic(msg.variableHeader().topicName());
String clientID = NettyUtils.clientID(channel);
String username = NettyUtils.userName(channel);
if (!m_authorizator.canWrite(topic, username, clientID)) {
LOG.error("MQTT client is not authorized to publish on topic. CId={}, topic={}", clientID, topic);
return;
}
final int messageID = msg.variableHeader().packetId();
String imtopic = topic.getTopic();
ByteBuf payload = msg.payload();
byte[] payloadContent = readBytesAndRewind(payload);
MemorySessionStore.Session session = m_sessionStore.getSession(clientID);
payloadContent = AES.AESDecrypt(payloadContent, session.getSecret(), true);
imHandler(clientID, username, imtopic, payloadContent, (errorCode, ackPayload) -> sendPubAck(clientID, messageID, ackPayload, errorCode));
}
private void sendPubAck(String clientId, int messageID, ByteBuf payload, ErrorCode errorCode) {
LOG.trace("sendPubAck invoked");
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBACK, false, AT_MOST_ONCE, false, 0);
ModifiedMqttPubAckMessage pubAckMessage = new ModifiedMqttPubAckMessage(fixedHeader, from(messageID), payload);
try {
if (connectionDescriptors == null) {
throw new RuntimeException("Internal bad error, found connectionDescriptors to null while it should " +
"be initialized, somewhere it's overwritten!!");
}
LOG.debug("clientIDs are {}", connectionDescriptors);
if (!connectionDescriptors.isConnected(clientId)) {
throw new RuntimeException(String.format("Can't find a ConnectionDescriptor for client %s in cache %s",
clientId, connectionDescriptors));
}
connectionDescriptors.sendMessage(pubAckMessage, messageID, clientId, errorCode);
} catch (Throwable t) {
LOG.error(null, t);
}
}
}
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2012-2017 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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.moquette.server.netty.NettyUtils;
import io.moquette.spi.impl.subscriptions.Topic;
import io.moquette.spi.security.IAuthorizator;
import io.netty.channel.Channel;
abstract class QosPublishHandler {
private static final Logger LOG = LoggerFactory.getLogger(QosPublishHandler.class);
protected final IAuthorizator m_authorizator;
protected QosPublishHandler(IAuthorizator m_authorizator) {
this.m_authorizator = m_authorizator;
}
public boolean checkWriteOnTopic(Topic topic, Channel channel) {
String clientID = NettyUtils.clientID(channel);
String username = NettyUtils.userName(channel);
if (!m_authorizator.canWrite(topic, username, clientID)) {
LOG.error("MQTT client is not authorized to publish on topic. CId={}, topic={}", clientID, topic);
return true;
}
return false;
}
}
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2012-2017 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() {
}
}
@@ -0,0 +1,114 @@
/*
* Copyright (c) 2012-2017 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}"
*/
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 {
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
*
* @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();
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;
}
private ACLFileParser() {
}
}
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2012-2017 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 {
public boolean checkValid(String clientId, String username, byte[] password) {
return true;
}
}
@@ -0,0 +1,73 @@
/*
* Copyright (c) 2012-2017 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;
}
}
@@ -0,0 +1,161 @@
/*
* Copyright (c) 2012-2017 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.IAuthorizator;
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 IAuthorizator {
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();
}
}
@@ -0,0 +1,112 @@
/*
* Copyright (c) 2012-2017 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.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 win.liyufan.im.Utility;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.*;
/**
* 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 final PreparedStatement preparedStatement;
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) {
try {
Class.forName(driver);
final Connection connection = DriverManager.getConnection(jdbcUrl);
this.messageDigest = MessageDigest.getInstance(digestMethod);
this.preparedStatement = connection.prepareStatement(sqlQuery);
} catch (ClassNotFoundException cnfe) {
LOG.error(String.format("Can't find driver %s", driver), cnfe);
throw new RuntimeException(cnfe);
} catch (SQLException sqle) {
LOG.error(String.format("Can't connect to %s", jdbcUrl), sqle);
throw new RuntimeException(sqle);
} 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 r = null;
try {
this.preparedStatement.setString(1, username);
r = this.preparedStatement.executeQuery();
if (r.next()) {
final String foundPwq = r.getString(1);
messageDigest.update(password);
byte[] digest = messageDigest.digest();
String encodedPasswd = new String(Hex.encodeHex(digest));
return foundPwq.equals(encodedPasswd);
}
r.close();
} catch (SQLException e) {
e.printStackTrace();
Utility.printExecption(LOG, e);
}
return false;
}
@Override
protected void finalize() throws Throwable {
this.preparedStatement.close();
this.preparedStatement.getConnection().close();
super.finalize();
}
}
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2012-2017 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.IAuthorizator;
public class DenyAllAuthorizator implements IAuthorizator {
@Override
public boolean canWrite(Topic topic, String user, String client) {
return true;
}
@Override
public boolean canRead(Topic topic, String user, String client) {
return true;
}
}
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2012-2017 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);
}
}
@@ -0,0 +1,5 @@
package io.moquette.spi.impl.security;
public interface ITokenGenerator {
public String generateToken(String username);
}
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2012-2017 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.IAuthorizator;
public class PermitAllAuthorizator implements IAuthorizator {
@Override
public boolean canWrite(Topic topic, String user, String client) {
return true;
}
@Override
public boolean canRead(Topic topic, String user, String client) {
return true;
}
}
@@ -0,0 +1,129 @@
/*
* Copyright (c) 2012-2017 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);
}
}
@@ -0,0 +1,33 @@
package io.moquette.spi.impl.security;
import java.io.IOException;
import io.moquette.spi.security.IAuthenticator;
import io.moquette.spi.security.Tokenor;
public class TokenAuthenticator implements IAuthenticator, ITokenGenerator {
public static void main(String[] args) throws IOException {
TokenAuthenticator authenticator = new TokenAuthenticator();
String strToken = authenticator.generateToken("user1");
if (authenticator.checkValid(null, "user1", strToken.getBytes())) {
System.out.println("pass" + strToken);
} else {
System.out.println("fail" + strToken);
}
}
@Override
public boolean checkValid(String clientId, String username, byte[] password) {
String id = Tokenor.getUserId(password);
if (id != null && id.equals(username)) {
return true;
}
return false;
}
@Override
public String generateToken(String username) {
return Tokenor.getToken(username);
}
}
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2012-2017 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,183 @@
/*
* Copyright (c) 2012-2017 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.List;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Topic implements Serializable {
private static final Logger LOG = LoggerFactory.getLogger(Topic.class);
private static final long serialVersionUID = 2438799283749822L;
private final String topic;
private transient List<Token> tokens;
private transient boolean valid;
public Topic(String topic) {
this.topic = topic;
}
public String getTopic() {
return topic;
}
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 {
List<Token> res = new ArrayList<>();
String[] splitted = topic.split("/");
if (splitted.length == 0) {
res.add(Token.EMPTY);
}
if (topic.endsWith("/")) {
// Add a fictious space
String[] newSplitted = new String[splitted.length + 1];
System.arraycopy(splitted, 0, newSplitted, 0, splitted.length);
newSplitted[splitted.length] = "";
splitted = newSplitted;
}
for (int i = 0; i < splitted.length; i++) {
String s = splitted[i];
if (s.isEmpty()) {
// if (i != 0) {
// throw new ParseException("Bad format of topic, expetec topic name between
// separators", i);
// }
res.add(Token.EMPTY);
} else if (s.equals("#")) {
// check that multi is the last symbol
if (i != splitted.length - 1) {
throw new ParseException(
"Bad format of topic, the multi symbol (#) has to be the last one after a separator",
i);
}
res.add(Token.MULTI);
} else if (s.contains("#")) {
throw new ParseException("Bad format of topic, invalid subtopic name: " + s, i);
} else if (s.equals("+")) {
res.add(Token.SINGLE);
} else if (s.contains("+")) {
throw new ParseException("Bad format of topic, invalid subtopic name: " + s, i);
} else {
res.add(new Token(s));
}
}
return res;
}
public boolean isValid() {
if (tokens == null)
getTokens();
return valid;
}
/**
* Verify if the 2 topics matching respecting the rules of MQTT Appendix A
*
* @param subscriptionTopic
* the topic filter of the subscription
* @return true if the two topics match.
*/
// TODO reimplement with iterators or with queues
public boolean match(Topic subscriptionTopic) {
List<Token> msgTokens = getTokens();
List<Token> subscriptionTokens = subscriptionTopic.getTokens();
int i = 0;
for (; i < subscriptionTokens.size(); i++) {
Token subToken = subscriptionTokens.get(i);
if (subToken != Token.MULTI && subToken != Token.SINGLE) {
if (i >= msgTokens.size()) {
return false;
}
Token msgToken = msgTokens.get(i);
if (!msgToken.equals(subToken)) {
return false;
}
} else {
if (subToken == Token.MULTI) {
return true;
}
if (subToken == Token.SINGLE) {
// skip a step forward
}
}
}
// if last token was a SINGLE then treat it as an empty
// if (subToken == Token.SINGLE && (i - msgTokens.size() == 1)) {
// i--;
// }
return i == msgTokens.size();
}
@Override
public 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();
}
/**
* Factory method
* */
public static Topic asTopic(String s) {
return new Topic(s);
}
}