wildfire chat server source initial submit
This commit is contained in:
Regular → Executable
+66
-21
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* 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
|
||||
@@ -23,34 +23,25 @@ public final class BrokerConstants {
|
||||
public static final String INTERCEPT_HANDLER_PROPERTY_NAME = "intercept.handler";
|
||||
public static final String BROKER_INTERCEPTOR_THREAD_POOL_SIZE = "intercept.thread_pool.size";
|
||||
public static final String PERSISTENT_STORE_PROPERTY_NAME = "persistent_store";
|
||||
public static final String AUTOSAVE_INTERVAL_PROPERTY_NAME = "autosave_interval";
|
||||
public static final String PASSWORD_FILE_PROPERTY_NAME = "password_file";
|
||||
public static final String SERVER_IP_PROPERTY_NAME = "server.ip";
|
||||
public static final String PORT_PROPERTY_NAME = "port";
|
||||
public static final String HOST_PROPERTY_NAME = "host";
|
||||
public static final String DEFAULT_MOQUETTE_STORE_H2_DB_FILENAME = "moquette_store.h2";
|
||||
public static final String HTTP_SERVER_PORT = "http_port";
|
||||
public static final String HTTP_LOCAL_PORT = "local_port";
|
||||
|
||||
public static final String HTTP_ADMIN_PORT = "http.admin.port";
|
||||
public static final String NODE_ID = "node_id";
|
||||
public static final String NODE_IDS = "node_ids";
|
||||
public static final String DEFAULT_MOQUETTE_STORE_MAP_DB_FILENAME = "moquette_store.mapdb";
|
||||
public static final String DEFAULT_PERSISTENT_PATH = System.getProperty("user.dir") + File.separator
|
||||
+ DEFAULT_MOQUETTE_STORE_H2_DB_FILENAME;
|
||||
+ DEFAULT_MOQUETTE_STORE_MAP_DB_FILENAME;
|
||||
public static final String WEB_SOCKET_PORT_PROPERTY_NAME = "websocket_port";
|
||||
public static final String WSS_PORT_PROPERTY_NAME = "secure_websocket_port";
|
||||
|
||||
/**
|
||||
* Defines the SSL implementation to use, default to "JDK".
|
||||
* @see io.netty.handler.ssl.SslProvider#name()
|
||||
*/
|
||||
public static final String SSL_PROVIDER = "ssl_provider";
|
||||
public static final String SSL_PORT_PROPERTY_NAME = "ssl_port";
|
||||
public static final String JKS_PATH_PROPERTY_NAME = "jks_path";
|
||||
|
||||
/** @see java.security.KeyStore#getInstance(String) for allowed types, default to "jks" */
|
||||
public static final String KEY_STORE_TYPE = "key_store_type";
|
||||
public static final String KEY_STORE_PASSWORD_PROPERTY_NAME = "key_store_password";
|
||||
public static final String KEY_MANAGER_PASSWORD_PROPERTY_NAME = "key_manager_password";
|
||||
public static final String ALLOW_ANONYMOUS_PROPERTY_NAME = "allow_anonymous";
|
||||
public static final String REAUTHORIZE_SUBSCRIPTIONS_ON_CONNECT = "reauthorize_subscriptions_on_connect";
|
||||
public static final String ALLOW_ZERO_BYTE_CLIENT_ID_PROPERTY_NAME = "allow_zero_byte_client_id";
|
||||
public static final String ACL_FILE_PROPERTY_NAME = "acl_file";
|
||||
public static final String AUTHORIZATOR_CLASS_NAME = "authorizator_class";
|
||||
public static final String AUTHENTICATOR_CLASS_NAME = "authenticator_class";
|
||||
public static final String DB_AUTHENTICATOR_DRIVER = "authenticator.db.driver";
|
||||
public static final String DB_AUTHENTICATOR_URL = "authenticator.db.url";
|
||||
public static final String DB_AUTHENTICATOR_QUERY = "authenticator.db.query";
|
||||
@@ -60,14 +51,15 @@ public final class BrokerConstants {
|
||||
public static final String DISABLED_PORT_BIND = "disabled";
|
||||
public static final String HOST = "0.0.0.0";
|
||||
public static final String NEED_CLIENT_AUTH = "need_client_auth";
|
||||
public static final String HAZELCAST_CONFIGURATION = "hazelcast.configuration";
|
||||
public static final String HAZELCAST_CLIENT_IP = "hazelcast.client.ip";
|
||||
public static final String HAZELCAST_CLIENT_PORT = "hazelcast.client.port";
|
||||
public static final String NETTY_SO_BACKLOG_PROPERTY_NAME = "netty.so_backlog";
|
||||
public static final String NETTY_SO_REUSEADDR_PROPERTY_NAME = "netty.so_reuseaddr";
|
||||
public static final String NETTY_TCP_NODELAY_PROPERTY_NAME = "netty.tcp_nodelay";
|
||||
public static final String NETTY_SO_KEEPALIVE_PROPERTY_NAME = "netty.so_keepalive";
|
||||
public static final String NETTY_CHANNEL_TIMEOUT_SECONDS_PROPERTY_NAME = "netty.channel_timeout.seconds";
|
||||
public static final String NETTY_EPOLL_PROPERTY_NAME = "netty.epoll";
|
||||
public static final String NETTY_MAX_BYTES_PROPERTY_NAME = "netty.mqtt.message_size";
|
||||
public static final int DEFAULT_NETTY_MAX_BYTES_IN_MESSAGE = 8092;
|
||||
public static final String METRICS_ENABLE_PROPERTY_NAME = "use_metrics";
|
||||
public static final String METRICS_LIBRATO_EMAIL_PROPERTY_NAME = "metrics.librato.email";
|
||||
public static final String METRICS_LIBRATO_TOKEN_PROPERTY_NAME = "metrics.librato.token";
|
||||
@@ -78,6 +70,59 @@ public final class BrokerConstants {
|
||||
|
||||
public static final String STORAGE_CLASS_NAME = "storage_class";
|
||||
|
||||
public static final String QINIU_SERVER_URL= "qiniu.server_url";
|
||||
public static final String QINIU_ACCESS_KEY = "qiniu.access_key";
|
||||
public static final String QINIU_SECRET_KEY = "qiniu.secret_key";
|
||||
|
||||
public static final String QINIU_BUCKET_GENERAL_NAME= "qiniu.bucket_general_name";
|
||||
public static final String QINIU_BUCKET_GENERAL_DOMAIN = "qiniu.bucket_general_domain";
|
||||
public static final String QINIU_BUCKET_IMAGE_NAME= "qiniu.bucket_image_name";
|
||||
public static final String QINIU_BUCKET_IMAGE_DOMAIN = "qiniu.bucket_image_domain";
|
||||
public static final String QINIU_BUCKET_VOICE_NAME= "qiniu.bucket_voice_name";
|
||||
public static final String QINIU_BUCKET_VOICE_DOMAIN = "qiniu.bucket_voice_domain";
|
||||
public static final String QINIU_BUCKET_VIDEO_NAME= "qiniu.bucket_video_name";
|
||||
public static final String QINIU_BUCKET_VIDEO_DOMAIN = "qiniu.bucket_video_domain";
|
||||
public static final String QINIU_BUCKET_FILE_NAME= "qiniu.bucket_file_name";
|
||||
public static final String QINIU_BUCKET_FILE_DOMAIN = "qiniu.bucket_file_domain";
|
||||
public static final String QINIU_BUCKET_PORTRAIT_NAME= "qiniu.bucket_portrait_name";
|
||||
public static final String QINIU_BUCKET_PORTRAIT_DOMAIN = "qiniu.bucket_portrait_domain";
|
||||
public static final String QINIU_BUCKET_FAVORITE_NAME= "qiniu.bucket_favorite_name";
|
||||
public static final String QINIU_BUCKET_FAVORITE_DOMAIN = "qiniu.bucket_favorite_domain";
|
||||
|
||||
public static final String FILE_STORAGE_ROOT = "local.media.storage.root";
|
||||
public static final String USER_QINIU = "media.server.use_qiniu";
|
||||
|
||||
public static final String PUSH_ANDROID_SERVER_ADDRESS = "push.android.server.address";
|
||||
public static final String PUSH_IOS_SERVER_ADDRESS = "push.ios.server.address";
|
||||
|
||||
public static final String HZ_Cluster_Node_External_IP = "node_external_ip";
|
||||
public static final String HZ_Cluster_Node_External_Long_Port = "node_external_long_port";
|
||||
public static final String HZ_Cluster_Node_External_Short_Port = "node_external_short_port";
|
||||
public static final String HZ_Cluster_Node_ID = "node_id";
|
||||
public static final String HTTP_SERVER_SECRET_KEY = "http.admin.secret_key";
|
||||
public static final String CLIENT_PROTO_SECRET_KEY = "client.proto.secret_key";
|
||||
|
||||
public static final String EMBED_DB_PROPERTY_NAME = "embed.db";
|
||||
|
||||
public static final String HZ_Cluster_Master_Node = "master_node";
|
||||
|
||||
public static final String SENSITIVE_Filter_Type = "sensitive.filter.type";
|
||||
|
||||
public static final String MESSAGE_Forward_Url = "message.forward.url";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public static final int MESSAGE_CONTENT_TYPE_CREATE_GROUP = 104;
|
||||
public static final int MESSAGE_CONTENT_TYPE_ADD_GROUP_MEMBER = 105;
|
||||
public static final int MESSAGE_CONTENT_TYPE_KICKOF_GROUP_MEMBER = 106;
|
||||
public static final int MESSAGE_CONTENT_TYPE_QUIT_GROUP = 107;
|
||||
public static final int MESSAGE_CONTENT_TYPE_DISMISS_GROUP = 108;
|
||||
public static final int MESSAGE_CONTENT_TYPE_TRANSFER_GROUP_OWNER = 109;
|
||||
public static final int MESSAGE_CONTENT_TYPE_CHANGE_GROUP_NAME = 110;
|
||||
public static final int MESSAGE_CONTENT_TYPE_MODIFY_GROUP_ALIAS = 111;
|
||||
public static final int MESSAGE_CONTENT_TYPE_CHANGE_GROUP_PORTRAIT = 112;
|
||||
private BrokerConstants() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.broker;
|
||||
|
||||
import io.moquette.broker.subscriptions.Topic;
|
||||
import io.moquette.broker.security.IAuthorizatorPolicy;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import io.netty.handler.codec.mqtt.MqttSubscribeMessage;
|
||||
import io.netty.handler.codec.mqtt.MqttTopicSubscription;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static io.moquette.broker.Utils.messageId;
|
||||
import static io.netty.handler.codec.mqtt.MqttQoS.FAILURE;
|
||||
|
||||
final class Authorizator {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Authorizator.class);
|
||||
|
||||
private final IAuthorizatorPolicy policy;
|
||||
|
||||
Authorizator(IAuthorizatorPolicy policy) {
|
||||
this.policy = policy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param clientID
|
||||
* the clientID
|
||||
* @param username
|
||||
* the username
|
||||
* @param msg
|
||||
* the subscribe message to verify
|
||||
* @return the list of verified topics for the given subscribe message.
|
||||
*/
|
||||
List<MqttTopicSubscription> verifyTopicsReadAccess(String clientID, String username, MqttSubscribeMessage msg) {
|
||||
List<MqttTopicSubscription> ackTopics = new ArrayList<>();
|
||||
|
||||
final int messageId = messageId(msg);
|
||||
for (MqttTopicSubscription req : msg.payload().topicSubscriptions()) {
|
||||
Topic topic = new Topic(req.topicName());
|
||||
if (!policy.canRead(topic, username, clientID)) {
|
||||
// send SUBACK with 0x80, the user hasn't credentials to read the topic
|
||||
LOG.warn("Client does not have read permissions on the topic CId={}, username: {}, messageId: {}, " +
|
||||
"topic: {}", clientID, username, messageId, topic);
|
||||
ackTopics.add(new MqttTopicSubscription(topic.toString(), FAILURE));
|
||||
} else {
|
||||
MqttQoS qos;
|
||||
if (topic.isValid()) {
|
||||
LOG.debug("Client will be subscribed to the topic CId={}, username: {}, messageId: {}, topic: {}",
|
||||
clientID, username, messageId, topic);
|
||||
qos = req.qualityOfService();
|
||||
} else {
|
||||
LOG.warn("Topic filter is not valid CId={}, username: {}, messageId: {}, topic: {}", clientID,
|
||||
username, messageId, topic);
|
||||
qos = FAILURE;
|
||||
}
|
||||
ackTopics.add(new MqttTopicSubscription(topic.toString(), qos));
|
||||
}
|
||||
}
|
||||
return ackTopics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the authorization policy if the topic can be used in a publish.
|
||||
*
|
||||
* @param topic
|
||||
* the topic to write to.
|
||||
* @param user
|
||||
* the user
|
||||
* @param client
|
||||
* the client
|
||||
* @return true if the user from client can publish data on topic.
|
||||
*/
|
||||
boolean canWrite(Topic topic, String user, String client) {
|
||||
return policy.canWrite(topic, user, client);
|
||||
}
|
||||
|
||||
boolean canRead(Topic topic, String user, String client) {
|
||||
return policy.canRead(topic, user, client);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.broker;
|
||||
|
||||
import io.moquette.BrokerConstants;
|
||||
import io.moquette.broker.config.IConfig;
|
||||
|
||||
class BrokerConfiguration {
|
||||
|
||||
private final boolean allowAnonymous;
|
||||
private final boolean allowZeroByteClientId;
|
||||
private final boolean reauthorizeSubscriptionsOnConnect;
|
||||
|
||||
BrokerConfiguration(IConfig props) {
|
||||
allowAnonymous = props.boolProp(BrokerConstants.ALLOW_ANONYMOUS_PROPERTY_NAME, true);
|
||||
allowZeroByteClientId = props.boolProp(BrokerConstants.ALLOW_ZERO_BYTE_CLIENT_ID_PROPERTY_NAME, false);
|
||||
reauthorizeSubscriptionsOnConnect = props.boolProp(BrokerConstants.REAUTHORIZE_SUBSCRIPTIONS_ON_CONNECT, false);
|
||||
}
|
||||
|
||||
public BrokerConfiguration(boolean allowAnonymous, boolean allowZeroByteClientId,
|
||||
boolean reauthorizeSubscriptionsOnConnect) {
|
||||
this.allowAnonymous = allowAnonymous;
|
||||
this.allowZeroByteClientId = allowZeroByteClientId;
|
||||
this.reauthorizeSubscriptionsOnConnect = reauthorizeSubscriptionsOnConnect;
|
||||
}
|
||||
|
||||
public boolean isAllowAnonymous() {
|
||||
return allowAnonymous;
|
||||
}
|
||||
|
||||
public boolean isAllowZeroByteClientId() {
|
||||
return allowZeroByteClientId;
|
||||
}
|
||||
|
||||
public boolean isReauthorizeSubscriptionsOnConnect() {
|
||||
return reauthorizeSubscriptionsOnConnect;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package io.moquette.broker;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class ClientDescriptor {
|
||||
|
||||
private final String clientID;
|
||||
private final String address;
|
||||
private final int port;
|
||||
|
||||
ClientDescriptor(String clientID, String address, int port) {
|
||||
this.clientID = clientID;
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public String getClientID() {
|
||||
return clientID;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ClientDescriptor that = (ClientDescriptor) o;
|
||||
return port == that.port &&
|
||||
Objects.equals(clientID, that.clientID) &&
|
||||
Objects.equals(address, that.address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(clientID, address, port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ClientDescriptor{" +
|
||||
"clientID='" + clientID + '\'' +
|
||||
", address='" + address + '\'' +
|
||||
", port=" + port +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.broker;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public final class DebugUtils {
|
||||
|
||||
public static String payload2Str(ByteBuf content) {
|
||||
final ByteBuf copy = content.copy();
|
||||
final byte[] bytesContent;
|
||||
if (copy.isDirect()) {
|
||||
final int size = copy.readableBytes();
|
||||
bytesContent = new byte[size];
|
||||
copy.readBytes(bytesContent);
|
||||
} else {
|
||||
bytesContent = copy.array();
|
||||
}
|
||||
return new String(bytesContent, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private DebugUtils() {
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.broker;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.moquette.BrokerConstants;
|
||||
import io.moquette.broker.config.IConfig;
|
||||
import io.netty.handler.ssl.ClientAuth;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
import io.netty.handler.ssl.SslContextBuilder;
|
||||
import io.netty.handler.ssl.SslProvider;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Moquette integration implementation to load SSL certificate from local filesystem path configured in
|
||||
* config file.
|
||||
*/
|
||||
class DefaultMoquetteSslContextCreator implements ISslContextCreator {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DefaultMoquetteSslContextCreator.class);
|
||||
|
||||
private final IConfig props;
|
||||
|
||||
DefaultMoquetteSslContextCreator(IConfig props) {
|
||||
this.props = Objects.requireNonNull(props);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SslContext initSSLContext() {
|
||||
LOG.info("Checking SSL configuration properties...");
|
||||
|
||||
final String keyPassword = props.getProperty(BrokerConstants.KEY_MANAGER_PASSWORD_PROPERTY_NAME);
|
||||
if (keyPassword == null || keyPassword.isEmpty()) {
|
||||
LOG.warn("The key manager password is null or empty. The SSL context won't be initialized.");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
SslProvider sslProvider = getSSLProvider();
|
||||
KeyStore ks = loadKeyStore();
|
||||
SslContextBuilder contextBuilder;
|
||||
switch (sslProvider) {
|
||||
case JDK:
|
||||
contextBuilder = builderWithJdkProvider(ks, keyPassword);
|
||||
break;
|
||||
case OPENSSL:
|
||||
case OPENSSL_REFCNT:
|
||||
contextBuilder = builderWithOpenSSLProvider(ks, keyPassword);
|
||||
break;
|
||||
default:
|
||||
LOG.error("unsupported SSL provider {}", sslProvider);
|
||||
return null;
|
||||
}
|
||||
// if client authentification is enabled a trustmanager needs to be added to the ServerContext
|
||||
String sNeedsClientAuth = props.getProperty(BrokerConstants.NEED_CLIENT_AUTH, "false");
|
||||
if (Boolean.valueOf(sNeedsClientAuth)) {
|
||||
addClientAuthentication(ks, contextBuilder);
|
||||
}
|
||||
contextBuilder.sslProvider(sslProvider);
|
||||
SslContext sslContext = contextBuilder.build();
|
||||
LOG.info("The SSL context has been initialized successfully.");
|
||||
return sslContext;
|
||||
} catch (GeneralSecurityException | IOException ex) {
|
||||
LOG.error("Unable to initialize SSL context.", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private KeyStore loadKeyStore() throws IOException, GeneralSecurityException {
|
||||
final String jksPath = props.getProperty(BrokerConstants.JKS_PATH_PROPERTY_NAME);
|
||||
LOG.info("Initializing SSL context. KeystorePath = {}.", jksPath);
|
||||
if (jksPath == null || jksPath.isEmpty()) {
|
||||
LOG.warn("The keystore path is null or empty. The SSL context won't be initialized.");
|
||||
return null;
|
||||
}
|
||||
final String keyStorePassword = props.getProperty(BrokerConstants.KEY_STORE_PASSWORD_PROPERTY_NAME);
|
||||
if (keyStorePassword == null || keyStorePassword.isEmpty()) {
|
||||
LOG.warn("The keystore password is null or empty. The SSL context won't be initialized.");
|
||||
return null;
|
||||
}
|
||||
String ksType = props.getProperty(BrokerConstants.KEY_STORE_TYPE, "jks");
|
||||
final KeyStore keyStore = KeyStore.getInstance(ksType);
|
||||
LOG.info("Loading keystore. KeystorePath = {}.", jksPath);
|
||||
try (InputStream jksInputStream = jksDatastore(jksPath)) {
|
||||
keyStore.load(jksInputStream, keyStorePassword.toCharArray());
|
||||
}
|
||||
return keyStore;
|
||||
}
|
||||
|
||||
private static SslContextBuilder builderWithJdkProvider(KeyStore ks, String keyPassword)
|
||||
throws GeneralSecurityException {
|
||||
LOG.info("Initializing key manager...");
|
||||
final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
kmf.init(ks, keyPassword.toCharArray());
|
||||
LOG.info("Initializing SSL context...");
|
||||
return SslContextBuilder.forServer(kmf);
|
||||
}
|
||||
|
||||
/**
|
||||
* The OpenSSL provider does not support the {@link KeyManagerFactory}, so we have to lookup the integration
|
||||
* certificate and key in order to provide it to OpenSSL.
|
||||
* <p>
|
||||
* TODO: SNI is currently not supported, we use only the first found private key.
|
||||
*/
|
||||
private static SslContextBuilder builderWithOpenSSLProvider(KeyStore ks, String keyPassword)
|
||||
throws GeneralSecurityException {
|
||||
for (String alias : Collections.list(ks.aliases())) {
|
||||
if (ks.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class)) {
|
||||
PrivateKey key = (PrivateKey) ks.getKey(alias, keyPassword.toCharArray());
|
||||
Certificate[] chain = ks.getCertificateChain(alias);
|
||||
X509Certificate[] certChain = new X509Certificate[chain.length];
|
||||
System.arraycopy(chain, 0, certChain, 0, chain.length);
|
||||
return SslContextBuilder.forServer(key, certChain);
|
||||
}
|
||||
}
|
||||
throw new KeyManagementException("the SSL key-store does not contain a private key");
|
||||
}
|
||||
|
||||
private static void addClientAuthentication(KeyStore ks, SslContextBuilder contextBuilder)
|
||||
throws NoSuchAlgorithmException, KeyStoreException {
|
||||
LOG.warn("Client authentication is enabled. The keystore will be used as a truststore.");
|
||||
// use keystore as truststore, as integration needs to trust certificates signed by the integration certificates
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init(ks);
|
||||
contextBuilder.clientAuth(ClientAuth.REQUIRE);
|
||||
contextBuilder.trustManager(tmf);
|
||||
}
|
||||
|
||||
private SslProvider getSSLProvider() {
|
||||
String providerName = props.getProperty(BrokerConstants.SSL_PROVIDER, SslProvider.JDK.name());
|
||||
try {
|
||||
return SslProvider.valueOf(providerName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn("unknown SSL Provider {}, falling back on JDK provider", providerName);
|
||||
return SslProvider.JDK;
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream jksDatastore(String jksPath) throws FileNotFoundException {
|
||||
URL jksUrl = getClass().getClassLoader().getResource(jksPath);
|
||||
if (jksUrl != null) {
|
||||
LOG.info("Starting with jks at {}, jks normal {}", jksUrl.toExternalForm(), jksUrl);
|
||||
return getClass().getClassLoader().getResourceAsStream(jksPath);
|
||||
}
|
||||
LOG.warn("No keystore has been found in the bundled resources. Scanning filesystem...");
|
||||
File jksFile = new File(jksPath);
|
||||
if (jksFile.exists()) {
|
||||
LOG.info("Loading external keystore. Url = {}.", jksFile.getAbsolutePath());
|
||||
return new FileInputStream(jksFile);
|
||||
}
|
||||
throw new FileNotFoundException("The keystore file does not exist. Url = " + jksFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package io.moquette.broker;
|
||||
|
||||
import java.util.Queue;
|
||||
|
||||
public interface IQueueRepository {
|
||||
|
||||
Queue<SessionRegistry.EnqueuedMessage> createQueue(String cli, boolean clean);
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2016 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.broker;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.concurrent.EventExecutor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Resend inflight not ack'ed publish packets (QoS1 PUB and QoS2 PUB/PUBREL). It's inspired by IdleStateHandler but it's
|
||||
* specialized version, just invoking Session's resendInflightNotAcked by the channel after a period.
|
||||
*/
|
||||
public class InflightResender extends ChannelDuplexHandler {
|
||||
|
||||
/**
|
||||
* Placeholder event to resend not-acked publish messages in the in flight window.
|
||||
* */
|
||||
public static class ResendNotAckedPublishes {
|
||||
}
|
||||
|
||||
private final class WriterIdleTimeoutTask implements Runnable {
|
||||
|
||||
private final ChannelHandlerContext ctx;
|
||||
|
||||
WriterIdleTimeoutTask(ChannelHandlerContext ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!ctx.channel().isOpen()) {
|
||||
return;
|
||||
}
|
||||
long nextDelay = resenderTimeNanos - (System.nanoTime() - lastExecutionTime);
|
||||
if (nextDelay <= 0) {
|
||||
// Writer is idle - set a new timeout and notify the callback.
|
||||
resenderTimeout = ctx.executor().schedule(this, resenderTimeNanos, TimeUnit.NANOSECONDS);
|
||||
try {
|
||||
resendNotAcked(ctx/* , event */);
|
||||
} catch (Throwable t) {
|
||||
ctx.fireExceptionCaught(t);
|
||||
}
|
||||
} else {
|
||||
// Write occurred before the timeout - set a new timeout with shorter delay.
|
||||
resenderTimeout = ctx.executor().schedule(this, nextDelay, TimeUnit.NANOSECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(InflightResender.class);
|
||||
private static final long MIN_TIMEOUT_NANOS = TimeUnit.MILLISECONDS.toNanos(1);
|
||||
|
||||
private final long resenderTimeNanos;
|
||||
volatile ScheduledFuture<?> resenderTimeout;
|
||||
volatile long lastExecutionTime;
|
||||
|
||||
private volatile int state; // 0 - none, 1 - initialized, 2 - destroyed
|
||||
|
||||
public InflightResender(long writerIdleTime, TimeUnit unit) {
|
||||
if (unit == null) {
|
||||
throw new NullPointerException("unit");
|
||||
}
|
||||
resenderTimeNanos = Math.max(unit.toNanos(writerIdleTime), MIN_TIMEOUT_NANOS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
|
||||
if (ctx.channel().isActive() && ctx.channel().isRegistered()) {
|
||||
// channelActive() event has been fired already, which means this.channelActive() will
|
||||
// not be invoked. We have to initialize here instead.
|
||||
initialize(ctx);
|
||||
} else {
|
||||
// channelActive() event has not been fired yet. this.channelActive() will be invoked
|
||||
// and initialization will occur there.
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
|
||||
destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
|
||||
// Initialize early if channel is active already.
|
||||
if (ctx.channel().isActive()) {
|
||||
initialize(ctx);
|
||||
}
|
||||
super.channelRegistered(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
||||
// This method will be invoked only if this handler was added
|
||||
// before channelActive() event is fired. If a user adds this handler
|
||||
// after the channelActive() event, initialize() will be called by beforeAdd().
|
||||
initialize(ctx);
|
||||
super.channelActive(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
destroy();
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
|
||||
private void initialize(ChannelHandlerContext ctx) {
|
||||
// Avoid the case where destroy() is called before scheduling timeouts.
|
||||
// See: https://github.com/netty/netty/issues/143
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Initializing autoflush handler on channel {}", ctx.channel());
|
||||
}
|
||||
switch (state) {
|
||||
case 1:
|
||||
case 2:
|
||||
return;
|
||||
}
|
||||
|
||||
state = 1;
|
||||
|
||||
EventExecutor loop = ctx.executor();
|
||||
|
||||
lastExecutionTime = System.nanoTime();
|
||||
resenderTimeout = loop.schedule(new WriterIdleTimeoutTask(ctx), resenderTimeNanos, TimeUnit.NANOSECONDS);
|
||||
}
|
||||
|
||||
private void destroy() {
|
||||
state = 2;
|
||||
|
||||
if (resenderTimeout != null) {
|
||||
resenderTimeout.cancel(false);
|
||||
resenderTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void resendNotAcked(ChannelHandlerContext ctx/* , IdleStateEvent evt */) {
|
||||
if (LOG.isTraceEnabled()) {
|
||||
LOG.trace("Flushing idle Netty channel {} for clientId: {}", ctx.channel(),
|
||||
NettyUtils.clientID(ctx.channel()));
|
||||
}
|
||||
ctx.fireUserEventTriggered(new ResendNotAckedPublishes());
|
||||
}
|
||||
}
|
||||
@@ -1,496 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.broker;
|
||||
|
||||
import io.moquette.broker.subscriptions.Topic;
|
||||
import io.moquette.broker.security.IAuthenticator;
|
||||
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 java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static io.netty.channel.ChannelFutureListener.CLOSE_ON_FAILURE;
|
||||
import static io.netty.channel.ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE;
|
||||
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.*;
|
||||
|
||||
final class MQTTConnection {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MQTTConnection.class);
|
||||
|
||||
final Channel channel;
|
||||
private BrokerConfiguration brokerConfig;
|
||||
private IAuthenticator authenticator;
|
||||
private SessionRegistry sessionRegistry;
|
||||
private final PostOffice postOffice;
|
||||
private boolean connected;
|
||||
private final AtomicInteger lastPacketId = new AtomicInteger(0);
|
||||
|
||||
MQTTConnection(Channel channel, BrokerConfiguration brokerConfig, IAuthenticator authenticator,
|
||||
SessionRegistry sessionRegistry, PostOffice postOffice) {
|
||||
this.channel = channel;
|
||||
this.brokerConfig = brokerConfig;
|
||||
this.authenticator = authenticator;
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
this.postOffice = postOffice;
|
||||
this.connected = false;
|
||||
}
|
||||
|
||||
void handleMessage(MqttMessage msg) {
|
||||
MqttMessageType messageType = msg.fixedHeader().messageType();
|
||||
LOG.debug("Received MQTT message, type: {}, channel: {}", messageType, channel);
|
||||
switch (messageType) {
|
||||
case CONNECT:
|
||||
processConnect((MqttConnectMessage) msg);
|
||||
break;
|
||||
case SUBSCRIBE:
|
||||
processSubscribe((MqttSubscribeMessage) msg);
|
||||
break;
|
||||
case UNSUBSCRIBE:
|
||||
processUnsubscribe((MqttUnsubscribeMessage) msg);
|
||||
break;
|
||||
case PUBLISH:
|
||||
processPublish((MqttPublishMessage) msg);
|
||||
break;
|
||||
case PUBREC:
|
||||
processPubRec(msg);
|
||||
break;
|
||||
case PUBCOMP:
|
||||
processPubComp(msg);
|
||||
break;
|
||||
case PUBREL:
|
||||
processPubRel(msg);
|
||||
break;
|
||||
case DISCONNECT:
|
||||
processDisconnect(msg);
|
||||
break;
|
||||
case PUBACK:
|
||||
processPubAck(msg);
|
||||
break;
|
||||
case PINGREQ:
|
||||
MqttFixedHeader pingHeader = new MqttFixedHeader(MqttMessageType.PINGRESP, false, AT_MOST_ONCE,
|
||||
false, 0);
|
||||
MqttMessage pingResp = new MqttMessage(pingHeader);
|
||||
channel.writeAndFlush(pingResp).addListener(CLOSE_ON_FAILURE);
|
||||
break;
|
||||
default:
|
||||
LOG.error("Unknown MessageType: {}, channel: {}", messageType, channel);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void processPubComp(MqttMessage msg) {
|
||||
final int messageID = ((MqttMessageIdVariableHeader) msg.variableHeader()).messageId();
|
||||
final Session session = sessionRegistry.retrieve(getClientId());
|
||||
session.processPubComp(messageID);
|
||||
}
|
||||
|
||||
private void processPubRec(MqttMessage msg) {
|
||||
final int messageID = ((MqttMessageIdVariableHeader) msg.variableHeader()).messageId();
|
||||
final Session session = sessionRegistry.retrieve(getClientId());
|
||||
session.processPubRec(messageID);
|
||||
}
|
||||
|
||||
static MqttMessage pubrel(int messageID) {
|
||||
MqttFixedHeader pubRelHeader = new MqttFixedHeader(MqttMessageType.PUBREL, false, AT_LEAST_ONCE, false, 0);
|
||||
return new MqttMessage(pubRelHeader, from(messageID));
|
||||
}
|
||||
|
||||
private void processPubAck(MqttMessage msg) {
|
||||
final int messageID = ((MqttMessageIdVariableHeader) msg.variableHeader()).messageId();
|
||||
Session session = sessionRegistry.retrieve(getClientId());
|
||||
session.pubAckReceived(messageID);
|
||||
}
|
||||
|
||||
void processConnect(MqttConnectMessage msg) {
|
||||
MqttConnectPayload payload = msg.payload();
|
||||
String clientId = payload.clientIdentifier();
|
||||
final String username = payload.userName();
|
||||
LOG.trace("Processing CONNECT message. CId={} username: {} channel: {}", clientId, username, channel);
|
||||
|
||||
if (isNotProtocolVersion(msg, MqttVersion.MQTT_3_1) && isNotProtocolVersion(msg, MqttVersion.MQTT_3_1_1)) {
|
||||
LOG.warn("MQTT protocol version is not valid. CId={} channel: {}", clientId, channel);
|
||||
abortConnection(CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION);
|
||||
return;
|
||||
}
|
||||
final boolean cleanSession = msg.variableHeader().isCleanSession();
|
||||
if (clientId == null || clientId.length() == 0) {
|
||||
if (!brokerConfig.isAllowZeroByteClientId()) {
|
||||
LOG.warn("Broker doesn't permit MQTT empty client ID. Username: {}, channel: {}", username, channel);
|
||||
abortConnection(CONNECTION_REFUSED_IDENTIFIER_REJECTED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cleanSession) {
|
||||
LOG.warn("MQTT client ID cannot be empty for persistent session. Username: {}, channel: {}",
|
||||
username, channel);
|
||||
abortConnection(CONNECTION_REFUSED_IDENTIFIER_REJECTED);
|
||||
return;
|
||||
}
|
||||
|
||||
// Generating client id.
|
||||
clientId = UUID.randomUUID().toString().replace("-", "");
|
||||
LOG.debug("Client has connected with integration generated id: {}, username: {}, channel: {}", clientId,
|
||||
username, channel);
|
||||
}
|
||||
|
||||
if (!login(msg, clientId)) {
|
||||
abortConnection(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
|
||||
channel.close().addListener(CLOSE_ON_FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
LOG.trace("Binding MQTTConnection (channel: {}) to session", channel);
|
||||
sessionRegistry.bindToSession(this, msg, clientId);
|
||||
|
||||
initializeKeepAliveTimeout(channel, msg, clientId);
|
||||
setupInflightResender(channel);
|
||||
|
||||
NettyUtils.clientID(channel, clientId);
|
||||
LOG.trace("CONNACK sent, channel: {}", channel);
|
||||
postOffice.dispatchConnection(msg);
|
||||
LOG.trace("dispatch connection: {}", msg.toString());
|
||||
} catch (SessionCorruptedException scex) {
|
||||
LOG.warn("MQTT session for client ID {} cannot be created, channel: {}", clientId, channel);
|
||||
abortConnection(CONNECTION_REFUSED_SERVER_UNAVAILABLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupInflightResender(Channel channel) {
|
||||
channel.pipeline()
|
||||
.addFirst("inflightResender", new InflightResender(5_000, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
private void initializeKeepAliveTimeout(Channel channel, MqttConnectMessage msg, String clientId) {
|
||||
int keepAlive = msg.variableHeader().keepAliveTimeSeconds();
|
||||
NettyUtils.keepAlive(channel, keepAlive);
|
||||
NettyUtils.cleanSession(channel, msg.variableHeader().isCleanSession());
|
||||
NettyUtils.clientID(channel, clientId);
|
||||
int idleTime = Math.round(keepAlive * 1.5f);
|
||||
setIdleTime(channel.pipeline(), idleTime);
|
||||
|
||||
LOG.debug("Connection has been configured CId={}, keepAlive={}, removeTemporaryQoS2={}, idleTime={}",
|
||||
clientId, keepAlive, msg.variableHeader().isCleanSession(), idleTime);
|
||||
}
|
||||
|
||||
private void setIdleTime(ChannelPipeline pipeline, int idleTime) {
|
||||
if (pipeline.names().contains("idleStateHandler")) {
|
||||
pipeline.remove("idleStateHandler");
|
||||
}
|
||||
pipeline.addFirst("idleStateHandler", new IdleStateHandler(idleTime, 0, 0));
|
||||
}
|
||||
|
||||
private boolean isNotProtocolVersion(MqttConnectMessage msg, MqttVersion version) {
|
||||
return msg.variableHeader().version() != version.protocolLevel();
|
||||
}
|
||||
|
||||
private void abortConnection(MqttConnectReturnCode returnCode) {
|
||||
MqttConnAckMessage badProto = connAck(returnCode, false);
|
||||
channel.writeAndFlush(badProto).addListener(FIRE_EXCEPTION_ON_FAILURE);
|
||||
channel.close().addListener(CLOSE_ON_FAILURE);
|
||||
}
|
||||
|
||||
private MqttConnAckMessage connAck(MqttConnectReturnCode returnCode, boolean sessionPresent) {
|
||||
MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE,
|
||||
false, 0);
|
||||
MqttConnAckVariableHeader mqttConnAckVariableHeader = new MqttConnAckVariableHeader(returnCode, sessionPresent);
|
||||
return new MqttConnAckMessage(mqttFixedHeader, mqttConnAckVariableHeader);
|
||||
}
|
||||
|
||||
private boolean login(MqttConnectMessage msg, final String clientId) {
|
||||
// handle user authentication
|
||||
if (msg.variableHeader().hasUserName()) {
|
||||
byte[] pwd = null;
|
||||
if (msg.variableHeader().hasPassword()) {
|
||||
pwd = msg.payload().password().getBytes(StandardCharsets.UTF_8);
|
||||
} else if (!brokerConfig.isAllowAnonymous()) {
|
||||
LOG.error("Client didn't supply any password and MQTT anonymous mode is disabled CId={}", clientId);
|
||||
return false;
|
||||
}
|
||||
final String login = msg.payload().userName();
|
||||
if (!authenticator.checkValid(clientId, login, pwd)) {
|
||||
LOG.error("Authenticator has rejected the MQTT credentials CId={}, username={}", clientId, login);
|
||||
return false;
|
||||
}
|
||||
NettyUtils.userName(channel, login);
|
||||
} else if (!brokerConfig.isAllowAnonymous()) {
|
||||
LOG.error("Client didn't supply any credentials and MQTT anonymous mode is disabled. CId={}", clientId);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void handleConnectionLost() {
|
||||
String clientID = NettyUtils.clientID(channel);
|
||||
if (clientID == null || clientID.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
LOG.info("Notifying connection lost event. CId: {}, channel: {}", clientID, channel);
|
||||
Session session = sessionRegistry.retrieve(clientID);
|
||||
if (session.hasWill()) {
|
||||
postOffice.fireWill(session.getWill());
|
||||
}
|
||||
if (session.isClean()) {
|
||||
sessionRegistry.remove(clientID);
|
||||
} else {
|
||||
sessionRegistry.disconnect(clientID);
|
||||
}
|
||||
connected = false;
|
||||
//dispatch connection lost to intercept.
|
||||
String userName = NettyUtils.userName(channel);
|
||||
postOffice.dispatchConnectionLost(clientID,userName);
|
||||
LOG.trace("dispatch disconnection: clientId={}, userName={}", clientID, userName);
|
||||
}
|
||||
|
||||
void sendConnAck(boolean isSessionAlreadyPresent) {
|
||||
connected = true;
|
||||
final MqttConnAckMessage ackMessage = connAck(CONNECTION_ACCEPTED, isSessionAlreadyPresent);
|
||||
channel.writeAndFlush(ackMessage).addListener(FIRE_EXCEPTION_ON_FAILURE);
|
||||
}
|
||||
|
||||
boolean isConnected() {
|
||||
return connected;
|
||||
}
|
||||
|
||||
void dropConnection() {
|
||||
channel.close().addListener(FIRE_EXCEPTION_ON_FAILURE);
|
||||
}
|
||||
|
||||
void processDisconnect(MqttMessage msg) {
|
||||
final String clientID = NettyUtils.clientID(channel);
|
||||
LOG.trace("Start DISCONNECT CId={}, channel: {}", clientID, channel);
|
||||
if (!connected) {
|
||||
LOG.info("DISCONNECT received on already closed connection, CId={}, channel: {}", clientID, channel);
|
||||
return;
|
||||
}
|
||||
sessionRegistry.disconnect(clientID);
|
||||
connected = false;
|
||||
channel.close().addListener(FIRE_EXCEPTION_ON_FAILURE);
|
||||
LOG.trace("Processed DISCONNECT CId={}, channel: {}", clientID, channel);
|
||||
String userName = NettyUtils.userName(channel);
|
||||
postOffice.dispatchDisconnection(clientID,userName);
|
||||
LOG.trace("dispatch disconnection: clientId={}, userName={}", clientID, userName);
|
||||
}
|
||||
|
||||
void processSubscribe(MqttSubscribeMessage msg) {
|
||||
final String clientID = NettyUtils.clientID(channel);
|
||||
if (!connected) {
|
||||
LOG.warn("SUBSCRIBE received on already closed connection, CId={}, channel: {}", clientID, channel);
|
||||
dropConnection();
|
||||
return;
|
||||
}
|
||||
postOffice.subscribeClientToTopics(msg, clientID, NettyUtils.userName(channel), this);
|
||||
}
|
||||
|
||||
void sendSubAckMessage(int messageID, MqttSubAckMessage ackMessage) {
|
||||
final String clientId = NettyUtils.clientID(channel);
|
||||
LOG.trace("Sending SUBACK response CId={}, messageId: {}", clientId, messageID);
|
||||
channel.writeAndFlush(ackMessage).addListener(FIRE_EXCEPTION_ON_FAILURE);
|
||||
}
|
||||
|
||||
private void processUnsubscribe(MqttUnsubscribeMessage msg) {
|
||||
List<String> topics = msg.payload().topics();
|
||||
String clientID = NettyUtils.clientID(channel);
|
||||
|
||||
LOG.trace("Processing UNSUBSCRIBE message. CId={}, topics: {}", clientID, topics);
|
||||
postOffice.unsubscribe(topics, this, msg.variableHeader().messageId());
|
||||
}
|
||||
|
||||
void sendUnsubAckMessage(List<String> topics, String clientID, int messageID) {
|
||||
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.UNSUBACK, false, AT_MOST_ONCE,
|
||||
false, 0);
|
||||
MqttUnsubAckMessage ackMessage = new MqttUnsubAckMessage(fixedHeader, from(messageID));
|
||||
|
||||
LOG.trace("Sending UNSUBACK message. CId={}, messageId: {}, topics: {}", clientID, messageID, topics);
|
||||
channel.writeAndFlush(ackMessage).addListener(FIRE_EXCEPTION_ON_FAILURE);
|
||||
LOG.trace("Client <{}> unsubscribed from topics <{}>", clientID, topics);
|
||||
}
|
||||
|
||||
void processPublish(MqttPublishMessage msg) {
|
||||
final MqttQoS qos = msg.fixedHeader().qosLevel();
|
||||
final String username = NettyUtils.userName(channel);
|
||||
final String topicName = msg.variableHeader().topicName();
|
||||
final String clientId = getClientId();
|
||||
LOG.trace("Processing PUBLISH message. CId={}, topic: {}, messageId: {}, qos: {}", clientId, topicName,
|
||||
msg.variableHeader().packetId(), qos);
|
||||
ByteBuf payload = msg.payload();
|
||||
final boolean retain = msg.fixedHeader().isRetain();
|
||||
final Topic topic = new Topic(topicName);
|
||||
if (!topic.isValid()) {
|
||||
LOG.debug("Drop connection because of invalid topic format");
|
||||
dropConnection();
|
||||
}
|
||||
switch (qos) {
|
||||
case AT_MOST_ONCE:
|
||||
postOffice.receivedPublishQos0(topic, username, clientId, payload, retain, msg);
|
||||
break;
|
||||
case AT_LEAST_ONCE: {
|
||||
final int messageID = msg.variableHeader().packetId();
|
||||
postOffice.receivedPublishQos1(this, topic, username, payload, messageID, retain, msg);
|
||||
break;
|
||||
}
|
||||
case EXACTLY_ONCE: {
|
||||
final int messageID = msg.variableHeader().packetId();
|
||||
final Session session = sessionRegistry.retrieve(clientId);
|
||||
session.receivedPublishQos2(messageID, msg);
|
||||
postOffice.receivedPublishQos2(this, msg, username);
|
||||
// msg.release();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
LOG.error("Unknown QoS-Type:{}", qos);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void sendPublishReceived(int messageID) {
|
||||
LOG.trace("sendPubRec invoked on channel: {}", channel);
|
||||
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBREC, false, AT_MOST_ONCE,
|
||||
false, 0);
|
||||
MqttPubAckMessage pubRecMessage = new MqttPubAckMessage(fixedHeader, from(messageID));
|
||||
sendIfWritableElseDrop(pubRecMessage);
|
||||
}
|
||||
|
||||
private void processPubRel(MqttMessage msg) {
|
||||
final Session session = sessionRegistry.retrieve(getClientId());
|
||||
final int messageID = ((MqttMessageIdVariableHeader) msg.variableHeader()).messageId();
|
||||
session.receivedPubRelQos2(messageID);
|
||||
sendPubCompMessage(messageID);
|
||||
}
|
||||
|
||||
void sendPublish(MqttPublishMessage publishMsg) {
|
||||
final int packetId = publishMsg.variableHeader().packetId();
|
||||
final String topicName = publishMsg.variableHeader().topicName();
|
||||
final String clientId = getClientId();
|
||||
MqttQoS qos = publishMsg.fixedHeader().qosLevel();
|
||||
if (LOG.isTraceEnabled()) {
|
||||
LOG.trace("Sending PUBLISH({}) message. MessageId={}, CId={}, topic={}, payload={}", qos, packetId,
|
||||
clientId, topicName, DebugUtils.payload2Str(publishMsg.payload()));
|
||||
} else {
|
||||
LOG.debug("Sending PUBLISH({}) message. MessageId={}, CId={}, topic={}", qos, packetId, clientId,
|
||||
topicName);
|
||||
}
|
||||
sendIfWritableElseDrop(publishMsg);
|
||||
}
|
||||
|
||||
void sendIfWritableElseDrop(MqttMessage msg) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("OUT {} on channel {}", msg.fixedHeader().messageType(), channel);
|
||||
}
|
||||
if (channel.isWritable()) {
|
||||
channel.write(msg).addListener(FIRE_EXCEPTION_ON_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
public void writabilityChanged() {
|
||||
if (channel.isWritable()) {
|
||||
LOG.debug("Channel {} is again writable", channel);
|
||||
final Session session = sessionRegistry.retrieve(getClientId());
|
||||
session.writabilityChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void sendPubAck(int messageID) {
|
||||
LOG.trace("sendPubAck invoked");
|
||||
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBACK, false, AT_MOST_ONCE,
|
||||
false, 0);
|
||||
MqttPubAckMessage pubAckMessage = new MqttPubAckMessage(fixedHeader, from(messageID));
|
||||
sendIfWritableElseDrop(pubAckMessage);
|
||||
}
|
||||
|
||||
private void sendPubCompMessage(int messageID) {
|
||||
LOG.trace("Sending PUBCOMP message on channel: {}, messageId: {}", channel, messageID);
|
||||
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, AT_MOST_ONCE, false, 0);
|
||||
MqttMessage pubCompMessage = new MqttMessage(fixedHeader, from(messageID));
|
||||
sendIfWritableElseDrop(pubCompMessage);
|
||||
}
|
||||
|
||||
String getClientId() {
|
||||
return NettyUtils.clientID(channel);
|
||||
}
|
||||
|
||||
String getUsername() {
|
||||
return NettyUtils.userName(channel);
|
||||
}
|
||||
|
||||
public void sendPublishRetainedQos0(Topic topic, MqttQoS qos, ByteBuf payload) {
|
||||
MqttPublishMessage publishMsg = retainedPublish(topic.toString(), qos, payload);
|
||||
sendPublish(publishMsg);
|
||||
}
|
||||
|
||||
public void sendPublishRetainedWithPacketId(Topic topic, MqttQoS qos, ByteBuf payload) {
|
||||
final int packetId = nextPacketId();
|
||||
MqttPublishMessage publishMsg = retainedPublishWithMessageId(topic.toString(), qos, payload, packetId);
|
||||
sendPublish(publishMsg);
|
||||
}
|
||||
|
||||
private static MqttPublishMessage retainedPublish(String topic, MqttQoS qos, ByteBuf message) {
|
||||
return retainedPublishWithMessageId(topic, qos, message, 0);
|
||||
}
|
||||
|
||||
private static MqttPublishMessage retainedPublishWithMessageId(String topic, MqttQoS qos, ByteBuf message,
|
||||
int messageId) {
|
||||
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, false, qos, true, 0);
|
||||
MqttPublishVariableHeader varHeader = new MqttPublishVariableHeader(topic, messageId);
|
||||
return new MqttPublishMessage(fixedHeader, varHeader, message);
|
||||
}
|
||||
|
||||
// TODO move this method in Session
|
||||
void sendPublishNotRetainedQos0(Topic topic, MqttQoS qos, ByteBuf payload) {
|
||||
MqttPublishMessage publishMsg = notRetainedPublish(topic.toString(), qos, payload);
|
||||
sendPublish(publishMsg);
|
||||
}
|
||||
|
||||
static MqttPublishMessage notRetainedPublish(String topic, MqttQoS qos, ByteBuf message) {
|
||||
return notRetainedPublishWithMessageId(topic, qos, message, 0);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public void resendNotAckedPublishes() {
|
||||
final Session session = sessionRegistry.retrieve(getClientId());
|
||||
session.resendInflightNotAcked();
|
||||
}
|
||||
|
||||
int nextPacketId() {
|
||||
return lastPacketId.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MQTTConnection{channel=" + channel + ", connected=" + connected + '}';
|
||||
}
|
||||
|
||||
InetSocketAddress remoteAddress() {
|
||||
return (InetSocketAddress) channel.remoteAddress();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.broker;
|
||||
|
||||
import io.moquette.broker.security.IAuthenticator;
|
||||
import io.netty.channel.Channel;
|
||||
|
||||
class MQTTConnectionFactory {
|
||||
|
||||
private final BrokerConfiguration brokerConfig;
|
||||
private final IAuthenticator authenticator;
|
||||
private final SessionRegistry sessionRegistry;
|
||||
private final PostOffice postOffice;
|
||||
|
||||
MQTTConnectionFactory(BrokerConfiguration brokerConfig, IAuthenticator authenticator,
|
||||
SessionRegistry sessionRegistry, PostOffice postOffice) {
|
||||
this.brokerConfig = brokerConfig;
|
||||
this.authenticator = authenticator;
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
this.postOffice = postOffice;
|
||||
}
|
||||
|
||||
MQTTConnection create(Channel channel) {
|
||||
return new MQTTConnection(channel, brokerConfig, authenticator, sessionRegistry, postOffice);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package io.moquette.broker;
|
||||
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
public class MemoryQueueRepository implements IQueueRepository {
|
||||
|
||||
@Override
|
||||
public Queue<SessionRegistry.EnqueuedMessage> createQueue(String cli, boolean clean) {
|
||||
return new ConcurrentLinkedQueue<>();
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.broker;
|
||||
|
||||
import io.moquette.broker.subscriptions.Topic;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.mqtt.MqttPublishMessage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/*
|
||||
* In memory retained messages store
|
||||
* */
|
||||
final class MemoryRetainedRepository implements IRetainedRepository {
|
||||
|
||||
private final ConcurrentMap<Topic, RetainedMessage> storage = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void cleanRetained(Topic topic) {
|
||||
storage.remove(topic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void retain(Topic topic, MqttPublishMessage msg) {
|
||||
final ByteBuf payload = msg.content();
|
||||
byte[] rawPayload = new byte[payload.readableBytes()];
|
||||
payload.getBytes(0, rawPayload);
|
||||
final RetainedMessage toStore = new RetainedMessage(msg.fixedHeader().qosLevel(), rawPayload);
|
||||
storage.put(topic, toStore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return storage.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RetainedMessage> retainedOnTopic(String topic) {
|
||||
final Topic searchTopic = new Topic(topic);
|
||||
final List<RetainedMessage> matchingMessages = new ArrayList<>();
|
||||
for (Map.Entry<Topic, RetainedMessage> entry : storage.entrySet()) {
|
||||
final Topic scanTopic = entry.getKey();
|
||||
if (searchTopic.match(scanTopic)) {
|
||||
matchingMessages.add(entry.getValue());
|
||||
}
|
||||
}
|
||||
return matchingMessages;
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.broker;
|
||||
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.ChannelHandler.Sharable;
|
||||
import io.netty.handler.codec.mqtt.MqttMessage;
|
||||
import io.netty.util.AttributeKey;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static io.netty.channel.ChannelFutureListener.CLOSE_ON_FAILURE;
|
||||
|
||||
@Sharable
|
||||
public class NewNettyMQTTHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(NewNettyMQTTHandler.class);
|
||||
|
||||
private static final String ATTR_CONNECTION = "connection";
|
||||
private static final AttributeKey<Object> ATTR_KEY_CONNECTION = AttributeKey.valueOf(ATTR_CONNECTION);
|
||||
|
||||
private MQTTConnectionFactory connectionFactory;
|
||||
|
||||
NewNettyMQTTHandler(MQTTConnectionFactory connectionFactory) {
|
||||
this.connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
private static void mqttConnection(Channel channel, MQTTConnection connection) {
|
||||
channel.attr(ATTR_KEY_CONNECTION).set(connection);
|
||||
}
|
||||
|
||||
private static MQTTConnection mqttConnection(Channel channel) {
|
||||
return (MQTTConnection) channel.attr(ATTR_KEY_CONNECTION).get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object message) throws Exception {
|
||||
MqttMessage msg = NettyUtils.validateMessage(message);
|
||||
final MQTTConnection mqttConnection = mqttConnection(ctx.channel());
|
||||
try {
|
||||
mqttConnection.handleMessage(msg);
|
||||
} catch (Throwable ex) {
|
||||
//ctx.fireExceptionCaught(ex);
|
||||
LOG.error("Error processing protocol message: {}", msg.fixedHeader().messageType(), ex);
|
||||
ctx.channel().close().addListener(new ChannelFutureListener() {
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture future) {
|
||||
LOG.info("Closed client channel due to exception in processing");
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(ChannelHandlerContext ctx) {
|
||||
MQTTConnection connection = connectionFactory.create(ctx.channel());
|
||||
mqttConnection(ctx.channel(), connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) {
|
||||
final MQTTConnection mqttConnection = mqttConnection(ctx.channel());
|
||||
mqttConnection.handleConnectionLost();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
LOG.error("Unexpected exception while processing MQTT message. Closing Netty channel. CId={}",
|
||||
NettyUtils.clientID(ctx.channel()), cause);
|
||||
ctx.close().addListener(CLOSE_ON_FAILURE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelWritabilityChanged(ChannelHandlerContext ctx) {
|
||||
// if (ctx.channel().isWritable()) {
|
||||
// m_processor.notifyChannelWritable(ctx.channel());
|
||||
// }
|
||||
final MQTTConnection mqttConnection = mqttConnection(ctx.channel());
|
||||
mqttConnection.writabilityChanged();
|
||||
ctx.fireChannelWritabilityChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
|
||||
if (evt instanceof InflightResender.ResendNotAckedPublishes) {
|
||||
final MQTTConnection mqttConnection = mqttConnection(ctx.channel());
|
||||
mqttConnection.resendNotAckedPublishes();
|
||||
}
|
||||
ctx.fireUserEventTriggered(evt);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.broker;
|
||||
|
||||
import io.moquette.interception.BrokerInterceptor;
|
||||
import io.moquette.broker.subscriptions.ISubscriptionsDirectory;
|
||||
import io.moquette.broker.subscriptions.Subscription;
|
||||
import io.moquette.broker.subscriptions.Topic;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.handler.codec.mqtt.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static io.moquette.broker.Utils.messageId;
|
||||
import static io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader.from;
|
||||
import static io.netty.handler.codec.mqtt.MqttQoS.*;
|
||||
|
||||
class PostOffice {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PostOffice.class);
|
||||
|
||||
private final Authorizator authorizator;
|
||||
private final ISubscriptionsDirectory subscriptions;
|
||||
private final IRetainedRepository retainedRepository;
|
||||
private SessionRegistry sessionRegistry;
|
||||
private BrokerInterceptor interceptor;
|
||||
|
||||
PostOffice(ISubscriptionsDirectory subscriptions, IRetainedRepository retainedRepository,
|
||||
SessionRegistry sessionRegistry, BrokerInterceptor interceptor, Authorizator authorizator) {
|
||||
this.authorizator = authorizator;
|
||||
this.subscriptions = subscriptions;
|
||||
this.retainedRepository = retainedRepository;
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
this.interceptor = interceptor;
|
||||
}
|
||||
|
||||
public void init(SessionRegistry sessionRegistry) {
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
}
|
||||
|
||||
public void fireWill(Session.Will will) {
|
||||
// MQTT 3.1.2.8-17
|
||||
publish2Subscribers(will.payload, new Topic(will.topic), will.qos);
|
||||
}
|
||||
|
||||
public void subscribeClientToTopics(MqttSubscribeMessage msg, String clientID, String username,
|
||||
MQTTConnection mqttConnection) {
|
||||
// verify which topics of the subscribe ongoing has read access permission
|
||||
int messageID = messageId(msg);
|
||||
List<MqttTopicSubscription> ackTopics = authorizator.verifyTopicsReadAccess(clientID, username, msg);
|
||||
MqttSubAckMessage ackMessage = doAckMessageFromValidateFilters(ackTopics, messageID);
|
||||
|
||||
// store topics subscriptions in session
|
||||
List<Subscription> newSubscriptions = ackTopics.stream()
|
||||
.filter(req -> req.qualityOfService() != FAILURE)
|
||||
.map(req -> {
|
||||
final Topic topic = new Topic(req.topicName());
|
||||
return new Subscription(clientID, topic, req.qualityOfService());
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
for (Subscription subscription : newSubscriptions) {
|
||||
subscriptions.add(subscription);
|
||||
}
|
||||
|
||||
// add the subscriptions to Session
|
||||
Session session = sessionRegistry.retrieve(clientID);
|
||||
session.addSubscriptions(newSubscriptions);
|
||||
|
||||
// send ack message
|
||||
mqttConnection.sendSubAckMessage(messageID, ackMessage);
|
||||
|
||||
publishRetainedMessagesForSubscriptions(clientID, newSubscriptions);
|
||||
|
||||
for (Subscription subscription : newSubscriptions) {
|
||||
interceptor.notifyTopicSubscribed(subscription, username);
|
||||
}
|
||||
}
|
||||
|
||||
private void publishRetainedMessagesForSubscriptions(String clientID, List<Subscription> newSubscriptions) {
|
||||
Session targetSession = this.sessionRegistry.retrieve(clientID);
|
||||
for (Subscription subscription : newSubscriptions) {
|
||||
final String topicFilter = subscription.getTopicFilter().toString();
|
||||
final List<RetainedMessage> retainedMsgs = retainedRepository.retainedOnTopic(topicFilter);
|
||||
|
||||
if (retainedMsgs.isEmpty()) {
|
||||
// not found
|
||||
continue;
|
||||
}
|
||||
for (RetainedMessage retainedMsg : retainedMsgs) {
|
||||
final MqttQoS retainedQos = retainedMsg.qosLevel();
|
||||
MqttQoS qos = lowerQosToTheSubscriptionDesired(subscription, retainedQos);
|
||||
|
||||
// final ByteBuf origPayload = retainedMsg.getPayload();
|
||||
final ByteBuf payloadBuf = Unpooled.wrappedBuffer(retainedMsg.getPayload());
|
||||
// ByteBuf payload = origPayload.retainedDuplicate();
|
||||
targetSession.sendRetainedPublishOnSessionAtQos(subscription.getTopicFilter(), qos, payloadBuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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_MOST_ONCE,
|
||||
false, 0);
|
||||
MqttSubAckPayload payload = new MqttSubAckPayload(grantedQoSLevels);
|
||||
return new MqttSubAckMessage(fixedHeader, from(messageId), payload);
|
||||
}
|
||||
|
||||
public void unsubscribe(List<String> topics, MQTTConnection mqttConnection, int messageId) {
|
||||
final String clientID = mqttConnection.getClientId();
|
||||
for (String t : topics) {
|
||||
Topic topic = new Topic(t);
|
||||
boolean validTopic = topic.isValid();
|
||||
if (!validTopic) {
|
||||
// close the connection, not valid topicFilter is a protocol violation
|
||||
mqttConnection.dropConnection();
|
||||
LOG.warn("Topic filter is not valid. CId={}, topics: {}, offending topic filter: {}", clientID,
|
||||
topics, topic);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.trace("Removing subscription. CId={}, topic={}", clientID, topic);
|
||||
subscriptions.removeSubscription(topic, clientID);
|
||||
|
||||
// TODO remove the subscriptions to Session
|
||||
// clientSession.unsubscribeFrom(topic);
|
||||
|
||||
String username = NettyUtils.userName(mqttConnection.channel);
|
||||
interceptor.notifyTopicUnsubscribed(topic.toString(), clientID, username);
|
||||
}
|
||||
|
||||
// ack the client
|
||||
mqttConnection.sendUnsubAckMessage(topics, clientID, messageId);
|
||||
}
|
||||
|
||||
void receivedPublishQos0(Topic topic, String username, String clientID, ByteBuf payload, boolean retain,
|
||||
MqttPublishMessage msg) {
|
||||
if (!authorizator.canWrite(topic, username, clientID)) {
|
||||
LOG.error("MQTT client: {} is not authorized to publish on topic: {}", clientID, topic);
|
||||
return;
|
||||
}
|
||||
publish2Subscribers(payload, topic, AT_MOST_ONCE);
|
||||
|
||||
if (retain) {
|
||||
// QoS == 0 && retain => clean old retained
|
||||
retainedRepository.cleanRetained(topic);
|
||||
}
|
||||
|
||||
interceptor.notifyTopicPublished(msg, clientID, username);
|
||||
}
|
||||
|
||||
void receivedPublishQos1(MQTTConnection connection, Topic topic, String username, ByteBuf payload, int messageID,
|
||||
boolean retain, MqttPublishMessage msg) {
|
||||
// verify if topic can be write
|
||||
topic.getTokens();
|
||||
if (!topic.isValid()) {
|
||||
LOG.warn("Invalid topic format, force close the connection");
|
||||
connection.dropConnection();
|
||||
return;
|
||||
}
|
||||
final String clientId = connection.getClientId();
|
||||
if (!authorizator.canWrite(topic, username, clientId)) {
|
||||
LOG.error("MQTT client: {} is not authorized to publish on topic: {}", clientId, topic);
|
||||
return;
|
||||
}
|
||||
|
||||
publish2Subscribers(payload, topic, AT_LEAST_ONCE);
|
||||
|
||||
connection.sendPubAck(messageID);
|
||||
|
||||
if (retain) {
|
||||
if (!payload.isReadable()) {
|
||||
retainedRepository.cleanRetained(topic);
|
||||
} else {
|
||||
// before wasn't stored
|
||||
retainedRepository.retain(topic, msg);
|
||||
}
|
||||
}
|
||||
interceptor.notifyTopicPublished(msg, clientId, username);
|
||||
}
|
||||
|
||||
private void publish2Subscribers(ByteBuf origPayload, Topic topic, MqttQoS publishingQos) {
|
||||
Set<Subscription> topicMatchingSubscriptions = subscriptions.matchQosSharpening(topic);
|
||||
|
||||
for (final Subscription sub : topicMatchingSubscriptions) {
|
||||
MqttQoS qos = lowerQosToTheSubscriptionDesired(sub, publishingQos);
|
||||
Session targetSession = this.sessionRegistry.retrieve(sub.getClientId());
|
||||
|
||||
boolean isSessionPresent = targetSession != null;
|
||||
if (isSessionPresent) {
|
||||
LOG.debug("Sending PUBLISH message to active subscriber CId: {}, topicFilter: {}, qos: {}",
|
||||
sub.getClientId(), sub.getTopicFilter(), qos);
|
||||
// we need to retain because duplicate only copy r/w indexes and don't retain() causing refCnt = 0
|
||||
ByteBuf payload = origPayload.retainedDuplicate();
|
||||
targetSession.sendPublishOnSessionAtQos(topic, qos, payload);
|
||||
} else {
|
||||
// If we are, the subscriber disconnected after the subscriptions tree selected that session as a
|
||||
// destination.
|
||||
LOG.debug("PUBLISH to not yet present session. CId: {}, topicFilter: {}, qos: {}", sub.getClientId(),
|
||||
sub.getTopicFilter(), qos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* First phase of a publish QoS2 protocol, sent by publisher to the broker. Publish to all interested
|
||||
* subscribers.
|
||||
*/
|
||||
void receivedPublishQos2(MQTTConnection connection, MqttPublishMessage mqttPublishMessage, String username) {
|
||||
LOG.trace("Processing PUBREL message on connection: {}", connection);
|
||||
final Topic topic = new Topic(mqttPublishMessage.variableHeader().topicName());
|
||||
final ByteBuf payload = mqttPublishMessage.payload();
|
||||
|
||||
final String clientId = connection.getClientId();
|
||||
if (!authorizator.canWrite(topic, username, clientId)) {
|
||||
LOG.error("MQTT client is not authorized to publish on topic. CId={}, topic: {}", clientId, topic);
|
||||
return;
|
||||
}
|
||||
|
||||
publish2Subscribers(payload, topic, EXACTLY_ONCE);
|
||||
|
||||
final boolean retained = mqttPublishMessage.fixedHeader().isRetain();
|
||||
if (retained) {
|
||||
if (!payload.isReadable()) {
|
||||
retainedRepository.cleanRetained(topic);
|
||||
} else {
|
||||
// before wasn't stored
|
||||
retainedRepository.retain(topic, mqttPublishMessage);
|
||||
}
|
||||
}
|
||||
|
||||
String clientID = connection.getClientId();
|
||||
interceptor.notifyTopicPublished(mqttPublishMessage, clientID, username);
|
||||
}
|
||||
|
||||
static MqttQoS lowerQosToTheSubscriptionDesired(Subscription sub, MqttQoS qos) {
|
||||
if (qos.value() > sub.getRequestedQos().value()) {
|
||||
qos = sub.getRequestedQos();
|
||||
}
|
||||
return qos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intended usage is only for embedded versions of the broker, where the hosting application
|
||||
* want to use the broker to send a publish message. Like normal external publish message but
|
||||
* with some changes to avoid security check, and the handshake phases for Qos1 and Qos2. It
|
||||
* also doesn't notifyTopicPublished because using internally the owner should already know
|
||||
* where it's publishing.
|
||||
*
|
||||
* @param msg
|
||||
* the message to publish
|
||||
*/
|
||||
public void internalPublish(MqttPublishMessage msg) {
|
||||
final MqttQoS qos = msg.fixedHeader().qosLevel();
|
||||
final Topic topic = new Topic(msg.variableHeader().topicName());
|
||||
final ByteBuf payload = msg.payload();
|
||||
LOG.info("Sending internal PUBLISH message Topic={}, qos={}", topic, qos);
|
||||
|
||||
publish2Subscribers(payload, topic, qos);
|
||||
|
||||
if (!msg.fixedHeader().isRetain()) {
|
||||
return;
|
||||
}
|
||||
if (qos == AT_MOST_ONCE || msg.payload().readableBytes() == 0) {
|
||||
// QoS == 0 && retain => clean old retained
|
||||
retainedRepository.cleanRetained(topic);
|
||||
return;
|
||||
}
|
||||
retainedRepository.retain(topic, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* notify MqttConnectMessage after connection established (already pass login).
|
||||
* @param msg
|
||||
*/
|
||||
void dispatchConnection(MqttConnectMessage msg){
|
||||
interceptor.notifyClientConnected(msg);
|
||||
}
|
||||
|
||||
void dispatchDisconnection(String clientId,String userName){
|
||||
interceptor.notifyClientDisconnected(clientId,userName);
|
||||
}
|
||||
|
||||
void dispatchConnectionLost(String clientId,String userName){
|
||||
interceptor.notifyClientConnectionLost(clientId,userName);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package io.moquette.broker;
|
||||
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class RetainedMessage implements Serializable{
|
||||
|
||||
private final MqttQoS qos;
|
||||
private final byte[] payload;
|
||||
|
||||
public RetainedMessage(MqttQoS qos, byte[] payload) {
|
||||
this.qos = qos;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public MqttQoS qosLevel() {
|
||||
return qos;
|
||||
}
|
||||
|
||||
public byte[] getPayload() {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@@ -1,389 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.broker;
|
||||
|
||||
import io.moquette.BrokerConstants;
|
||||
import io.moquette.broker.config.*;
|
||||
import io.moquette.interception.InterceptHandler;
|
||||
import io.moquette.persistence.H2Builder;
|
||||
import io.moquette.persistence.MemorySubscriptionsRepository;
|
||||
import io.moquette.interception.BrokerInterceptor;
|
||||
import io.moquette.broker.security.*;
|
||||
import io.moquette.broker.subscriptions.CTrieSubscriptionDirectory;
|
||||
import io.moquette.broker.subscriptions.ISubscriptionsDirectory;
|
||||
import io.moquette.broker.security.IAuthenticator;
|
||||
import io.moquette.broker.security.IAuthorizatorPolicy;
|
||||
import io.netty.handler.codec.mqtt.MqttPublishMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.text.ParseException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import static io.moquette.logging.LoggingUtils.getInterceptorIds;
|
||||
|
||||
public class Server {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(io.moquette.broker.Server.class);
|
||||
|
||||
private ScheduledExecutorService scheduler;
|
||||
private NewNettyAcceptor acceptor;
|
||||
private volatile boolean initialized;
|
||||
private PostOffice dispatcher;
|
||||
private BrokerInterceptor interceptor;
|
||||
private H2Builder h2Builder;
|
||||
private SessionRegistry sessions;
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
final Server server = new Server();
|
||||
server.startServer();
|
||||
System.out.println("Server started, version 0.12.1");
|
||||
//Bind a shutdown hook
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(server::stopServer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts Moquette bringing the configuration from the file located at m_config/moquette.conf
|
||||
*
|
||||
* @throws IOException in case of any IO error.
|
||||
*/
|
||||
public void startServer() throws IOException {
|
||||
File defaultConfigurationFile = defaultConfigFile();
|
||||
LOG.info("Starting Moquette integration. Configuration file path={}", defaultConfigurationFile.getAbsolutePath());
|
||||
IResourceLoader filesystemLoader = new FileResourceLoader(defaultConfigurationFile);
|
||||
final IConfig config = new ResourceLoaderConfig(filesystemLoader);
|
||||
startServer(config);
|
||||
}
|
||||
|
||||
private static File defaultConfigFile() {
|
||||
String configPath = System.getProperty("moquette.path", null);
|
||||
return new File(configPath, IConfig.DEFAULT_CONFIG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts Moquette bringing the configuration from the given file
|
||||
*
|
||||
* @param configFile text file that contains the configuration.
|
||||
* @throws IOException in case of any IO Error.
|
||||
*/
|
||||
public void startServer(File configFile) throws IOException {
|
||||
LOG.info("Starting Moquette integration. Configuration file path: {}", configFile.getAbsolutePath());
|
||||
IResourceLoader filesystemLoader = new FileResourceLoader(configFile);
|
||||
final IConfig config = new ResourceLoaderConfig(filesystemLoader);
|
||||
startServer(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the integration with the given properties.
|
||||
* <p>
|
||||
* Its suggested to at least have the following properties:
|
||||
* <ul>
|
||||
* <li>port</li>
|
||||
* <li>password_file</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param configProps the properties map to use as configuration.
|
||||
* @throws IOException in case of any IO Error.
|
||||
*/
|
||||
public void startServer(Properties configProps) throws IOException {
|
||||
LOG.debug("Starting Moquette integration using properties object");
|
||||
final IConfig config = new MemoryConfig(configProps);
|
||||
startServer(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts Moquette bringing the configuration files from the given Config implementation.
|
||||
*
|
||||
* @param config the configuration to use to start the broker.
|
||||
* @throws IOException in case of any IO Error.
|
||||
*/
|
||||
public void startServer(IConfig config) throws IOException {
|
||||
LOG.debug("Starting Moquette integration using IConfig instance");
|
||||
startServer(config, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts Moquette with config provided by an implementation of IConfig class and with the set
|
||||
* of InterceptHandler.
|
||||
*
|
||||
* @param config the configuration to use to start the broker.
|
||||
* @param handlers the handlers to install in the broker.
|
||||
* @throws IOException in case of any IO Error.
|
||||
*/
|
||||
public void startServer(IConfig config, List<? extends InterceptHandler> handlers) throws IOException {
|
||||
LOG.debug("Starting moquette integration using IConfig instance and intercept handlers");
|
||||
startServer(config, handlers, null, null, null);
|
||||
}
|
||||
|
||||
public void startServer(IConfig config, List<? extends InterceptHandler> handlers, ISslContextCreator sslCtxCreator,
|
||||
IAuthenticator authenticator, IAuthorizatorPolicy authorizatorPolicy) {
|
||||
final long start = System.currentTimeMillis();
|
||||
if (handlers == null) {
|
||||
handlers = Collections.emptyList();
|
||||
}
|
||||
LOG.trace("Starting Moquette Server. MQTT message interceptors={}", getInterceptorIds(handlers));
|
||||
|
||||
scheduler = Executors.newScheduledThreadPool(1);
|
||||
|
||||
final String handlerProp = System.getProperty(BrokerConstants.INTERCEPT_HANDLER_PROPERTY_NAME);
|
||||
if (handlerProp != null) {
|
||||
config.setProperty(BrokerConstants.INTERCEPT_HANDLER_PROPERTY_NAME, handlerProp);
|
||||
}
|
||||
final String persistencePath = config.getProperty(BrokerConstants.PERSISTENT_STORE_PROPERTY_NAME);
|
||||
LOG.debug("Configuring Using persistent store file, path: {}", persistencePath);
|
||||
initInterceptors(config, handlers);
|
||||
LOG.debug("Initialized MQTT protocol processor");
|
||||
if (sslCtxCreator == null) {
|
||||
LOG.info("Using default SSL context creator");
|
||||
sslCtxCreator = new DefaultMoquetteSslContextCreator(config);
|
||||
}
|
||||
authenticator = initializeAuthenticator(authenticator, config);
|
||||
authorizatorPolicy = initializeAuthorizatorPolicy(authorizatorPolicy, config);
|
||||
|
||||
final ISubscriptionsRepository subscriptionsRepository;
|
||||
final IQueueRepository queueRepository;
|
||||
final IRetainedRepository retainedRepository;
|
||||
if (persistencePath != null && !persistencePath.isEmpty()) {
|
||||
LOG.trace("Configuring H2 subscriptions store to {}", persistencePath);
|
||||
h2Builder = new H2Builder(config, scheduler).initStore();
|
||||
subscriptionsRepository = h2Builder.subscriptionsRepository();
|
||||
queueRepository = h2Builder.queueRepository();
|
||||
retainedRepository = h2Builder.retainedRepository();
|
||||
} else {
|
||||
LOG.trace("Configuring in-memory subscriptions store");
|
||||
subscriptionsRepository = new MemorySubscriptionsRepository();
|
||||
queueRepository = new MemoryQueueRepository();
|
||||
retainedRepository = new MemoryRetainedRepository();
|
||||
}
|
||||
|
||||
ISubscriptionsDirectory subscriptions = new CTrieSubscriptionDirectory();
|
||||
subscriptions.init(subscriptionsRepository);
|
||||
final Authorizator authorizator = new Authorizator(authorizatorPolicy);
|
||||
sessions = new SessionRegistry(subscriptions, queueRepository, authorizator);
|
||||
dispatcher = new PostOffice(subscriptions, retainedRepository, sessions, interceptor, authorizator);
|
||||
final BrokerConfiguration brokerConfig = new BrokerConfiguration(config);
|
||||
MQTTConnectionFactory connectionFactory = new MQTTConnectionFactory(brokerConfig, authenticator, sessions,
|
||||
dispatcher);
|
||||
|
||||
final NewNettyMQTTHandler mqttHandler = new NewNettyMQTTHandler(connectionFactory);
|
||||
acceptor = new NewNettyAcceptor();
|
||||
acceptor.initialize(mqttHandler, config, sslCtxCreator);
|
||||
|
||||
final long startTime = System.currentTimeMillis() - start;
|
||||
LOG.info("Moquette integration has been started successfully in {} ms", startTime);
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
private IAuthorizatorPolicy initializeAuthorizatorPolicy(IAuthorizatorPolicy authorizatorPolicy, IConfig props) {
|
||||
LOG.debug("Configuring MQTT authorizator policy");
|
||||
String authorizatorClassName = props.getProperty(BrokerConstants.AUTHORIZATOR_CLASS_NAME, "");
|
||||
if (authorizatorPolicy == null && !authorizatorClassName.isEmpty()) {
|
||||
authorizatorPolicy = loadClass(authorizatorClassName, IAuthorizatorPolicy.class, IConfig.class, props);
|
||||
}
|
||||
|
||||
if (authorizatorPolicy == null) {
|
||||
String aclFilePath = props.getProperty(BrokerConstants.ACL_FILE_PROPERTY_NAME, "");
|
||||
if (aclFilePath != null && !aclFilePath.isEmpty()) {
|
||||
authorizatorPolicy = new DenyAllAuthorizatorPolicy();
|
||||
try {
|
||||
LOG.info("Parsing ACL file. Path = {}", aclFilePath);
|
||||
IResourceLoader resourceLoader = props.getResourceLoader();
|
||||
authorizatorPolicy = ACLFileParser.parse(resourceLoader.loadResource(aclFilePath));
|
||||
} catch (ParseException pex) {
|
||||
LOG.error("Unable to parse ACL file. path = {}", aclFilePath, pex);
|
||||
}
|
||||
} else {
|
||||
authorizatorPolicy = new PermitAllAuthorizatorPolicy();
|
||||
}
|
||||
LOG.info("Authorizator policy {} instance will be used", authorizatorPolicy.getClass().getName());
|
||||
}
|
||||
return authorizatorPolicy;
|
||||
}
|
||||
|
||||
private IAuthenticator initializeAuthenticator(IAuthenticator authenticator, IConfig props) {
|
||||
LOG.debug("Configuring MQTT authenticator");
|
||||
String authenticatorClassName = props.getProperty(BrokerConstants.AUTHENTICATOR_CLASS_NAME, "");
|
||||
|
||||
if (authenticator == null && !authenticatorClassName.isEmpty()) {
|
||||
authenticator = loadClass(authenticatorClassName, IAuthenticator.class, IConfig.class, props);
|
||||
}
|
||||
|
||||
IResourceLoader resourceLoader = props.getResourceLoader();
|
||||
if (authenticator == null) {
|
||||
String passwdPath = props.getProperty(BrokerConstants.PASSWORD_FILE_PROPERTY_NAME, "");
|
||||
if (passwdPath.isEmpty()) {
|
||||
authenticator = new AcceptAllAuthenticator();
|
||||
} else {
|
||||
authenticator = new ResourceAuthenticator(resourceLoader, passwdPath);
|
||||
}
|
||||
LOG.info("An {} authenticator instance will be used", authenticator.getClass().getName());
|
||||
}
|
||||
return authenticator;
|
||||
}
|
||||
|
||||
private void initInterceptors(IConfig props, List<? extends InterceptHandler> embeddedObservers) {
|
||||
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,
|
||||
io.moquette.broker.Server.class, this);
|
||||
if (handler != null) {
|
||||
observers.add(handler);
|
||||
}
|
||||
}
|
||||
interceptor = new BrokerInterceptor(props, observers);
|
||||
}
|
||||
|
||||
@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)
|
||||
.getDeclaredConstructor().newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException |
|
||||
NoSuchMethodException | InvocationTargetException ex) {
|
||||
LOG.error("Unable to invoke default constructor. ClassName={}, interfaceName={}, cause={}, " +
|
||||
"errorMessage={}", className, intrface.getName(), ex.getCause(), ex.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the broker to publish a message. It's intended for embedding applications. It can be used
|
||||
* only after the integration is correctly started with startServer.
|
||||
*
|
||||
* @param msg the message to forward.
|
||||
* @param clientId the id of the sending integration.
|
||||
* @throws IllegalStateException if the integration is not yet started
|
||||
*/
|
||||
public void internalPublish(MqttPublishMessage msg, final String clientId) {
|
||||
final int messageID = msg.variableHeader().packetId();
|
||||
if (!initialized) {
|
||||
LOG.error("Moquette is not started, internal message cannot be published. CId: {}, messageId: {}", clientId,
|
||||
messageID);
|
||||
throw new IllegalStateException("Can't publish on a integration is not yet started");
|
||||
}
|
||||
LOG.trace("Internal publishing message CId: {}, messageId: {}", clientId, messageID);
|
||||
dispatcher.internalPublish(msg);
|
||||
}
|
||||
|
||||
public void stopServer() {
|
||||
LOG.info("Unbinding integration from the configured ports");
|
||||
acceptor.close();
|
||||
LOG.trace("Stopping MQTT protocol processor");
|
||||
initialized = false;
|
||||
|
||||
// calling shutdown() does not actually stop tasks that are not cancelled,
|
||||
// and SessionsRepository does not stop its tasks. Thus shutdownNow().
|
||||
scheduler.shutdownNow();
|
||||
|
||||
if (h2Builder != null) {
|
||||
LOG.trace("Shutting down H2 persistence {}");
|
||||
h2Builder.closeStore();
|
||||
}
|
||||
|
||||
LOG.info("Moquette integration has been stopped.");
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return acceptor.getPort();
|
||||
}
|
||||
|
||||
public int getSslPort() {
|
||||
return acceptor.getSslPort();
|
||||
}
|
||||
|
||||
/**
|
||||
* SPI method used by Broker embedded applications to get list of subscribers. Returns null if
|
||||
* the broker is not started.
|
||||
*
|
||||
* @return list of subscriptions.
|
||||
*/
|
||||
// TODO reimplement this
|
||||
// public List<Subscription> getSubscriptions() {
|
||||
// if (m_processorBootstrapper == null) {
|
||||
// return null;
|
||||
// }
|
||||
// return this.subscriptionsStore.listAllSubscriptions();
|
||||
// }
|
||||
|
||||
/**
|
||||
* SPI method used by Broker embedded applications to add intercept handlers.
|
||||
*
|
||||
* @param interceptHandler the handler to add.
|
||||
*/
|
||||
public void addInterceptHandler(InterceptHandler interceptHandler) {
|
||||
if (!initialized) {
|
||||
LOG.error("Moquette is not started, MQTT message interceptor cannot be added. InterceptorId={}",
|
||||
interceptHandler.getID());
|
||||
throw new IllegalStateException("Can't register interceptors on a integration that is not yet started");
|
||||
}
|
||||
LOG.info("Adding MQTT message interceptor. InterceptorId={}", interceptHandler.getID());
|
||||
interceptor.addInterceptHandler(interceptHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* SPI method used by Broker embedded applications to remove intercept handlers.
|
||||
*
|
||||
* @param interceptHandler the handler to remove.
|
||||
*/
|
||||
public void removeInterceptHandler(InterceptHandler interceptHandler) {
|
||||
if (!initialized) {
|
||||
LOG.error("Moquette is not started, MQTT message interceptor cannot be removed. InterceptorId={}",
|
||||
interceptHandler.getID());
|
||||
throw new IllegalStateException("Can't deregister interceptors from a integration that is not yet started");
|
||||
}
|
||||
LOG.info("Removing MQTT message interceptor. InterceptorId={}", interceptHandler.getID());
|
||||
interceptor.removeInterceptHandler(interceptHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of descriptors of connected clients.
|
||||
* */
|
||||
public Collection<ClientDescriptor> listConnectedClients() {
|
||||
return sessions.listConnectedClients();
|
||||
}
|
||||
}
|
||||
@@ -1,388 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.broker;
|
||||
|
||||
import io.moquette.broker.subscriptions.Subscription;
|
||||
import io.moquette.broker.subscriptions.Topic;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.mqtt.*;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.DelayQueue;
|
||||
import java.util.concurrent.Delayed;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
class Session {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Session.class);
|
||||
private static final int FLIGHT_BEFORE_RESEND_MS = 5_000;
|
||||
private static final int INFLIGHT_WINDOW_SIZE = 10;
|
||||
|
||||
static class InFlightPacket implements Delayed {
|
||||
|
||||
final int packetId;
|
||||
private long startTime;
|
||||
|
||||
InFlightPacket(int packetId, long delayInMilliseconds) {
|
||||
this.packetId = packetId;
|
||||
this.startTime = System.currentTimeMillis() + delayInMilliseconds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDelay(TimeUnit unit) {
|
||||
long diff = startTime - System.currentTimeMillis();
|
||||
return unit.convert(diff, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Delayed o) {
|
||||
if ((this.startTime - ((InFlightPacket) o).startTime) == 0) {
|
||||
return 0;
|
||||
}
|
||||
if ((this.startTime - ((InFlightPacket) o).startTime) > 0) {
|
||||
return 1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SessionStatus {
|
||||
CONNECTED, CONNECTING, DISCONNECTING, DISCONNECTED
|
||||
}
|
||||
|
||||
static final class Will {
|
||||
|
||||
final String topic;
|
||||
final ByteBuf payload;
|
||||
final MqttQoS qos;
|
||||
final boolean retained;
|
||||
|
||||
Will(String topic, ByteBuf payload, MqttQoS qos, boolean retained) {
|
||||
this.topic = topic;
|
||||
this.payload = payload;
|
||||
this.qos = qos;
|
||||
this.retained = retained;
|
||||
}
|
||||
}
|
||||
|
||||
private final String clientId;
|
||||
private boolean clean;
|
||||
private Will will;
|
||||
private Queue<SessionRegistry.EnqueuedMessage> sessionQueue;
|
||||
private final AtomicReference<SessionStatus> status = new AtomicReference<>(SessionStatus.DISCONNECTED);
|
||||
private MQTTConnection mqttConnection;
|
||||
private List<Subscription> subscriptions = new ArrayList<>();
|
||||
private final Map<Integer, SessionRegistry.EnqueuedMessage> inflightWindow = new HashMap<>();
|
||||
private final DelayQueue<InFlightPacket> inflightTimeouts = new DelayQueue<>();
|
||||
private final Map<Integer, MqttPublishMessage> qos2Receiving = new HashMap<>();
|
||||
private final AtomicInteger inflightSlots = new AtomicInteger(INFLIGHT_WINDOW_SIZE); // this should be configurable
|
||||
|
||||
Session(String clientId, boolean clean, Will will, Queue<SessionRegistry.EnqueuedMessage> sessionQueue) {
|
||||
this(clientId, clean, sessionQueue);
|
||||
this.will = will;
|
||||
}
|
||||
|
||||
Session(String clientId, boolean clean, Queue<SessionRegistry.EnqueuedMessage> sessionQueue) {
|
||||
this.clientId = clientId;
|
||||
this.clean = clean;
|
||||
this.sessionQueue = sessionQueue;
|
||||
}
|
||||
|
||||
void update(boolean clean, Will will) {
|
||||
this.clean = clean;
|
||||
this.will = will;
|
||||
}
|
||||
|
||||
void markConnected() {
|
||||
assignState(SessionStatus.DISCONNECTED, SessionStatus.CONNECTED);
|
||||
}
|
||||
|
||||
void bind(MQTTConnection mqttConnection) {
|
||||
this.mqttConnection = mqttConnection;
|
||||
}
|
||||
|
||||
public boolean disconnected() {
|
||||
return status.get() == SessionStatus.DISCONNECTED;
|
||||
}
|
||||
|
||||
public boolean connected() {
|
||||
return status.get() == SessionStatus.CONNECTED;
|
||||
}
|
||||
|
||||
public String getClientID() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public List<Subscription> getSubscriptions() {
|
||||
return new ArrayList<>(subscriptions);
|
||||
}
|
||||
|
||||
public void addSubscriptions(List<Subscription> newSubscriptions) {
|
||||
subscriptions.addAll(newSubscriptions);
|
||||
}
|
||||
|
||||
public boolean hasWill() {
|
||||
return will != null;
|
||||
}
|
||||
|
||||
public Will getWill() {
|
||||
return will;
|
||||
}
|
||||
|
||||
boolean assignState(SessionStatus expected, SessionStatus newState) {
|
||||
return status.compareAndSet(expected, newState);
|
||||
}
|
||||
|
||||
public void closeImmediately() {
|
||||
mqttConnection.dropConnection();
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
final boolean res = assignState(SessionStatus.CONNECTED, SessionStatus.DISCONNECTING);
|
||||
if (!res) {
|
||||
// someone already moved away from CONNECTED
|
||||
// TODO what to do?
|
||||
return;
|
||||
}
|
||||
|
||||
mqttConnection = null;
|
||||
will = null;
|
||||
|
||||
assignState(SessionStatus.DISCONNECTING, SessionStatus.DISCONNECTED);
|
||||
}
|
||||
|
||||
boolean isClean() {
|
||||
return clean;
|
||||
}
|
||||
|
||||
public void processPubRec(int packetId) {
|
||||
inflightWindow.remove(packetId);
|
||||
inflightSlots.incrementAndGet();
|
||||
if (canSkipQueue()) {
|
||||
inflightSlots.decrementAndGet();
|
||||
int pubRelPacketId = packetId/*mqttConnection.nextPacketId()*/;
|
||||
inflightWindow.put(pubRelPacketId, new SessionRegistry.PubRelMarker());
|
||||
inflightTimeouts.add(new InFlightPacket(pubRelPacketId, FLIGHT_BEFORE_RESEND_MS));
|
||||
MqttMessage pubRel = MQTTConnection.pubrel(pubRelPacketId);
|
||||
mqttConnection.sendIfWritableElseDrop(pubRel);
|
||||
|
||||
drainQueueToConnection();
|
||||
} else {
|
||||
sessionQueue.add(new SessionRegistry.PubRelMarker());
|
||||
}
|
||||
}
|
||||
|
||||
public void processPubComp(int messageID) {
|
||||
inflightWindow.remove(messageID);
|
||||
inflightSlots.incrementAndGet();
|
||||
|
||||
drainQueueToConnection();
|
||||
|
||||
// TODO notify the interceptor
|
||||
// final InterceptAcknowledgedMessage interceptAckMsg = new InterceptAcknowledgedMessage(inflightMsg,
|
||||
// topic, username, messageID);
|
||||
// m_interceptor.notifyMessageAcknowledged(interceptAckMsg);
|
||||
}
|
||||
|
||||
public void sendPublishOnSessionAtQos(Topic topic, MqttQoS qos, ByteBuf payload) {
|
||||
switch (qos) {
|
||||
case AT_MOST_ONCE:
|
||||
if (connected()) {
|
||||
mqttConnection.sendPublishNotRetainedQos0(topic, qos, payload);
|
||||
}
|
||||
break;
|
||||
case AT_LEAST_ONCE:
|
||||
sendPublishQos1(topic, qos, payload);
|
||||
break;
|
||||
case EXACTLY_ONCE:
|
||||
sendPublishQos2(topic, qos, payload);
|
||||
break;
|
||||
case FAILURE:
|
||||
LOG.error("Not admissible");
|
||||
}
|
||||
}
|
||||
|
||||
private void sendPublishQos1(Topic topic, MqttQoS qos, ByteBuf payload) {
|
||||
if (!connected() && isClean()) {
|
||||
//pushing messages to disconnected not clean session
|
||||
return;
|
||||
}
|
||||
|
||||
if (canSkipQueue()) {
|
||||
inflightSlots.decrementAndGet();
|
||||
int packetId = mqttConnection.nextPacketId();
|
||||
inflightWindow.put(packetId, new SessionRegistry.PublishedMessage(topic, qos, payload));
|
||||
inflightTimeouts.add(new InFlightPacket(packetId, FLIGHT_BEFORE_RESEND_MS));
|
||||
MqttPublishMessage publishMsg = MQTTConnection.notRetainedPublishWithMessageId(topic.toString(), qos,
|
||||
payload, packetId);
|
||||
mqttConnection.sendPublish(publishMsg);
|
||||
|
||||
// TODO drainQueueToConnection();?
|
||||
} else {
|
||||
final SessionRegistry.PublishedMessage msg = new SessionRegistry.PublishedMessage(topic, qos, payload);
|
||||
sessionQueue.add(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendPublishQos2(Topic topic, MqttQoS qos, ByteBuf payload) {
|
||||
if (canSkipQueue()) {
|
||||
inflightSlots.decrementAndGet();
|
||||
int packetId = mqttConnection.nextPacketId();
|
||||
inflightWindow.put(packetId, new SessionRegistry.PublishedMessage(topic, qos, payload));
|
||||
inflightTimeouts.add(new InFlightPacket(packetId, FLIGHT_BEFORE_RESEND_MS));
|
||||
MqttPublishMessage publishMsg = MQTTConnection.notRetainedPublishWithMessageId(topic.toString(), qos,
|
||||
payload, packetId);
|
||||
mqttConnection.sendPublish(publishMsg);
|
||||
|
||||
drainQueueToConnection();
|
||||
} else {
|
||||
final SessionRegistry.PublishedMessage msg = new SessionRegistry.PublishedMessage(topic, qos, payload);
|
||||
sessionQueue.add(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canSkipQueue() {
|
||||
return sessionQueue.isEmpty() &&
|
||||
inflightSlots.get() > 0 &&
|
||||
connected() &&
|
||||
mqttConnection.channel.isWritable();
|
||||
}
|
||||
|
||||
private boolean inflighHasSlotsAndConnectionIsUp() {
|
||||
return inflightSlots.get() > 0 &&
|
||||
connected() &&
|
||||
mqttConnection.channel.isWritable();
|
||||
}
|
||||
|
||||
void pubAckReceived(int ackPacketId) {
|
||||
// TODO remain to invoke in somehow m_interceptor.notifyMessageAcknowledged
|
||||
inflightWindow.remove(ackPacketId);
|
||||
inflightSlots.incrementAndGet();
|
||||
drainQueueToConnection();
|
||||
}
|
||||
|
||||
public void resendInflightNotAcked() {
|
||||
Collection<InFlightPacket> expired = new ArrayList<>(INFLIGHT_WINDOW_SIZE);
|
||||
inflightTimeouts.drainTo(expired);
|
||||
|
||||
debugLogPacketIds(expired);
|
||||
|
||||
for (InFlightPacket notAckPacketId : expired) {
|
||||
if (inflightWindow.containsKey(notAckPacketId.packetId)) {
|
||||
final SessionRegistry.PublishedMessage msg =
|
||||
(SessionRegistry.PublishedMessage) inflightWindow.get(notAckPacketId.packetId);
|
||||
final Topic topic = msg.topic;
|
||||
final MqttQoS qos = msg.publishingQos;
|
||||
final ByteBuf payload = msg.payload;
|
||||
final ByteBuf copiedPayload = payload.retainedDuplicate();
|
||||
MqttPublishMessage publishMsg = publishNotRetainedDuplicated(notAckPacketId, topic, qos, copiedPayload);
|
||||
mqttConnection.sendPublish(publishMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void debugLogPacketIds(Collection<InFlightPacket> expired) {
|
||||
if (!LOG.isDebugEnabled() || expired.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (InFlightPacket packet : expired) {
|
||||
sb.append(packet.packetId).append(", ");
|
||||
}
|
||||
LOG.debug("Resending {} in flight packets [{}]", expired.size(), sb);
|
||||
}
|
||||
|
||||
private MqttPublishMessage publishNotRetainedDuplicated(InFlightPacket notAckPacketId, Topic topic, MqttQoS qos,
|
||||
ByteBuf payload) {
|
||||
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, true, qos, false, 0);
|
||||
MqttPublishVariableHeader varHeader = new MqttPublishVariableHeader(topic.toString(), notAckPacketId.packetId);
|
||||
return new MqttPublishMessage(fixedHeader, varHeader, payload);
|
||||
}
|
||||
|
||||
private void drainQueueToConnection() {
|
||||
// consume the queue
|
||||
while (!sessionQueue.isEmpty() && inflighHasSlotsAndConnectionIsUp()) {
|
||||
final SessionRegistry.EnqueuedMessage msg = sessionQueue.remove();
|
||||
inflightSlots.decrementAndGet();
|
||||
int sendPacketId = mqttConnection.nextPacketId();
|
||||
inflightWindow.put(sendPacketId, msg);
|
||||
if (msg instanceof SessionRegistry.PubRelMarker) {
|
||||
MqttMessage pubRel = MQTTConnection.pubrel(sendPacketId);
|
||||
mqttConnection.sendIfWritableElseDrop(pubRel);
|
||||
} else {
|
||||
final SessionRegistry.PublishedMessage msgPub = (SessionRegistry.PublishedMessage) msg;
|
||||
MqttPublishMessage publishMsg = MQTTConnection.notRetainedPublishWithMessageId(msgPub.topic.toString(),
|
||||
msgPub.publishingQos,
|
||||
msgPub.payload, sendPacketId);
|
||||
mqttConnection.sendPublish(publishMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void writabilityChanged() {
|
||||
drainQueueToConnection();
|
||||
}
|
||||
|
||||
public void sendQueuedMessagesWhileOffline() {
|
||||
LOG.trace("Republishing all saved messages for session {} on CId={}", this, this.clientId);
|
||||
drainQueueToConnection();
|
||||
}
|
||||
|
||||
void sendRetainedPublishOnSessionAtQos(Topic topic, MqttQoS qos, ByteBuf payload) {
|
||||
if (qos != MqttQoS.AT_MOST_ONCE) {
|
||||
// QoS 1 or 2
|
||||
mqttConnection.sendPublishRetainedWithPacketId(topic, qos, payload);
|
||||
} else {
|
||||
mqttConnection.sendPublishRetainedQos0(topic, qos, payload);
|
||||
}
|
||||
}
|
||||
|
||||
public void receivedPublishQos2(int messageID, MqttPublishMessage msg) {
|
||||
qos2Receiving.put(messageID, msg);
|
||||
msg.retain(); // retain to put in the inflight map
|
||||
mqttConnection.sendPublishReceived(messageID);
|
||||
}
|
||||
|
||||
public void receivedPubRelQos2(int messageID) {
|
||||
final MqttPublishMessage removedMsg = qos2Receiving.remove(messageID);
|
||||
ReferenceCountUtil.release(removedMsg);
|
||||
}
|
||||
|
||||
Optional<InetSocketAddress> remoteAddress() {
|
||||
if (connected()) {
|
||||
return Optional.of(mqttConnection.remoteAddress());
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Session{" +
|
||||
"clientId='" + clientId + '\'' +
|
||||
", clean=" + clean +
|
||||
", status=" + status +
|
||||
", inflightSlots=" + inflightSlots +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.broker;
|
||||
|
||||
import io.moquette.broker.Session.SessionStatus;
|
||||
import io.moquette.broker.subscriptions.ISubscriptionsDirectory;
|
||||
import io.moquette.broker.subscriptions.Subscription;
|
||||
import io.moquette.broker.subscriptions.Topic;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.handler.codec.mqtt.MqttConnectMessage;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SessionRegistry {
|
||||
|
||||
public abstract static class EnqueuedMessage {
|
||||
}
|
||||
|
||||
static class PublishedMessage extends EnqueuedMessage {
|
||||
|
||||
final Topic topic;
|
||||
final MqttQoS publishingQos;
|
||||
final ByteBuf payload;
|
||||
|
||||
PublishedMessage(Topic topic, MqttQoS publishingQos, ByteBuf payload) {
|
||||
this.topic = topic;
|
||||
this.publishingQos = publishingQos;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
static final class PubRelMarker extends EnqueuedMessage {
|
||||
}
|
||||
|
||||
private enum PostConnectAction {
|
||||
NONE, SEND_STORED_MESSAGES
|
||||
}
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SessionRegistry.class);
|
||||
|
||||
private final ConcurrentMap<String, Session> pool = new ConcurrentHashMap<>();
|
||||
private final ISubscriptionsDirectory subscriptionsDirectory;
|
||||
private final IQueueRepository queueRepository;
|
||||
private final Authorizator authorizator;
|
||||
private final ConcurrentMap<String, Queue<SessionRegistry.EnqueuedMessage>> queues = new ConcurrentHashMap<>();
|
||||
|
||||
SessionRegistry(ISubscriptionsDirectory subscriptionsDirectory,
|
||||
IQueueRepository queueRepository,
|
||||
Authorizator authorizator) {
|
||||
this.subscriptionsDirectory = subscriptionsDirectory;
|
||||
this.queueRepository = queueRepository;
|
||||
this.authorizator = authorizator;
|
||||
}
|
||||
|
||||
void bindToSession(MQTTConnection mqttConnection, MqttConnectMessage msg, String clientId) {
|
||||
boolean isSessionAlreadyStored = false;
|
||||
PostConnectAction postConnectAction = PostConnectAction.NONE;
|
||||
if (!pool.containsKey(clientId)) {
|
||||
// case 1
|
||||
final Session newSession = createNewSession(mqttConnection, msg, clientId);
|
||||
|
||||
// publish the session
|
||||
final Session previous = pool.putIfAbsent(clientId, newSession);
|
||||
final boolean success = previous == null;
|
||||
|
||||
if (success) {
|
||||
LOG.trace("case 1, not existing session with CId {}", clientId);
|
||||
} else {
|
||||
postConnectAction = bindToExistingSession(mqttConnection, msg, clientId, newSession);
|
||||
isSessionAlreadyStored = true;
|
||||
}
|
||||
} else {
|
||||
final Session newSession = createNewSession(mqttConnection, msg, clientId);
|
||||
postConnectAction = bindToExistingSession(mqttConnection, msg, clientId, newSession);
|
||||
isSessionAlreadyStored = true;
|
||||
}
|
||||
final boolean msgCleanSessionFlag = msg.variableHeader().isCleanSession();
|
||||
boolean isSessionAlreadyPresent = !msgCleanSessionFlag && isSessionAlreadyStored;
|
||||
mqttConnection.sendConnAck(isSessionAlreadyPresent);
|
||||
|
||||
if (postConnectAction == PostConnectAction.SEND_STORED_MESSAGES) {
|
||||
final Session session = pool.get(clientId);
|
||||
session.sendQueuedMessagesWhileOffline();
|
||||
}
|
||||
}
|
||||
|
||||
private PostConnectAction bindToExistingSession(MQTTConnection mqttConnection, MqttConnectMessage msg,
|
||||
String clientId, Session newSession) {
|
||||
PostConnectAction postConnectAction = PostConnectAction.NONE;
|
||||
final boolean newIsClean = msg.variableHeader().isCleanSession();
|
||||
final Session oldSession = pool.get(clientId);
|
||||
if (newIsClean && oldSession.disconnected()) {
|
||||
// case 2
|
||||
dropQueuesForClient(clientId);
|
||||
unsubscribe(oldSession);
|
||||
|
||||
// publish new session
|
||||
boolean result = oldSession.assignState(SessionStatus.DISCONNECTED, SessionStatus.CONNECTING);
|
||||
if (!result) {
|
||||
throw new SessionCorruptedException("old session was already changed state");
|
||||
}
|
||||
copySessionConfig(msg, oldSession);
|
||||
oldSession.bind(mqttConnection);
|
||||
|
||||
result = oldSession.assignState(SessionStatus.CONNECTING, SessionStatus.CONNECTED);
|
||||
if (!result) {
|
||||
throw new SessionCorruptedException("old session moved in connected state by other thread");
|
||||
}
|
||||
final boolean published = pool.replace(clientId, oldSession, oldSession);
|
||||
if (!published) {
|
||||
throw new SessionCorruptedException("old session was already removed");
|
||||
}
|
||||
LOG.trace("case 2, oldSession with same CId {} disconnected", clientId);
|
||||
} else if (!newIsClean && oldSession.disconnected()) {
|
||||
// case 3
|
||||
final String username = mqttConnection.getUsername();
|
||||
reactivateSubscriptions(oldSession, username);
|
||||
|
||||
// mark as connected
|
||||
final boolean connecting = oldSession.assignState(SessionStatus.DISCONNECTED, SessionStatus.CONNECTING);
|
||||
if (!connecting) {
|
||||
throw new SessionCorruptedException("old session moved in connected state by other thread");
|
||||
}
|
||||
oldSession.bind(mqttConnection);
|
||||
|
||||
final boolean connected = oldSession.assignState(SessionStatus.CONNECTING, SessionStatus.CONNECTED);
|
||||
if (!connected) {
|
||||
throw new SessionCorruptedException("old session moved in other state state by other thread");
|
||||
}
|
||||
|
||||
// publish new session
|
||||
final boolean published = pool.replace(clientId, oldSession, oldSession);
|
||||
if (!published) {
|
||||
throw new SessionCorruptedException("old session was already removed");
|
||||
}
|
||||
postConnectAction = PostConnectAction.SEND_STORED_MESSAGES;
|
||||
LOG.trace("case 3, oldSession with same CId {} disconnected", clientId);
|
||||
} else if (oldSession.connected()) {
|
||||
// case 4
|
||||
LOG.trace("case 4, oldSession with same CId {} still connected, force to close", clientId);
|
||||
oldSession.closeImmediately();
|
||||
//remove(clientId);
|
||||
// publish new session
|
||||
final boolean published = pool.replace(clientId, oldSession, newSession);
|
||||
if (!published) {
|
||||
throw new SessionCorruptedException("old session was already removed");
|
||||
}
|
||||
}
|
||||
// case not covered new session is clean true/false and old session not in CONNECTED/DISCONNECTED
|
||||
return postConnectAction;
|
||||
}
|
||||
|
||||
private void reactivateSubscriptions(Session session, String username) {
|
||||
//verify if subscription still satisfy read ACL permissions
|
||||
for (Subscription existingSub : session.getSubscriptions()) {
|
||||
final boolean topicReadable = authorizator.canRead(existingSub.getTopicFilter(), username,
|
||||
session.getClientID());
|
||||
if (!topicReadable) {
|
||||
subscriptionsDirectory.removeSubscription(existingSub.getTopicFilter(), session.getClientID());
|
||||
}
|
||||
// TODO
|
||||
// subscriptionsDirectory.reactivate(existingSub.getTopicFilter(), session.getClientID());
|
||||
}
|
||||
}
|
||||
|
||||
private void unsubscribe(Session session) {
|
||||
for (Subscription existingSub : session.getSubscriptions()) {
|
||||
subscriptionsDirectory.removeSubscription(existingSub.getTopicFilter(), session.getClientID());
|
||||
}
|
||||
}
|
||||
|
||||
private Session createNewSession(MQTTConnection mqttConnection, MqttConnectMessage msg, String clientId) {
|
||||
final boolean clean = msg.variableHeader().isCleanSession();
|
||||
final Queue<SessionRegistry.EnqueuedMessage> sessionQueue =
|
||||
queues.computeIfAbsent(clientId, (String cli) -> queueRepository.createQueue(cli, clean));
|
||||
final Session newSession;
|
||||
if (msg.variableHeader().isWillFlag()) {
|
||||
final Session.Will will = createWill(msg);
|
||||
newSession = new Session(clientId, clean, will, sessionQueue);
|
||||
} else {
|
||||
newSession = new Session(clientId, clean, sessionQueue);
|
||||
}
|
||||
|
||||
newSession.markConnected();
|
||||
newSession.bind(mqttConnection);
|
||||
|
||||
return newSession;
|
||||
}
|
||||
|
||||
private void copySessionConfig(MqttConnectMessage msg, Session session) {
|
||||
final boolean clean = msg.variableHeader().isCleanSession();
|
||||
final Session.Will will;
|
||||
if (msg.variableHeader().isWillFlag()) {
|
||||
will = createWill(msg);
|
||||
} else {
|
||||
will = null;
|
||||
}
|
||||
session.update(clean, will);
|
||||
}
|
||||
|
||||
private Session.Will createWill(MqttConnectMessage msg) {
|
||||
final ByteBuf willPayload = Unpooled.copiedBuffer(msg.payload().willMessageInBytes());
|
||||
final String willTopic = msg.payload().willTopic();
|
||||
final boolean retained = msg.variableHeader().isWillRetain();
|
||||
final MqttQoS qos = MqttQoS.valueOf(msg.variableHeader().willQos());
|
||||
return new Session.Will(willTopic, willPayload, qos, retained);
|
||||
}
|
||||
|
||||
Session retrieve(String clientID) {
|
||||
return pool.get(clientID);
|
||||
}
|
||||
|
||||
public void remove(String clientID) {
|
||||
pool.remove(clientID);
|
||||
}
|
||||
|
||||
public void disconnect(String clientID) {
|
||||
final Session session = retrieve(clientID);
|
||||
if (session == null) {
|
||||
LOG.debug("Some other thread already removed the session CId={}", clientID);
|
||||
return;
|
||||
}
|
||||
session.disconnect();
|
||||
}
|
||||
|
||||
private void dropQueuesForClient(String clientId) {
|
||||
queues.remove(clientId);
|
||||
}
|
||||
|
||||
Collection<ClientDescriptor> listConnectedClients() {
|
||||
return pool.values().stream()
|
||||
.filter(Session::connected)
|
||||
.map(this::createClientDescriptor)
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Optional<ClientDescriptor> createClientDescriptor(Session s) {
|
||||
final String clientID = s.getClientID();
|
||||
final Optional<InetSocketAddress> remoteAddressOpt = s.remoteAddress();
|
||||
return remoteAddressOpt.map(r -> new ClientDescriptor(clientID, r.getHostString(), r.getPort()));
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.broker.subscriptions;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
class CNode {
|
||||
|
||||
Token token;
|
||||
private List<INode> children;
|
||||
Set<Subscription> subscriptions;
|
||||
|
||||
CNode() {
|
||||
this.children = new ArrayList<>();
|
||||
this.subscriptions = new HashSet<>();
|
||||
}
|
||||
|
||||
//Copy constructor
|
||||
private CNode(Token token, List<INode> children, Set<Subscription> subscriptions) {
|
||||
this.token = token; // keep reference, root comparison in directory logic relies on it for now.
|
||||
this.subscriptions = new HashSet<>(subscriptions);
|
||||
this.children = new ArrayList<>(children);
|
||||
}
|
||||
|
||||
boolean anyChildrenMatch(Token token) {
|
||||
for (INode iNode : children) {
|
||||
final CNode child = iNode.mainNode();
|
||||
if (child.equalsToken(token)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<INode> allChildren() {
|
||||
return this.children;
|
||||
}
|
||||
|
||||
INode childOf(Token token) {
|
||||
for (INode iNode : children) {
|
||||
final CNode child = iNode.mainNode();
|
||||
if (child.equalsToken(token)) {
|
||||
return iNode;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Asked for a token that doesn't exists in any child [" + token + "]");
|
||||
}
|
||||
|
||||
private boolean equalsToken(Token token) {
|
||||
return token != null && this.token != null && this.token.equals(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(token);
|
||||
}
|
||||
|
||||
CNode copy() {
|
||||
return new CNode(this.token, this.children, this.subscriptions);
|
||||
}
|
||||
|
||||
public void add(INode newINode) {
|
||||
this.children.add(newINode);
|
||||
}
|
||||
public void remove(INode node) {
|
||||
this.children.remove(node);
|
||||
}
|
||||
|
||||
CNode addSubscription(Subscription newSubscription) {
|
||||
// if already contains one with same topic and same client, keep that with higher QoS
|
||||
if (subscriptions.contains(newSubscription)) {
|
||||
final Subscription existing = subscriptions.stream()
|
||||
.filter(s -> s.equals(newSubscription))
|
||||
.findFirst().get();
|
||||
if (existing.getRequestedQos().value() < newSubscription.getRequestedQos().value()) {
|
||||
subscriptions.remove(existing);
|
||||
subscriptions.add(new Subscription(newSubscription));
|
||||
}
|
||||
} else {
|
||||
this.subscriptions.add(new Subscription(newSubscription));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true iff the subscriptions contained in this node are owned by clientId
|
||||
* AND at least one subscription is actually present for that clientId
|
||||
* */
|
||||
boolean containsOnly(String clientId) {
|
||||
for (Subscription sub : this.subscriptions) {
|
||||
if (!sub.clientId.equals(clientId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !this.subscriptions.isEmpty();
|
||||
}
|
||||
|
||||
//TODO this is equivalent to negate(containsOnly(clientId))
|
||||
public boolean contains(String clientId) {
|
||||
for (Subscription sub : this.subscriptions) {
|
||||
if (sub.clientId.equals(clientId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void removeSubscriptionsFor(String clientId) {
|
||||
Set<Subscription> toRemove = new HashSet<>();
|
||||
for (Subscription sub : this.subscriptions) {
|
||||
if (sub.clientId.equals(clientId)) {
|
||||
toRemove.add(sub);
|
||||
}
|
||||
}
|
||||
this.subscriptions.removeAll(toRemove);
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
package io.moquette.broker.subscriptions;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public class CTrie {
|
||||
|
||||
interface IVisitor<T> {
|
||||
|
||||
void visit(CNode node, int deep);
|
||||
|
||||
T getResult();
|
||||
}
|
||||
|
||||
private static final Token ROOT = new Token("root");
|
||||
private static final INode NO_PARENT = null;
|
||||
|
||||
private enum Action {
|
||||
OK, REPEAT
|
||||
}
|
||||
|
||||
INode root;
|
||||
|
||||
CTrie() {
|
||||
final CNode mainNode = new CNode();
|
||||
mainNode.token = ROOT;
|
||||
this.root = new INode(mainNode);
|
||||
}
|
||||
|
||||
Optional<CNode> lookup(Topic topic) {
|
||||
INode inode = this.root;
|
||||
Token token = topic.headToken();
|
||||
while (!topic.isEmpty() && (inode.mainNode().anyChildrenMatch(token))) {
|
||||
topic = topic.exceptHeadToken();
|
||||
inode = inode.mainNode().childOf(token);
|
||||
token = topic.headToken();
|
||||
}
|
||||
if (inode == null || !topic.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(inode.mainNode());
|
||||
}
|
||||
|
||||
enum NavigationAction {
|
||||
MATCH, GODEEP, STOP
|
||||
}
|
||||
|
||||
private NavigationAction evaluate(Topic topic, CNode cnode) {
|
||||
if (Token.MULTI.equals(cnode.token)) {
|
||||
return NavigationAction.MATCH;
|
||||
}
|
||||
if (topic.isEmpty()) {
|
||||
return NavigationAction.STOP;
|
||||
}
|
||||
final Token token = topic.headToken();
|
||||
if (!(Token.SINGLE.equals(cnode.token) || cnode.token.equals(token) || ROOT.equals(cnode.token))) {
|
||||
return NavigationAction.STOP;
|
||||
}
|
||||
return NavigationAction.GODEEP;
|
||||
}
|
||||
|
||||
public Set<Subscription> recursiveMatch(Topic topic) {
|
||||
return recursiveMatch(topic, this.root);
|
||||
}
|
||||
|
||||
private Set<Subscription> recursiveMatch(Topic topic, INode inode) {
|
||||
CNode cnode = inode.mainNode();
|
||||
NavigationAction action = evaluate(topic, cnode);
|
||||
if (action == NavigationAction.MATCH) {
|
||||
return cnode.subscriptions;
|
||||
}
|
||||
if (action == NavigationAction.STOP) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
if (cnode instanceof TNode) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Topic remainingTopic = (ROOT.equals(cnode.token)) ? topic : topic.exceptHeadToken();
|
||||
Set<Subscription> subscriptions = new HashSet<>();
|
||||
if (remainingTopic.isEmpty()) {
|
||||
subscriptions.addAll(cnode.subscriptions);
|
||||
}
|
||||
for (INode subInode : cnode.allChildren()) {
|
||||
subscriptions.addAll(recursiveMatch(remainingTopic, subInode));
|
||||
}
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
public void addToTree(Subscription newSubscription) {
|
||||
Action res;
|
||||
do {
|
||||
res = insert(newSubscription.topicFilter, this.root, newSubscription);
|
||||
} while (res == Action.REPEAT);
|
||||
}
|
||||
|
||||
private Action insert(Topic topic, final INode inode, Subscription newSubscription) {
|
||||
Token token = topic.headToken();
|
||||
if (!topic.isEmpty() && inode.mainNode().anyChildrenMatch(token)) {
|
||||
Topic remainingTopic = topic.exceptHeadToken();
|
||||
INode nextInode = inode.mainNode().childOf(token);
|
||||
return insert(remainingTopic, nextInode, newSubscription);
|
||||
} else {
|
||||
if (topic.isEmpty()) {
|
||||
return insertSubscription(inode, newSubscription);
|
||||
} else {
|
||||
return createNodeAndInsertSubscription(topic, inode, newSubscription);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Action insertSubscription(INode inode, Subscription newSubscription) {
|
||||
CNode cnode = inode.mainNode();
|
||||
CNode updatedCnode = cnode.copy().addSubscription(newSubscription);
|
||||
if (inode.compareAndSet(cnode, updatedCnode)) {
|
||||
return Action.OK;
|
||||
} else {
|
||||
return Action.REPEAT;
|
||||
}
|
||||
}
|
||||
|
||||
private Action createNodeAndInsertSubscription(Topic topic, INode inode, Subscription newSubscription) {
|
||||
INode newInode = createPathRec(topic, newSubscription);
|
||||
CNode cnode = inode.mainNode();
|
||||
CNode updatedCnode = cnode.copy();
|
||||
updatedCnode.add(newInode);
|
||||
|
||||
return inode.compareAndSet(cnode, updatedCnode) ? Action.OK : Action.REPEAT;
|
||||
}
|
||||
|
||||
private INode createPathRec(Topic topic, Subscription newSubscription) {
|
||||
Topic remainingTopic = topic.exceptHeadToken();
|
||||
if (!remainingTopic.isEmpty()) {
|
||||
INode inode = createPathRec(remainingTopic, newSubscription);
|
||||
CNode cnode = new CNode();
|
||||
cnode.token = topic.headToken();
|
||||
cnode.add(inode);
|
||||
return new INode(cnode);
|
||||
} else {
|
||||
return createLeafNodes(topic.headToken(), newSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
private INode createLeafNodes(Token token, Subscription newSubscription) {
|
||||
CNode newLeafCnode = new CNode();
|
||||
newLeafCnode.token = token;
|
||||
newLeafCnode.addSubscription(newSubscription);
|
||||
|
||||
return new INode(newLeafCnode);
|
||||
}
|
||||
|
||||
public void removeFromTree(Topic topic, String clientID) {
|
||||
Action res;
|
||||
do {
|
||||
res = remove(clientID, topic, this.root, NO_PARENT);
|
||||
} while (res == Action.REPEAT);
|
||||
}
|
||||
|
||||
private Action remove(String clientId, Topic topic, INode inode, INode iParent) {
|
||||
Token token = topic.headToken();
|
||||
if (!topic.isEmpty() && (inode.mainNode().anyChildrenMatch(token))) {
|
||||
Topic remainingTopic = topic.exceptHeadToken();
|
||||
INode nextInode = inode.mainNode().childOf(token);
|
||||
return remove(clientId, remainingTopic, nextInode, inode);
|
||||
} else {
|
||||
final CNode cnode = inode.mainNode();
|
||||
if (cnode instanceof TNode) {
|
||||
// this inode is a tomb, has no clients and should be cleaned up
|
||||
// Because we implemented cleanTomb below, this should be rare, but possible
|
||||
// Consider calling cleanTomb here too
|
||||
return Action.OK;
|
||||
}
|
||||
if (cnode.containsOnly(clientId) && topic.isEmpty() && cnode.allChildren().isEmpty()) {
|
||||
// last client to leave this node, AND there are no downstream children, remove via TNode tomb
|
||||
if (inode == this.root) {
|
||||
return inode.compareAndSet(cnode, inode.mainNode().copy()) ? Action.OK : Action.REPEAT;
|
||||
}
|
||||
TNode tnode = new TNode();
|
||||
return inode.compareAndSet(cnode, tnode) ? cleanTomb(inode, iParent) : Action.REPEAT;
|
||||
} else if (cnode.contains(clientId) && topic.isEmpty()) {
|
||||
CNode updatedCnode = cnode.copy();
|
||||
updatedCnode.removeSubscriptionsFor(clientId);
|
||||
return inode.compareAndSet(cnode, updatedCnode) ? Action.OK : Action.REPEAT;
|
||||
} else {
|
||||
//someone else already removed
|
||||
return Action.OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Cleans Disposes of TNode in separate Atomic CAS operation per
|
||||
* http://bravenewgeek.com/breaking-and-entering-lose-the-lock-while-embracing-concurrency/
|
||||
*
|
||||
* We roughly follow this theory above, but we allow CNode with no Subscriptions to linger (for now).
|
||||
*
|
||||
*
|
||||
* @param inode inode that handle to the tomb node.
|
||||
* @param iParent inode parent.
|
||||
* @return REPEAT if the this methods wasn't successful or OK.
|
||||
*/
|
||||
private Action cleanTomb(INode inode, INode iParent) {
|
||||
CNode updatedCnode = iParent.mainNode().copy();
|
||||
updatedCnode.remove(inode);
|
||||
return iParent.compareAndSet(iParent.mainNode(), updatedCnode) ? Action.OK : Action.REPEAT;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
SubscriptionCounterVisitor visitor = new SubscriptionCounterVisitor();
|
||||
dfsVisit(this.root, visitor, 0);
|
||||
return visitor.getResult();
|
||||
}
|
||||
|
||||
public String dumpTree() {
|
||||
DumpTreeVisitor visitor = new DumpTreeVisitor();
|
||||
dfsVisit(this.root, visitor, 0);
|
||||
return visitor.getResult();
|
||||
}
|
||||
|
||||
private void dfsVisit(INode node, IVisitor<?> visitor, int deep) {
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
visitor.visit(node.mainNode(), deep);
|
||||
++deep;
|
||||
for (INode child : node.mainNode().allChildren()) {
|
||||
dfsVisit(child, visitor, deep);
|
||||
}
|
||||
}
|
||||
}
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.broker.subscriptions;
|
||||
|
||||
import io.moquette.broker.ISubscriptionsRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class CTrieSubscriptionDirectory implements ISubscriptionsDirectory {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CTrieSubscriptionDirectory.class);
|
||||
|
||||
private CTrie ctrie;
|
||||
private volatile ISubscriptionsRepository subscriptionsRepository;
|
||||
|
||||
@Override
|
||||
public void init(ISubscriptionsRepository subscriptionsRepository) {
|
||||
LOG.info("Initializing CTrie");
|
||||
ctrie = new CTrie();
|
||||
|
||||
LOG.info("Initializing subscriptions store...");
|
||||
this.subscriptionsRepository = subscriptionsRepository;
|
||||
// reload any subscriptions persisted
|
||||
if (LOG.isTraceEnabled()) {
|
||||
LOG.trace("Reloading all stored subscriptions. SubscriptionTree = {}", dumpTree());
|
||||
}
|
||||
|
||||
for (Subscription subscription : this.subscriptionsRepository.listAllSubscriptions()) {
|
||||
LOG.debug("Re-subscribing {}", subscription);
|
||||
ctrie.addToTree(subscription);
|
||||
}
|
||||
if (LOG.isTraceEnabled()) {
|
||||
LOG.trace("Stored subscriptions have been reloaded. SubscriptionTree = {}", dumpTree());
|
||||
}
|
||||
}
|
||||
|
||||
Optional<CNode> lookup(Topic topic) {
|
||||
return ctrie.lookup(topic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a topic string return the clients subscriptions that matches it. Topic string can't
|
||||
* contain character # and + because they are reserved to listeners subscriptions, and not topic
|
||||
* publishing.
|
||||
*
|
||||
* @param topic
|
||||
* to use fo searching matching subscriptions.
|
||||
* @return the list of matching subscriptions, or empty if not matching.
|
||||
*/
|
||||
@Override
|
||||
public Set<Subscription> matchWithoutQosSharpening(Topic topic) {
|
||||
return ctrie.recursiveMatch(topic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Subscription> matchQosSharpening(Topic topic) {
|
||||
final Set<Subscription> subscriptions = matchWithoutQosSharpening(topic);
|
||||
|
||||
Map<String, Subscription> subsGroupedByClient = new HashMap<>();
|
||||
for (Subscription sub : subscriptions) {
|
||||
Subscription existingSub = subsGroupedByClient.get(sub.clientId);
|
||||
// update the selected subscriptions if not present or if has a greater qos
|
||||
if (existingSub == null || existingSub.qosLessThan(sub)) {
|
||||
subsGroupedByClient.put(sub.clientId, sub);
|
||||
}
|
||||
}
|
||||
return new HashSet<>(subsGroupedByClient.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(Subscription newSubscription) {
|
||||
ctrie.addToTree(newSubscription);
|
||||
subscriptionsRepository.addNewSubscription(newSubscription);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes subscription from CTrie, adds TNode when the last client unsubscribes, then calls for cleanTomb in a
|
||||
* separate atomic CAS operation.
|
||||
*
|
||||
* @param topic the subscription's topic to remove.
|
||||
* @param clientID the Id of client owning the subscription.
|
||||
*/
|
||||
@Override
|
||||
public void removeSubscription(Topic topic, String clientID) {
|
||||
ctrie.removeFromTree(topic, clientID);
|
||||
this.subscriptionsRepository.removeSubscription(topic.toString(), clientID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return ctrie.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String dumpTree() {
|
||||
return ctrie.dumpTree();
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.broker.subscriptions;
|
||||
|
||||
import io.netty.util.internal.StringUtil;
|
||||
|
||||
class DumpTreeVisitor implements CTrie.IVisitor<String> {
|
||||
|
||||
String s = "";
|
||||
|
||||
@Override
|
||||
public void visit(CNode node, int deep) {
|
||||
String indentTabs = indentTabs(deep);
|
||||
s += indentTabs + (node.token == null ? "''" : node.token.toString()) + prettySubscriptions(node) + "\n";
|
||||
}
|
||||
|
||||
private String prettySubscriptions(CNode node) {
|
||||
if (node instanceof TNode) {
|
||||
return "TNode";
|
||||
}
|
||||
if (node.subscriptions.isEmpty()) {
|
||||
return StringUtil.EMPTY_STRING;
|
||||
}
|
||||
StringBuilder subScriptionsStr = new StringBuilder(" ~~[");
|
||||
int counter = 0;
|
||||
for (Subscription couple : node.subscriptions) {
|
||||
subScriptionsStr
|
||||
.append("{filter=").append(couple.topicFilter).append(", ")
|
||||
.append("qos=").append(couple.getRequestedQos()).append(", ")
|
||||
.append("client='").append(couple.clientId).append("'}");
|
||||
counter++;
|
||||
if (counter < node.subscriptions.size()) {
|
||||
subScriptionsStr.append(";");
|
||||
}
|
||||
}
|
||||
return subScriptionsStr.append("]").toString();
|
||||
}
|
||||
|
||||
private String indentTabs(int deep) {
|
||||
StringBuilder s = new StringBuilder();
|
||||
if (deep > 0) {
|
||||
s.append(" ");
|
||||
for (int i = 0; i < deep - 1; i++) {
|
||||
s.append("| ");
|
||||
}
|
||||
s.append("|-");
|
||||
}
|
||||
return s.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResult() {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.broker.subscriptions;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
class INode {
|
||||
private AtomicReference<CNode> mainNode = new AtomicReference<>();
|
||||
|
||||
INode(CNode mainNode) {
|
||||
this.mainNode.set(mainNode);
|
||||
if (mainNode instanceof TNode) { // this should never happen
|
||||
throw new IllegalStateException("TNode should not be set on mainNnode");
|
||||
}
|
||||
}
|
||||
|
||||
boolean compareAndSet(CNode old, CNode newNode) {
|
||||
return mainNode.compareAndSet(old, newNode);
|
||||
}
|
||||
|
||||
boolean compareAndSet(CNode old, TNode newNode) {
|
||||
return mainNode.compareAndSet(old, newNode);
|
||||
}
|
||||
|
||||
CNode mainNode() {
|
||||
return this.mainNode.get();
|
||||
}
|
||||
|
||||
boolean isTombed() {
|
||||
return this.mainNode() instanceof TNode;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.broker.subscriptions;
|
||||
|
||||
import io.moquette.broker.ISubscriptionsRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public interface ISubscriptionsDirectory {
|
||||
|
||||
void init(ISubscriptionsRepository sessionsRepository);
|
||||
|
||||
Set<Subscription> matchWithoutQosSharpening(Topic topic);
|
||||
|
||||
Set<Subscription> matchQosSharpening(Topic topic);
|
||||
|
||||
void add(Subscription newSubscription);
|
||||
|
||||
void removeSubscription(Topic topic, String clientID);
|
||||
|
||||
int size();
|
||||
|
||||
String dumpTree();
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.broker.subscriptions;
|
||||
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Maintain the information about which Topic a certain ClientID is subscribed and at which QoS
|
||||
*/
|
||||
public final class Subscription implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -3383457629635732794L;
|
||||
private final MqttQoS requestedQos; // max QoS acceptable
|
||||
final String clientId;
|
||||
final Topic topicFilter;
|
||||
|
||||
public Subscription(String clientId, Topic topicFilter, MqttQoS requestedQos) {
|
||||
this.requestedQos = requestedQos;
|
||||
this.clientId = clientId;
|
||||
this.topicFilter = topicFilter;
|
||||
}
|
||||
|
||||
public Subscription(Subscription orig) {
|
||||
this.requestedQos = orig.requestedQos;
|
||||
this.clientId = orig.clientId;
|
||||
this.topicFilter = orig.topicFilter;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public MqttQoS getRequestedQos() {
|
||||
return requestedQos;
|
||||
}
|
||||
|
||||
public Topic getTopicFilter() {
|
||||
return topicFilter;
|
||||
}
|
||||
|
||||
public boolean qosLessThan(Subscription sub) {
|
||||
return requestedQos.value() < sub.requestedQos.value();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
|
||||
Subscription that = (Subscription) o;
|
||||
|
||||
if (clientId != null ? !clientId.equals(that.clientId) : that.clientId != null)
|
||||
return false;
|
||||
return !(topicFilter != null ? !topicFilter.equals(that.topicFilter) : that.topicFilter != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = clientId != null ? clientId.hashCode() : 0;
|
||||
result = 31 * result + (topicFilter != null ? topicFilter.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("[filter:%s, clientID: %s, qos: %s]", topicFilter, clientId, requestedQos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Subscription clone() {
|
||||
try {
|
||||
return (Subscription) super.clone();
|
||||
} catch (CloneNotSupportedException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.broker.subscriptions;
|
||||
|
||||
class TNode extends CNode {
|
||||
|
||||
@Override
|
||||
INode childOf(Token token) {
|
||||
throw new IllegalStateException("Can't be invoked on TNode");
|
||||
}
|
||||
|
||||
@Override
|
||||
CNode copy() {
|
||||
throw new IllegalStateException("Can't be invoked on TNode");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(INode newINode) {
|
||||
throw new IllegalStateException("Can't be invoked on TNode");
|
||||
}
|
||||
|
||||
@Override
|
||||
CNode addSubscription(Subscription newSubscription) {
|
||||
throw new IllegalStateException("Can't be invoked on TNode");
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean containsOnly(String clientId) {
|
||||
throw new IllegalStateException("Can't be invoked on TNode");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String clientId) {
|
||||
throw new IllegalStateException("Can't be invoked on TNode");
|
||||
}
|
||||
|
||||
@Override
|
||||
void removeSubscriptionsFor(String clientId) {
|
||||
throw new IllegalStateException("Can't be invoked on TNode");
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean anyChildrenMatch(Token token) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.connections;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* This interface will be used by an external codebase to retrieve and close physical connections.
|
||||
*/
|
||||
public interface IConnectionsManager {
|
||||
|
||||
/**
|
||||
* Returns the number of physical connections
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
int getActiveConnectionsNo();
|
||||
|
||||
/**
|
||||
* Determines wether a MQTT client is connected to the broker.
|
||||
*
|
||||
* @param clientID
|
||||
* @return
|
||||
*/
|
||||
boolean isConnected(String clientID);
|
||||
|
||||
/**
|
||||
* Returns the identifiers of the MQTT clients that are connected to the broker.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Collection<String> getConnectedClientIds();
|
||||
|
||||
/**
|
||||
* Closes a physical connection.
|
||||
*
|
||||
* @param clientID
|
||||
* @param closeImmediately
|
||||
* If false, the connection will be flushed before it is closed.
|
||||
* @return
|
||||
*/
|
||||
boolean closeConnection(String clientID, boolean closeImmediately);
|
||||
|
||||
/**
|
||||
* Returns the state of the session of a given client.
|
||||
*
|
||||
* @param clientID
|
||||
* @return
|
||||
*/
|
||||
MqttSession getSessionStatus(String clientID);
|
||||
|
||||
/**
|
||||
* Returns the state of all the sessions
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Collection<MqttSession> getSessions();
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.connections;
|
||||
|
||||
/**
|
||||
* A class that represents the metrics of a given MQTT connection.
|
||||
*/
|
||||
public class MqttConnectionMetrics {
|
||||
|
||||
private long readKb;
|
||||
private long writtenKb;
|
||||
private long readMessages;
|
||||
private long writtenMessages;
|
||||
|
||||
public MqttConnectionMetrics(long readBytes, long writtenBytes, long readMessages, long writtenMessages) {
|
||||
this.readKb = readBytes / 1024;
|
||||
this.writtenKb = writtenBytes / 1024;
|
||||
this.readMessages = readMessages;
|
||||
this.writtenMessages = writtenMessages;
|
||||
}
|
||||
|
||||
public long getReadKb() {
|
||||
return readKb;
|
||||
}
|
||||
|
||||
public long getWrittenKb() {
|
||||
return writtenKb;
|
||||
}
|
||||
|
||||
public long getReadMessages() {
|
||||
return readMessages;
|
||||
}
|
||||
|
||||
public long getWrittenMessages() {
|
||||
return writtenMessages;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.connections;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* A class that represents the overall connection status of a MQTT session. Its instances will be
|
||||
* used by an external codebase when the broker is configured in embedded mode.
|
||||
*/
|
||||
public class MqttSession {
|
||||
|
||||
private boolean connectionEstablished;
|
||||
private boolean cleanSession;
|
||||
private int inflightMessages;
|
||||
private int pendingPublishMessagesNo;
|
||||
private int secondPhaseAckPendingMessages;
|
||||
private MqttConnectionMetrics connectionMetrics;
|
||||
|
||||
public boolean isConnectionEstablished() {
|
||||
return connectionEstablished;
|
||||
}
|
||||
|
||||
public void setConnectionEstablished(boolean connectionEstablished) {
|
||||
this.connectionEstablished = connectionEstablished;
|
||||
}
|
||||
|
||||
public boolean isCleanSession() {
|
||||
return cleanSession;
|
||||
}
|
||||
|
||||
public void setCleanSession(boolean cleanSession) {
|
||||
this.cleanSession = cleanSession;
|
||||
}
|
||||
|
||||
public int getPendingPublishMessagesNo() {
|
||||
return pendingPublishMessagesNo;
|
||||
}
|
||||
|
||||
public void setPendingPublishMessagesNo(int pendingPublishMessagesNo) {
|
||||
this.pendingPublishMessagesNo = pendingPublishMessagesNo;
|
||||
}
|
||||
|
||||
public int getInflightMessages() {
|
||||
return inflightMessages;
|
||||
}
|
||||
|
||||
public void setInflightMessages(int inflightMessages) {
|
||||
this.inflightMessages = inflightMessages;
|
||||
}
|
||||
|
||||
public int getSecondPhaseAckPendingMessages() {
|
||||
return secondPhaseAckPendingMessages;
|
||||
}
|
||||
|
||||
public void setSecondPhaseAckPendingMessages(int secondPhaseAckPendingMessages) {
|
||||
this.secondPhaseAckPendingMessages = secondPhaseAckPendingMessages;
|
||||
}
|
||||
|
||||
public MqttConnectionMetrics getConnectionMetrics() {
|
||||
return connectionMetrics;
|
||||
}
|
||||
|
||||
public void setConnectionMetrics(MqttConnectionMetrics connectionMetrics) {
|
||||
this.connectionMetrics = connectionMetrics;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.ProtoConstants;
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import static win.liyufan.im.ErrorCode.ERROR_CODE_SUCCESS;
|
||||
import static win.liyufan.im.IMTopic.HandleFriendRequestTopic;
|
||||
|
||||
@Handler(IMTopic.AddFriendRequestTopic)
|
||||
public class AddFriendHandler extends GroupHandler<WFCMessage.AddFriendRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.AddFriendRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
long[] head = new long[1];
|
||||
ErrorCode errorCode = m_messagesStore.saveAddFriendRequest(fromUser, request, head);
|
||||
if (errorCode == ERROR_CODE_SUCCESS) {
|
||||
WFCMessage.User user = m_messagesStore.getUserInfo(request.getTargetUid());
|
||||
if (user != null && user.getType() == ProtoConstants.UserType.UserType_Normal) {
|
||||
publisher.publishNotification(IMTopic.NotifyFriendRequestTopic, request.getTargetUid(), head[0]);
|
||||
} else if(user != null && user.getType() == ProtoConstants.UserType.UserType_Robot) {
|
||||
WFCMessage.HandleFriendRequest handleFriendRequest = WFCMessage.HandleFriendRequest.newBuilder().setTargetUid(fromUser).setStatus(ProtoConstants.FriendRequestStatus.RequestStatus_Accepted).build();
|
||||
mServer.internalRpcMsg(request.getTargetUid(), null, handleFriendRequest.toByteArray(), 0, fromUser, HandleFriendRequestTopic);
|
||||
}
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import com.xiaoleilu.loServer.pojos.GroupNotificationBinaryContent;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import static win.liyufan.im.ErrorCode.ERROR_CODE_SUCCESS;
|
||||
|
||||
@Handler(value = IMTopic.AddGroupMemberTopic)
|
||||
public class AddGroupMember extends GroupHandler<WFCMessage.AddGroupMemberRequest> {
|
||||
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.AddGroupMemberRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
ErrorCode errorCode = m_messagesStore.addGroupMembers(fromUser, request.getGroupId(), request.getAddedMemberList());
|
||||
if (errorCode == ERROR_CODE_SUCCESS) {
|
||||
if (request.hasNotifyContent() && request.getNotifyContent().getType() > 0) {
|
||||
sendGroupNotification(fromUser, request.getGroupId(), request.getToLineList(), request.getNotifyContent());
|
||||
} else {
|
||||
WFCMessage.MessageContent content = new GroupNotificationBinaryContent(fromUser, null, getMemberIdList(request.getAddedMemberList())).getAddGroupNotifyContent();
|
||||
sendGroupNotification(fromUser, request.getGroupId(), request.getToLineList(), content);
|
||||
}
|
||||
}
|
||||
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import static win.liyufan.im.ErrorCode.ERROR_CODE_SUCCESS;
|
||||
|
||||
@Handler(IMTopic.BlackListUserTopic)
|
||||
public class BlackListRequestHandler extends GroupHandler<WFCMessage.BlackUserRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.BlackUserRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
long[] head = new long[1];
|
||||
ErrorCode errorCode = m_messagesStore.blackUserRequest(fromUser, request.getUid(), request.getStatus(), head);
|
||||
if (errorCode == ERROR_CODE_SUCCESS) {
|
||||
publisher.publishNotification(IMTopic.NotifyFriendTopic, fromUser, head[0]);
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.xiaoleilu.loServer.pojos.GroupNotificationBinaryContent;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import static win.liyufan.im.ErrorCode.ERROR_CODE_SUCCESS;
|
||||
import static win.liyufan.im.IMTopic.PutUserSettingTopic;
|
||||
import static win.liyufan.im.UserSettingScope.kUserSettingListenedChannels;
|
||||
|
||||
@Handler(value = IMTopic.ChannelListenTopic)
|
||||
public class ChannelListenMember extends IMHandler<WFCMessage.ListenChannel> {
|
||||
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.ListenChannel request, Qos1PublishHandler.IMCallback callback) {
|
||||
ErrorCode errorCode = m_messagesStore.listenChannel(fromUser, request.getChannelId(), request.getListen()>0);
|
||||
if (errorCode == ErrorCode.ERROR_CODE_SUCCESS) {
|
||||
WFCMessage.ModifyUserSettingReq modifyUserSettingReq = WFCMessage.ModifyUserSettingReq.newBuilder().setScope(kUserSettingListenedChannels).setKey(request.getChannelId()).setValue(request.getListen() > 0 ? "1" : "0").build();
|
||||
mServer.internalRpcMsg(fromUser, null, modifyUserSettingReq.toByteArray(), 0, fromUser, PutUserSettingTopic);
|
||||
|
||||
publisher.notifyChannelListenStatusChanged(m_messagesStore.getChannelInfo(request.getChannelId()), fromUser, request.getListen() > 0);
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Handler(IMTopic.ChannelSearchTopic)
|
||||
public class ChannelSearchHandler extends IMHandler<WFCMessage.SearchUserRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.SearchUserRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
List<WFCMessage.ChannelInfo> users = m_messagesStore.searchChannel(request.getKeyword(), request.getFuzzy() > 0, request.getPage());
|
||||
WFCMessage.SearchChannelResult.Builder builder = WFCMessage.SearchChannelResult.newBuilder();
|
||||
builder.addAllChannel(users);
|
||||
builder.setKeyword(request.getKeyword());
|
||||
byte[] data = builder.build().toByteArray();
|
||||
ackPayload.ensureWritable(data.length).writeBytes(data);
|
||||
return ErrorCode.ERROR_CODE_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.hazelcast.util.StringUtil;
|
||||
import com.xiaoleilu.loServer.pojos.GroupNotificationBinaryContent;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IDUtils;
|
||||
import win.liyufan.im.IMTopic;
|
||||
import win.liyufan.im.UUIDGenerator;
|
||||
|
||||
import static win.liyufan.im.IMTopic.PutUserSettingTopic;
|
||||
import static win.liyufan.im.UserSettingScope.kUserSettingListenedChannels;
|
||||
import static win.liyufan.im.UserSettingScope.kUserSettingMyChannels;
|
||||
|
||||
@Handler(value = IMTopic.CreateChannelTopic)
|
||||
public class CreateChannelHandler extends GroupHandler<WFCMessage.ChannelInfo> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.ChannelInfo request, Qos1PublishHandler.IMCallback callback) {
|
||||
WFCMessage.ChannelInfo.Builder builder = request.toBuilder();
|
||||
if (StringUtil.isNullOrEmpty(request.getTargetId())) {
|
||||
builder.setTargetId(m_messagesStore.getShortUUID());
|
||||
}
|
||||
if (StringUtil.isNullOrEmpty(request.getOwner())) {
|
||||
builder.setOwner(fromUser);
|
||||
}
|
||||
if (StringUtil.isNullOrEmpty(request.getSecret())) {
|
||||
builder.setSecret(m_messagesStore.getShortUUID());
|
||||
}
|
||||
|
||||
long update = System.currentTimeMillis();
|
||||
request = builder.setUpdateDt(update).build();
|
||||
|
||||
ErrorCode errorCode = m_messagesStore.createChannel(fromUser, request);
|
||||
|
||||
if (errorCode == ErrorCode.ERROR_CODE_SUCCESS) {
|
||||
WFCMessage.ModifyUserSettingReq modifyUserSettingReq = WFCMessage.ModifyUserSettingReq.newBuilder().setScope(kUserSettingMyChannels).setKey(request.getTargetId()).setValue("1").build();
|
||||
mServer.internalRpcMsg(fromUser, null, modifyUserSettingReq.toByteArray(), 0, fromUser, PutUserSettingTopic);
|
||||
byte[] data = request.getTargetId().getBytes();
|
||||
ackPayload.ensureWritable(data.length).writeBytes(data);
|
||||
return ErrorCode.ERROR_CODE_SUCCESS;
|
||||
} else {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import com.xiaoleilu.loServer.pojos.GroupNotificationBinaryContent;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import static io.moquette.BrokerConstants.MESSAGE_CONTENT_TYPE_CREATE_GROUP;
|
||||
|
||||
@Handler(value = IMTopic.CreateGroupTopic)
|
||||
public class CreateGroupHandler extends GroupHandler<WFCMessage.CreateGroupRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.CreateGroupRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
WFCMessage.GroupInfo groupInfo = m_messagesStore.createGroup(fromUser, request.getGroup().getGroupInfo(), request.getGroup().getMembersList());
|
||||
if (groupInfo != null) {
|
||||
if(request.hasNotifyContent() && request.getNotifyContent().getType() > 0) {
|
||||
sendGroupNotification(fromUser, groupInfo.getTargetId(), request.getToLineList(), request.getNotifyContent());
|
||||
} else {
|
||||
WFCMessage.MessageContent content = new GroupNotificationBinaryContent(fromUser, groupInfo.getName(), "").getCreateGroupNotifyContent();
|
||||
sendGroupNotification(fromUser, groupInfo.getTargetId(), request.getToLineList(), content);
|
||||
}
|
||||
}
|
||||
byte[] data = groupInfo.getTargetId().getBytes();
|
||||
ackPayload.ensureWritable(data.length).writeBytes(data);
|
||||
return ErrorCode.ERROR_CODE_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
@Handler(IMTopic.DeleteFriendTopic)
|
||||
public class DeleteFriendHandler extends IMHandler<WFCMessage.IDBuf> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.IDBuf request, Qos1PublishHandler.IMCallback callback) {
|
||||
return m_messagesStore.deleteFriend(fromUser, request.getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.ProtoConstants;
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.xiaoleilu.loServer.pojos.GroupNotificationBinaryContent;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
|
||||
import static win.liyufan.im.IMTopic.DismissGroupTopic;
|
||||
|
||||
@Handler(value = DismissGroupTopic)
|
||||
public class DismissGroupHandler extends GroupHandler<WFCMessage.DismissGroupRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.DismissGroupRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
WFCMessage.GroupInfo groupInfo = m_messagesStore.getGroupInfo(request.getGroupId());
|
||||
ErrorCode errorCode;
|
||||
if (groupInfo == null) {
|
||||
errorCode = m_messagesStore.dismissGroup(fromUser, request.getGroupId());
|
||||
|
||||
} else if ((groupInfo.getType() == ProtoConstants.GroupType.GroupType_Normal || groupInfo.getType() == ProtoConstants.GroupType.GroupType_Restricted)
|
||||
&& groupInfo.getOwner() != null && groupInfo.getOwner().equals(fromUser)) {
|
||||
|
||||
//send notify message first, then dismiss group
|
||||
if (request.hasNotifyContent() && request.getNotifyContent().getType() > 0) {
|
||||
sendGroupNotification(fromUser, groupInfo.getTargetId(), request.getToLineList(), request.getNotifyContent());
|
||||
} else {
|
||||
WFCMessage.MessageContent content = new GroupNotificationBinaryContent(fromUser, null, "").getDismissGroupNotifyContent();
|
||||
sendGroupNotification(fromUser, request.getGroupId(), request.getToLineList(), content);
|
||||
}
|
||||
errorCode = m_messagesStore.dismissGroup(fromUser, request.getGroupId());
|
||||
} else {
|
||||
errorCode = ErrorCode.ERROR_CODE_NOT_RIGHT;
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import static win.liyufan.im.IMTopic.PutUserSettingTopic;
|
||||
import static win.liyufan.im.UserSettingScope.kUserSettingMyChannels;
|
||||
|
||||
@Handler(value = IMTopic.DestoryChannelInfoTopic)
|
||||
public class DistoryChannelHandler extends GroupHandler<WFCMessage.IDBuf> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.IDBuf request, Qos1PublishHandler.IMCallback callback) {
|
||||
ErrorCode errorCode = m_messagesStore.distoryChannel(fromUser, request.getId());
|
||||
if (errorCode == ErrorCode.ERROR_CODE_SUCCESS) {
|
||||
WFCMessage.ModifyUserSettingReq modifyUserSettingReq = WFCMessage.ModifyUserSettingReq.newBuilder().setScope(kUserSettingMyChannels).setKey(request.getId()).setValue("0").build();
|
||||
mServer.internalRpcMsg(fromUser, null, modifyUserSettingReq.toByteArray(), 0, fromUser, PutUserSettingTopic);
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.xiaoleilu.loServer.model.FriendData;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Handler(IMTopic.FriendPullTopic)
|
||||
public class FriendPullHandler extends IMHandler<WFCMessage.Version> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.Version request, Qos1PublishHandler.IMCallback callback) {
|
||||
List<FriendData> friendDatas = m_messagesStore.getFriendList(fromUser, request.getVersion());
|
||||
WFCMessage.GetFriendsResult.Builder builder = WFCMessage.GetFriendsResult.newBuilder();
|
||||
for (FriendData data : friendDatas
|
||||
) {
|
||||
builder.addEntry(WFCMessage.Friend.newBuilder().setState(data.getState()).setUid(data.getFriendUid()).setUpdateDt(data.getTimestamp()).build());
|
||||
}
|
||||
byte[] data = builder.build().toByteArray();
|
||||
ackPayload.ensureWritable(data.length).writeBytes(data);
|
||||
return ErrorCode.ERROR_CODE_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Handler(IMTopic.FriendRequestPullTopic)
|
||||
public class FriendRequestPullHandler extends IMHandler<WFCMessage.Version> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.Version request, Qos1PublishHandler.IMCallback callback) {
|
||||
List<WFCMessage.FriendRequest> friendDatas = m_messagesStore.getFriendRequestList(fromUser, request.getVersion());
|
||||
WFCMessage.GetFriendRequestResult.Builder builder = WFCMessage.GetFriendRequestResult.newBuilder();
|
||||
builder.addAllEntry(friendDatas);
|
||||
byte[] data = builder.build().toByteArray();
|
||||
ackPayload.ensureWritable(data.length).writeBytes(data);
|
||||
return ErrorCode.ERROR_CODE_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
@Handler(IMTopic.ChannelPullTopic)
|
||||
public class GetChannelInfoHandler extends IMHandler<WFCMessage.PullChannelInfo> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.PullChannelInfo request, Qos1PublishHandler.IMCallback callback) {
|
||||
ErrorCode errorCode = ErrorCode.ERROR_CODE_SUCCESS;
|
||||
WFCMessage.ChannelInfo info = m_messagesStore.getChannelInfo(request.getChannelId());
|
||||
if (info == null) {
|
||||
errorCode = ErrorCode.ERROR_CODE_NOT_EXIST;
|
||||
} else if(info.getUpdateDt() <= request.getHead()) {
|
||||
errorCode = ErrorCode.ERROR_CODE_NOT_MODIFIED;
|
||||
} else {
|
||||
if (!info.getOwner().equals(fromUser)) {
|
||||
info = info.toBuilder().clearCallback().clearSecret().build();
|
||||
}
|
||||
|
||||
byte[] data = info.toByteArray();
|
||||
ackPayload.ensureWritable(data.length).writeBytes(data);
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
@Handler(IMTopic.GetChatroomInfoTopic)
|
||||
public class GetChatroomInfoHandler extends IMHandler<WFCMessage.GetChatroomInfoRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.GetChatroomInfoRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
ErrorCode errorCode = ErrorCode.ERROR_CODE_SUCCESS;
|
||||
WFCMessage.ChatroomInfo info = m_messagesStore.getChatroomInfo(request.getChatroomId());
|
||||
if (info == null) {
|
||||
errorCode = ErrorCode.ERROR_CODE_NOT_EXIST;
|
||||
} else if(info.getUpdateDt() <= request.getUpdateDt()) {
|
||||
errorCode = ErrorCode.ERROR_CODE_NOT_MODIFIED;
|
||||
} else {
|
||||
int memberCount = m_messagesStore.getChatroomMemberCount(request.getChatroomId());
|
||||
byte[] data = info.toBuilder().setMemberCount(memberCount).build().toByteArray();
|
||||
ackPayload.ensureWritable(data.length).writeBytes(data);
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
@Handler(IMTopic.GetChatroomMemberTopic)
|
||||
public class GetChatroomMemberHandler extends IMHandler<WFCMessage.GetChatroomMemberInfoRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.GetChatroomMemberInfoRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
ErrorCode errorCode = ErrorCode.ERROR_CODE_SUCCESS;
|
||||
WFCMessage.ChatroomMemberInfo info = m_messagesStore.getChatroomMemberInfo(request.getChatroomId(), request.getMaxCount());
|
||||
if (info != null) {
|
||||
byte[] data = info.toByteArray();
|
||||
ackPayload.ensureWritable(data.length).writeBytes(data);
|
||||
} else {
|
||||
errorCode = ErrorCode.ERROR_CODE_NOT_EXIST;
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Handler(IMTopic.GetGroupInfoTopic)
|
||||
public class GetGroupInfoHandler extends IMHandler<WFCMessage.PullUserRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.PullUserRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
List<WFCMessage.GroupInfo> infos = m_messagesStore.getGroupInfos(request.getRequestList());
|
||||
|
||||
WFCMessage.PullGroupInfoResult result = WFCMessage.PullGroupInfoResult.newBuilder().addAllInfo(infos).build();
|
||||
byte[] data = result.toByteArray();
|
||||
ackPayload.ensureWritable(data.length).writeBytes(data);
|
||||
return ErrorCode.ERROR_CODE_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static win.liyufan.im.ErrorCode.ERROR_CODE_SUCCESS;
|
||||
|
||||
@Handler(IMTopic.GetGroupMemberTopic)
|
||||
public class GetGroupMemberHandler extends IMHandler<WFCMessage.PullGroupMemberRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.PullGroupMemberRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
List<WFCMessage.GroupMember> members = new ArrayList<>();
|
||||
ErrorCode errorCode = m_messagesStore.getGroupMembers(request.getTarget(), request.getHead(), members);
|
||||
|
||||
if (errorCode == ERROR_CODE_SUCCESS) {
|
||||
WFCMessage.PullGroupMemberResult result = WFCMessage.PullGroupMemberResult.newBuilder().addAllMember(members).build();
|
||||
byte[] data = result.toByteArray();
|
||||
ackPayload.ensureWritable(data.length).writeBytes(data);
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.qiniu.util.Auth;
|
||||
import com.xiaoleilu.loServer.action.UploadFileAction;
|
||||
import io.moquette.server.config.MediaServerConfig;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
@Handler(IMTopic.GetQiniuUploadTokenTopic)
|
||||
public class GetQiniuUploadTokenHandler extends IMHandler<Byte> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, Byte request, Qos1PublishHandler.IMCallback callback) {
|
||||
int type = request;
|
||||
String token;
|
||||
|
||||
WFCMessage.GetUploadTokenResult.Builder resultBuilder = WFCMessage.GetUploadTokenResult.newBuilder();
|
||||
if (MediaServerConfig.USER_QINIU) {
|
||||
Auth auth = Auth.create(MediaServerConfig.QINIU_ACCESS_KEY, MediaServerConfig.QINIU_SECRET_KEY);
|
||||
|
||||
//#Media_Type_GENERAL = 0,
|
||||
//#Media_Type_IMAGE = 1,
|
||||
//#Media_Type_VOICE = 2,
|
||||
//#Media_Type_VIDEO = 3,
|
||||
//#Media_Type_FILE = 4,
|
||||
//#Media_Type_PORTRAIT = 5,
|
||||
//#Media_Type_FAVORITE = 6
|
||||
|
||||
String bucketName;
|
||||
String bucketDomain;
|
||||
switch (type) {
|
||||
case 0:
|
||||
bucketName = MediaServerConfig.QINIU_BUCKET_GENERAL_NAME;
|
||||
bucketDomain = MediaServerConfig.QINIU_BUCKET_GENERAL_DOMAIN;
|
||||
break;
|
||||
case 1:
|
||||
bucketName = MediaServerConfig.QINIU_BUCKET_IMAGE_NAME;
|
||||
bucketDomain = MediaServerConfig.QINIU_BUCKET_IMAGE_DOMAIN;
|
||||
break;
|
||||
case 2:
|
||||
bucketName = MediaServerConfig.QINIU_BUCKET_VOICE_NAME;
|
||||
bucketDomain = MediaServerConfig.QINIU_BUCKET_VOICE_DOMAIN;
|
||||
break;
|
||||
case 3:
|
||||
bucketName = MediaServerConfig.QINIU_BUCKET_VIDEO_NAME;
|
||||
bucketDomain = MediaServerConfig.QINIU_BUCKET_VIDEO_DOMAIN;
|
||||
break;
|
||||
case 4:
|
||||
bucketName = MediaServerConfig.QINIU_BUCKET_FILE_NAME;
|
||||
bucketDomain = MediaServerConfig.QINIU_BUCKET_FILE_DOMAIN;
|
||||
break;
|
||||
case 5:
|
||||
bucketName = MediaServerConfig.QINIU_BUCKET_PORTRAIT_NAME;
|
||||
bucketDomain = MediaServerConfig.QINIU_BUCKET_PORTRAIT_DOMAIN;
|
||||
break;
|
||||
case 6:
|
||||
bucketName = MediaServerConfig.QINIU_BUCKET_FAVORITE_NAME;
|
||||
bucketDomain = MediaServerConfig.QINIU_BUCKET_FAVORITE_DOMAIN;
|
||||
break;
|
||||
default:
|
||||
bucketName = MediaServerConfig.QINIU_BUCKET_GENERAL_NAME;
|
||||
bucketDomain = MediaServerConfig.QINIU_BUCKET_GENERAL_DOMAIN;
|
||||
break;
|
||||
}
|
||||
|
||||
token = auth.uploadToken(bucketName);
|
||||
resultBuilder.setDomain(bucketDomain)
|
||||
.setServer(MediaServerConfig.QINIU_SERVER_URL);
|
||||
} else {
|
||||
token = UploadFileAction.getToken(type);
|
||||
resultBuilder.setDomain("http://" + MediaServerConfig.SERVER_IP + ":" + MediaServerConfig.HTTP_SERVER_PORT)
|
||||
.setServer(MediaServerConfig.SERVER_IP);
|
||||
}
|
||||
|
||||
resultBuilder.setToken(token);
|
||||
|
||||
byte[] data = resultBuilder.buildPartial().toByteArray();
|
||||
ackPayload.ensureWritable(data.length).writeBytes(data);
|
||||
return ErrorCode.ERROR_CODE_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.persistence.MemorySessionStore;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.moquette.spi.impl.security.TokenAuthenticator;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
@Handler(IMTopic.GetTokenTopic)
|
||||
public class GetTokenHandler extends IMHandler<WFCMessage.GetTokenRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.GetTokenRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
MemorySessionStore.Session session = m_sessionsStore.createUserSession(fromUser, clientID);
|
||||
TokenAuthenticator authenticator = new TokenAuthenticator();
|
||||
String strToken = authenticator.generateToken(fromUser);
|
||||
String result = strToken + "|" + session.getSecret() + "|" + session.getDbSecret();
|
||||
byte[] data = result.getBytes();
|
||||
ackPayload.ensureWritable(data.length).writeBytes(data);
|
||||
return ErrorCode.ERROR_CODE_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import static win.liyufan.im.ErrorCode.ERROR_CODE_SUCCESS;
|
||||
|
||||
@Handler(IMTopic.GetUserInfoTopic)
|
||||
public class GetUserInfoHandler extends IMHandler<WFCMessage.PullUserRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.PullUserRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
WFCMessage.PullUserResult.Builder resultBuilder = WFCMessage.PullUserResult.newBuilder();
|
||||
|
||||
ErrorCode errorCode = m_messagesStore.getUserInfo(request.getRequestList(), resultBuilder);
|
||||
if (errorCode == ERROR_CODE_SUCCESS) {
|
||||
byte[] data = resultBuilder.build().toByteArray();
|
||||
ackPayload.ensureWritable(data.length).writeBytes(data);
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import static win.liyufan.im.ErrorCode.ERROR_CODE_SUCCESS;
|
||||
|
||||
@Handler(IMTopic.GetUserSettingTopic)
|
||||
public class GetUserSettingHandler extends IMHandler<WFCMessage.Version> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.Version request, Qos1PublishHandler.IMCallback callback) {
|
||||
WFCMessage.GetUserSettingResult.Builder builder = WFCMessage.GetUserSettingResult.newBuilder();
|
||||
ErrorCode errorCode = m_messagesStore.getUserSettings(fromUser, request.getVersion(), builder);
|
||||
if (errorCode == ERROR_CODE_SUCCESS) {
|
||||
byte[] data = builder.build().toByteArray();
|
||||
ackPayload.ensureWritable(data.length).writeBytes(data);
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.ProtoConstants;
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import win.liyufan.im.MessageShardingUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
abstract public class GroupHandler<T> extends IMHandler<T> {
|
||||
protected void sendGroupNotification(String fromUser, String targetId, List<Integer> lines, WFCMessage.MessageContent content) {
|
||||
if (lines != null) {
|
||||
lines = new ArrayList<>();
|
||||
}
|
||||
|
||||
if (lines.isEmpty()) {
|
||||
lines.add(0);
|
||||
}
|
||||
|
||||
for (int line : lines) {
|
||||
WFCMessage.Message.Builder builder = WFCMessage.Message.newBuilder().setContent(content);
|
||||
builder.setConversation(builder.getConversationBuilder().setType(ProtoConstants.ConversationType.ConversationType_Group).setTarget(targetId).setLine(line));
|
||||
long timestamp = System.currentTimeMillis();
|
||||
builder.setFromUser(fromUser);
|
||||
long messageId = MessageShardingUtil.generateId();
|
||||
builder.setMessageId(messageId);
|
||||
saveAndPublish(fromUser, null, builder.build(), timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
protected List<String> getMemberIdList(List<WFCMessage.GroupMember> groupMembers) {
|
||||
List<String> out = new ArrayList<>();
|
||||
if (groupMembers != null) {
|
||||
for (WFCMessage.GroupMember gm : groupMembers
|
||||
) {
|
||||
out.add(gm.getMemberId());
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.ProtoConstants;
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
import win.liyufan.im.MessageShardingUtil;
|
||||
|
||||
import static win.liyufan.im.ErrorCode.ERROR_CODE_SUCCESS;
|
||||
|
||||
@Handler(IMTopic.HandleFriendRequestTopic)
|
||||
public class HandleFriendRequestHandler extends IMHandler<WFCMessage.HandleFriendRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.HandleFriendRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
WFCMessage.Message.Builder builder = WFCMessage.Message.newBuilder();
|
||||
builder.setFromUser(request.getTargetUid());
|
||||
long[] heads = new long[2];
|
||||
ErrorCode errorCode = m_messagesStore.handleFriendRequest(fromUser, request, builder, heads);
|
||||
|
||||
if (errorCode == ERROR_CODE_SUCCESS) {
|
||||
long messageId = MessageShardingUtil.generateId();
|
||||
builder.setMessageId(messageId);
|
||||
saveAndPublish(request.getTargetUid(), null, builder.build(), System.currentTimeMillis());
|
||||
|
||||
WFCMessage.MessageContent.Builder contentBuilder = WFCMessage.MessageContent.newBuilder().setType(90).setContent("以上是打招呼信息");
|
||||
builder = WFCMessage.Message.newBuilder();
|
||||
builder.setFromUser(request.getTargetUid());
|
||||
builder.setConversation(WFCMessage.Conversation.newBuilder().setTarget(fromUser).setLine(0).setType(ProtoConstants.ConversationType.ConversationType_Private).build());
|
||||
builder.setContent(contentBuilder);
|
||||
messageId = MessageShardingUtil.generateId();
|
||||
builder.setMessageId(messageId);
|
||||
saveAndPublish(request.getTargetUid(), null, builder.build(), System.currentTimeMillis());
|
||||
|
||||
contentBuilder.setContent("你们已经成为好友了,现在可以开始聊天了");
|
||||
builder.setContent(contentBuilder);
|
||||
messageId = MessageShardingUtil.generateId();
|
||||
builder.setMessageId(messageId);
|
||||
saveAndPublish(request.getTargetUid(), null, builder.build(), System.currentTimeMillis());
|
||||
|
||||
publisher.publishNotification(IMTopic.NotifyFriendTopic, request.getTargetUid(), heads[0]);
|
||||
publisher.publishNotification(IMTopic.NotifyFriendTopic, fromUser, heads[1]);
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* 注解,用于自定义访问的URL路径<br>
|
||||
* 值可以是一个请求路径,如果需要指定HTTP方法,在前面加方法名用":"分隔既可<br>
|
||||
* @author loolly
|
||||
*
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)/*保留的时间长短*/
|
||||
@Inherited/*只用于class,可被子类继承*/
|
||||
public @interface Handler {
|
||||
String value();
|
||||
}
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import cn.wildfirechat.server.ThreadPoolExecutorWrapper;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.protobuf.GeneratedMessage;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import io.moquette.server.Server;
|
||||
import io.moquette.spi.IMessagesStore;
|
||||
import io.moquette.spi.ISessionsStore;
|
||||
import io.moquette.spi.impl.MessagesPublisher;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.util.internal.StringUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.RateLimiter;
|
||||
import win.liyufan.im.Utility;
|
||||
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.Spliterator;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static win.liyufan.im.ErrorCode.ERROR_CODE_OVER_FREQUENCY;
|
||||
import static win.liyufan.im.ErrorCode.ERROR_CODE_SUCCESS;
|
||||
|
||||
/**
|
||||
* 请求处理接口<br>
|
||||
* 当用户请求某个Topic,则调用相应Handler的handle方法
|
||||
*
|
||||
*/
|
||||
|
||||
abstract public class IMHandler<T> {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(IMHandler.class);
|
||||
protected static IMessagesStore m_messagesStore = null;
|
||||
protected static ISessionsStore m_sessionsStore = null;
|
||||
protected static Server mServer = null;
|
||||
protected static MessagesPublisher publisher;
|
||||
private static ThreadPoolExecutorWrapper m_imBusinessExecutor;
|
||||
private static final RateLimiter mLimitCounter = new RateLimiter(5, 100);
|
||||
private Method parseDataMethod;
|
||||
private Class dataCls;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ActionMethod {
|
||||
}
|
||||
|
||||
protected static String actionName;
|
||||
|
||||
|
||||
public IMHandler() {
|
||||
try {
|
||||
if (StringUtil.isNullOrEmpty(actionName)) {
|
||||
Class cls = getClass();
|
||||
while (cls.getSuperclass() != null) {
|
||||
for (Method method : cls.getSuperclass().getDeclaredMethods()) {
|
||||
if (method.getAnnotation(ActionMethod.class) != null) {
|
||||
actionName = method.getName();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (StringUtil.isNullOrEmpty(actionName)) {
|
||||
cls = cls.getSuperclass();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (Method method : getClass().getDeclaredMethods()
|
||||
) {
|
||||
|
||||
if (method.getName() == actionName && method.getParameterCount() == 5) {
|
||||
dataCls = method.getParameterTypes()[3];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dataCls.getSuperclass().equals(GeneratedMessage.class)) {
|
||||
parseDataMethod = dataCls.getMethod("parseFrom", byte[].class);
|
||||
} else if (dataCls.isPrimitive()) {
|
||||
|
||||
} else {
|
||||
|
||||
}
|
||||
} catch (NoSuchMethodException e) {
|
||||
e.printStackTrace();
|
||||
Utility.printExecption(LOG, e);
|
||||
}
|
||||
}
|
||||
|
||||
private T getDataObject(byte[] bytes) throws IllegalAccessException, InvocationTargetException {
|
||||
if (parseDataMethod != null) {
|
||||
T object = (T) parseDataMethod.invoke(dataCls, bytes);
|
||||
return object;
|
||||
}
|
||||
|
||||
if (dataCls == String.class) {
|
||||
String str = new String(bytes);
|
||||
return (T)str;
|
||||
}
|
||||
|
||||
if (dataCls == Byte.class) {
|
||||
Byte b = bytes[0];
|
||||
return (T)b;
|
||||
}
|
||||
|
||||
if (dataCls == Void.class) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
//目前还没有需求传int的参数,这里先注释掉
|
||||
//在需要使用时,需要注意大小端的问题。
|
||||
//这里使用的示例代码是小端的,注意一定要与协议中保持一直!!!
|
||||
// if (dataCls == Integer.class) {
|
||||
// int i = bytes[0];
|
||||
// for(int index = 0; index <8; index++) {
|
||||
// i <<= 1;
|
||||
// i += bytes[index];
|
||||
// }
|
||||
// Integer object = i;
|
||||
// return (T)object;
|
||||
// }
|
||||
|
||||
//json ?
|
||||
return (T)(new Gson().fromJson(new String(bytes), dataCls));
|
||||
}
|
||||
|
||||
public static void init(IMessagesStore ms, ISessionsStore ss, MessagesPublisher p, ThreadPoolExecutorWrapper businessExecutor, Server server) {
|
||||
m_messagesStore = ms;
|
||||
m_sessionsStore = ss;
|
||||
publisher = p;
|
||||
m_imBusinessExecutor = businessExecutor;
|
||||
mServer = server;
|
||||
}
|
||||
|
||||
|
||||
public ErrorCode preAction(String clientID, String fromUser, String topic, Qos1PublishHandler.IMCallback callback) {
|
||||
LOG.info("imHandler fromUser={}, clientId={}, topic={}", fromUser, clientID, 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);
|
||||
}
|
||||
return ErrorCode.ERROR_CODE_OVER_FREQUENCY;
|
||||
}
|
||||
return ErrorCode.ERROR_CODE_SUCCESS;
|
||||
}
|
||||
|
||||
public void doHandler(String clientID, String fromUser, String topic, byte[] payloadContent, Qos1PublishHandler.IMCallback callback) {
|
||||
m_imBusinessExecutor.execute(() -> {
|
||||
Qos1PublishHandler.IMCallback callbackWrapper = new Qos1PublishHandler.IMCallback() {
|
||||
@Override
|
||||
public void onIMHandled(ErrorCode errorCode, ByteBuf ackPayload) {
|
||||
LOG.debug("execute handler {} with result {}", this.getClass().getName(), errorCode);
|
||||
callback.onIMHandled(errorCode, ackPayload);
|
||||
afterAction(clientID, fromUser, topic, callback);
|
||||
}
|
||||
};
|
||||
|
||||
ErrorCode preActionCode = preAction(clientID, fromUser, topic, callbackWrapper);
|
||||
|
||||
if (preActionCode == ErrorCode.ERROR_CODE_SUCCESS) {
|
||||
ByteBuf ackPayload = Unpooled.buffer(1);
|
||||
ErrorCode errorCode = ERROR_CODE_SUCCESS;
|
||||
ackPayload.ensureWritable(1).writeByte(errorCode.getCode());
|
||||
|
||||
try {
|
||||
LOG.debug("execute handler for topic {}", topic);
|
||||
errorCode = action(ackPayload, clientID, fromUser, getDataObject(payloadContent), callbackWrapper);
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
Utility.printExecption(LOG, e);
|
||||
errorCode = ErrorCode.ERROR_CODE_INVALID_DATA;
|
||||
} catch (InvocationTargetException e) {
|
||||
e.printStackTrace();
|
||||
Utility.printExecption(LOG, e);
|
||||
errorCode = ErrorCode.ERROR_CODE_INVALID_DATA;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Utility.printExecption(LOG, e);
|
||||
if (e instanceof InvalidProtocolBufferException) {
|
||||
errorCode = ErrorCode.ERROR_CODE_INVALID_DATA;
|
||||
} else {
|
||||
errorCode = ErrorCode.ERROR_CODE_SERVER_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
if(errorCode != ErrorCode.ERROR_CODE_ASYNC_HANDLER) {
|
||||
response(ackPayload, errorCode, callback);
|
||||
}
|
||||
} else {
|
||||
LOG.error("Handler {} preAction failure", this.getClass().getName());
|
||||
ByteBuf ackPayload = Unpooled.buffer(1);
|
||||
ackPayload.ensureWritable(1).writeByte(preActionCode.getCode());
|
||||
response(ackPayload, preActionCode, callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void response(ByteBuf ackPayload, ErrorCode errorCode, Qos1PublishHandler.IMCallback callback) {
|
||||
ackPayload.setByte(0, errorCode.getCode());
|
||||
try {
|
||||
callback.onIMHandled(errorCode, ackPayload);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Utility.printExecption(LOG, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ActionMethod
|
||||
abstract public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, T request, Qos1PublishHandler.IMCallback callback) ;
|
||||
|
||||
public void afterAction(String clientID, String fromUser, String topic, Qos1PublishHandler.IMCallback callback) {
|
||||
|
||||
}
|
||||
|
||||
protected long saveAndPublish(String username, String clientID, WFCMessage.Message message, long timestamp) {
|
||||
Set<String> notifyReceivers = new LinkedHashSet<>();
|
||||
|
||||
message = m_messagesStore.storeMessage(username, clientID, message, timestamp);
|
||||
int pullType = m_messagesStore.getNotifyReceivers(username, message, notifyReceivers);
|
||||
this.publisher.publish2Receivers(message, notifyReceivers, clientID, pullType);
|
||||
return message.getMessageId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
|
||||
@Handler(IMTopic.JoinChatroomTopic)
|
||||
public class JoinChatroomHandler extends IMHandler<WFCMessage.IDBuf> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.IDBuf request, Qos1PublishHandler.IMCallback callback) {
|
||||
return m_messagesStore.handleJoinChatroom(fromUser, clientID, request.getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.ProtoConstants;
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.xiaoleilu.loServer.pojos.GroupNotificationBinaryContent;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import static win.liyufan.im.IMTopic.KickoffGroupMemberTopic;
|
||||
|
||||
@Handler(value = KickoffGroupMemberTopic)
|
||||
public class KickoffGroupMember extends GroupHandler<WFCMessage.RemoveGroupMemberRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.RemoveGroupMemberRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
ErrorCode errorCode;
|
||||
WFCMessage.GroupInfo groupInfo = m_messagesStore.getGroupInfo(request.getGroupId());
|
||||
if (groupInfo == null) {
|
||||
errorCode = ErrorCode.ERROR_CODE_NOT_EXIST;
|
||||
|
||||
} else if ((groupInfo.getType() == ProtoConstants.GroupType.GroupType_Normal || groupInfo.getType() == ProtoConstants.GroupType.GroupType_Restricted)
|
||||
&& groupInfo.getOwner() != null && groupInfo.getOwner().equals(fromUser)) {
|
||||
|
||||
//send notify message first, then kickoff the member
|
||||
if (request.hasNotifyContent() && request.getNotifyContent().getType() > 0) {
|
||||
sendGroupNotification(fromUser, groupInfo.getTargetId(), request.getToLineList(), request.getNotifyContent());
|
||||
} else {
|
||||
WFCMessage.MessageContent content = new GroupNotificationBinaryContent(fromUser, null, request.getRemovedMemberList()).getKickokfMemberGroupNotifyContent();
|
||||
sendGroupNotification(fromUser, request.getGroupId(), request.getToLineList(), content);
|
||||
}
|
||||
errorCode = m_messagesStore.kickoffGroupMembers(fromUser, request.getGroupId(), request.getRemovedMemberList());
|
||||
} else {
|
||||
errorCode = ErrorCode.ERROR_CODE_NOT_RIGHT;
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.xiaoleilu.loServer.pojos.GroupNotificationBinaryContent;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
|
||||
import static cn.wildfirechat.proto.ProtoConstants.ModifyGroupInfoType.Modify_Group_Name;
|
||||
import static cn.wildfirechat.proto.ProtoConstants.ModifyGroupInfoType.Modify_Group_Portrait;
|
||||
import static win.liyufan.im.ErrorCode.ERROR_CODE_SUCCESS;
|
||||
import static win.liyufan.im.IMTopic.ModifyChannelInfoTopic;
|
||||
import static win.liyufan.im.IMTopic.ModifyGroupInfoTopic;
|
||||
|
||||
@Handler(value = ModifyChannelInfoTopic)
|
||||
public class ModifyChannelInfoHandler extends GroupHandler<WFCMessage.ModifyChannelInfo> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.ModifyChannelInfo request, Qos1PublishHandler.IMCallback callback) {
|
||||
ErrorCode errorCode= m_messagesStore.modifyChannelInfo(fromUser, request.getChannelId(), request.getType(), request.getValue());
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.xiaoleilu.loServer.pojos.GroupNotificationBinaryContent;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import static win.liyufan.im.ErrorCode.ERROR_CODE_SUCCESS;
|
||||
|
||||
@Handler(IMTopic.ModifyGroupAliasTopic)
|
||||
public class ModifyGroupAliasHandler extends GroupHandler<WFCMessage.ModifyGroupMemberAlias> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.ModifyGroupMemberAlias request, Qos1PublishHandler.IMCallback callback) {
|
||||
ErrorCode errorCode = m_messagesStore.modifyGroupAlias(fromUser, request.getGroupId(), request.getAlias());
|
||||
if (errorCode == ERROR_CODE_SUCCESS && request.hasNotifyContent()) {
|
||||
if (request.hasNotifyContent()&& request.getNotifyContent().getType() > 0) {
|
||||
sendGroupNotification(fromUser, request.getGroupId(), request.getToLineList(), request.getNotifyContent());
|
||||
} else {
|
||||
WFCMessage.MessageContent content = new GroupNotificationBinaryContent(fromUser, request.getAlias(), "").getModifyGroupMemberAliasNotifyContent();
|
||||
sendGroupNotification(fromUser, request.getGroupId(), request.getToLineList(), content);
|
||||
}
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.xiaoleilu.loServer.pojos.GroupNotificationBinaryContent;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
|
||||
import static cn.wildfirechat.proto.ProtoConstants.ModifyGroupInfoType.Modify_Group_Name;
|
||||
import static cn.wildfirechat.proto.ProtoConstants.ModifyGroupInfoType.Modify_Group_Portrait;
|
||||
import static win.liyufan.im.ErrorCode.ERROR_CODE_SUCCESS;
|
||||
import static win.liyufan.im.IMTopic.ModifyGroupInfoTopic;
|
||||
|
||||
@Handler(value = ModifyGroupInfoTopic)
|
||||
public class ModifyGroupInfoHandler extends GroupHandler<WFCMessage.ModifyGroupInfoRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.ModifyGroupInfoRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
ErrorCode errorCode= m_messagesStore.modifyGroupInfo(fromUser, request.getGroupId(), request.getType(), request.getValue());
|
||||
if (errorCode == ERROR_CODE_SUCCESS) {
|
||||
if(request.hasNotifyContent() && request.getNotifyContent().getType() > 0) {
|
||||
sendGroupNotification(fromUser, request.getGroupId(), request.getToLineList(), request.getNotifyContent());
|
||||
} else {
|
||||
WFCMessage.MessageContent content = null;
|
||||
if (request.getType() == Modify_Group_Name) {
|
||||
content = new GroupNotificationBinaryContent(fromUser, request.getValue(), "").getChangeGroupNameNotifyContent();
|
||||
} else if(request.getType() == Modify_Group_Portrait) {
|
||||
content = new GroupNotificationBinaryContent(fromUser, null, "").getChangeGroupPortraitNotifyContent();
|
||||
}
|
||||
|
||||
if (content != null) {
|
||||
sendGroupNotification(fromUser, request.getGroupId(), request.getToLineList(), content);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
@Handler(IMTopic.ModifyMyInfoTopic)
|
||||
public class ModifyMyInfoHandler extends IMHandler<WFCMessage.ModifyMyInfoRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.ModifyMyInfoRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
return m_messagesStore.modifyUserInfo(fromUser, request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.ProtoConstants;
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
@Handler(value = IMTopic.PullMessageTopic)
|
||||
public class PullMessageHandler extends IMHandler<WFCMessage.PullMessageRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.PullMessageRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
ErrorCode errorCode = ErrorCode.ERROR_CODE_SUCCESS;
|
||||
|
||||
if (request.getType() == ProtoConstants.PullType.Pull_ChatRoom && !m_messagesStore.checkUserClientInChatroom(fromUser, clientID, null)) {
|
||||
errorCode = ErrorCode.ERROR_CODE_NOT_IN_CHATROOM;
|
||||
} else {
|
||||
WFCMessage.PullMessageResult result = m_messagesStore.fetchMessage(fromUser, clientID, request.getId(), request.getType());
|
||||
byte[] data = result.toByteArray();
|
||||
LOG.info("User {} pull message with count({}), payload size({})", fromUser, result.getMessageCount(), data.length);
|
||||
ackPayload.ensureWritable(data.length).writeBytes(data);
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
@Handler(IMTopic.PutUserSettingTopic)
|
||||
public class PutUserSettingHandler extends IMHandler<WFCMessage.ModifyUserSettingReq> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.ModifyUserSettingReq request, Qos1PublishHandler.IMCallback callback) {
|
||||
long timestamp = m_messagesStore.updateUserSettings(fromUser, request);
|
||||
publisher.publishNotification(IMTopic.NotifyUserSettingTopic, fromUser, timestamp);
|
||||
return ErrorCode.ERROR_CODE_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
@Handler(IMTopic.QuitChatroomTopic)
|
||||
public class QuitChatroomHandler extends IMHandler<WFCMessage.IDBuf> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.IDBuf request, Qos1PublishHandler.IMCallback callback) {
|
||||
m_messagesStore.handleQuitChatroom(fromUser, clientID, request.getId());
|
||||
return ErrorCode.ERROR_CODE_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.xiaoleilu.loServer.pojos.GroupNotificationBinaryContent;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
|
||||
import static win.liyufan.im.IMTopic.QuitGroupTopic;
|
||||
|
||||
@Handler(value = QuitGroupTopic)
|
||||
public class QuitGroupHandler extends GroupHandler<WFCMessage.QuitGroupRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.QuitGroupRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
ErrorCode errorCode = ErrorCode.ERROR_CODE_SUCCESS;
|
||||
WFCMessage.GroupInfo groupInfo = m_messagesStore.getGroupInfo(request.getGroupId());
|
||||
if (groupInfo == null) {
|
||||
errorCode = m_messagesStore.quitGroup(fromUser, request.getGroupId());
|
||||
} else {
|
||||
//send notify message first, then quit group
|
||||
if (request.hasNotifyContent() && request.getNotifyContent().getType() > 0) {
|
||||
sendGroupNotification(fromUser, groupInfo.getTargetId(), request.getToLineList(), request.getNotifyContent());
|
||||
} else {
|
||||
WFCMessage.MessageContent content = new GroupNotificationBinaryContent(fromUser, null, "").getQuitGroupNotifyContent();
|
||||
sendGroupNotification(fromUser, request.getGroupId(), request.getToLineList(), content);
|
||||
}
|
||||
errorCode = m_messagesStore.quitGroup(fromUser, request.getGroupId());
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Handler(value = IMTopic.RecallMessageTopic)
|
||||
public class RecallMessageHandler extends IMHandler<WFCMessage.INT64Buf> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.INT64Buf int64Buf, Qos1PublishHandler.IMCallback callback) {
|
||||
ErrorCode errorCode = m_messagesStore.recallMessage(int64Buf.getId(), fromUser);
|
||||
|
||||
if(errorCode != ErrorCode.ERROR_CODE_SUCCESS) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
Set<String> notifyReceivers = new LinkedHashSet<>();
|
||||
WFCMessage.Message message = m_messagesStore.getMessage(int64Buf.getId());
|
||||
if (message == null) {
|
||||
return ErrorCode.ERROR_CODE_NOT_EXIST;
|
||||
}
|
||||
|
||||
m_messagesStore.getNotifyReceivers(fromUser, message, notifyReceivers);
|
||||
this.publisher.publishRecall2Receivers(int64Buf.getId(), fromUser, notifyReceivers, clientID);
|
||||
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.hazelcast.core.Member;
|
||||
import io.moquette.spi.ClientSession;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import static io.moquette.BrokerConstants.HZ_Cluster_Node_External_IP;
|
||||
import static io.moquette.BrokerConstants.HZ_Cluster_Node_External_Long_Port;
|
||||
import static io.moquette.BrokerConstants.HZ_Cluster_Node_External_Short_Port;
|
||||
|
||||
@Handler(IMTopic.RouteTopic)
|
||||
public class RouteHandler extends IMHandler<WFCMessage.RouteRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.RouteRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
Member member = mServer.getHazelcastInstance().getCluster().getLocalMember();
|
||||
String serverIp = member.getStringAttribute(HZ_Cluster_Node_External_IP);
|
||||
String longPort = member.getStringAttribute(HZ_Cluster_Node_External_Long_Port);
|
||||
String shortPort = member.getStringAttribute(HZ_Cluster_Node_External_Short_Port);
|
||||
|
||||
ClientSession clientSession = m_sessionsStore.sessionForClient(clientID);
|
||||
boolean isSessionAlreadyStored = clientSession != null;
|
||||
if (!isSessionAlreadyStored) {
|
||||
m_sessionsStore.createNewSession(fromUser, clientID, true);
|
||||
} else {
|
||||
m_sessionsStore.updateExistSession(fromUser, clientID, request, true);
|
||||
}
|
||||
|
||||
WFCMessage.RouteResponse response = WFCMessage.RouteResponse.newBuilder().setHost(serverIp).setLongPort(Integer.parseInt(longPort)).setShortPort(Integer.parseInt(shortPort)).build();
|
||||
|
||||
byte[] data = response.toByteArray();
|
||||
ackPayload.ensureWritable(data.length).writeBytes(data);
|
||||
return ErrorCode.ERROR_CODE_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.ProtoConstants;
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.hazelcast.util.StringUtil;
|
||||
import io.moquette.BrokerConstants;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
import win.liyufan.im.MessageShardingUtil;
|
||||
import win.liyufan.im.SensitiveFilter;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.wildfirechat.proto.ProtoConstants.ContentType.Text;
|
||||
|
||||
@Handler(value = IMTopic.SendMessageTopic)
|
||||
public class SendMessageHandler extends IMHandler<WFCMessage.Message> {
|
||||
private int mSensitiveType = 0; //命中敏感词时,0 失败,1 吞掉, 2 敏感词替换成*。
|
||||
private String mForwardUrl = null;
|
||||
public SendMessageHandler() {
|
||||
super();
|
||||
|
||||
String forwardUrl = mServer.getConfig().getProperty(BrokerConstants.MESSAGE_Forward_Url);
|
||||
if (!StringUtil.isNullOrEmpty(forwardUrl)) {
|
||||
mForwardUrl = forwardUrl;
|
||||
}
|
||||
|
||||
try {
|
||||
mSensitiveType = Integer.parseInt(mServer.getConfig().getProperty(BrokerConstants.SENSITIVE_Filter_Type));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.Message message, Qos1PublishHandler.IMCallback callback) {
|
||||
ErrorCode errorCode = ErrorCode.ERROR_CODE_SUCCESS;
|
||||
if (message != null) {
|
||||
int userStatus = m_messagesStore.getUserStatus(fromUser);
|
||||
if (userStatus == 1 || userStatus == 2) {
|
||||
return ErrorCode.ERROR_CODE_FORBIDDEN_SEND_MSG;
|
||||
}
|
||||
|
||||
if (message.getConversation().getType() == ProtoConstants.ConversationType.ConversationType_Private) {
|
||||
if (m_messagesStore.isBlacked(message.getConversation().getTarget(), fromUser)) {
|
||||
return ErrorCode.ERROR_CODE_IN_BLACK_LIST;
|
||||
}
|
||||
}
|
||||
|
||||
long timestamp = System.currentTimeMillis();
|
||||
if (message.getConversation().getType() == ProtoConstants.ConversationType.ConversationType_Group &&
|
||||
!m_messagesStore.isMemberInGroup(fromUser, message.getConversation().getTarget())) {
|
||||
errorCode = ErrorCode.ERROR_CODE_NOT_IN_GROUP;
|
||||
} else if (message.getConversation().getType() == ProtoConstants.ConversationType.ConversationType_Group &&
|
||||
m_messagesStore.isForbiddenInGroup(fromUser, message.getConversation().getTarget())) {
|
||||
errorCode = ErrorCode.ERROR_CODE_NOT_RIGHT;
|
||||
} else if (message.getConversation().getType() == ProtoConstants.ConversationType.ConversationType_ChatRoom &&
|
||||
!m_messagesStore.checkUserClientInChatroom(fromUser, clientID, message.getConversation().getTarget())) {
|
||||
errorCode = ErrorCode.ERROR_CODE_NOT_IN_CHATROOM;
|
||||
} else if (message.getConversation().getType() == ProtoConstants.ConversationType.ConversationType_Channel &&
|
||||
!m_messagesStore.checkUserInChannel(fromUser, message.getConversation().getTarget())) {
|
||||
errorCode = ErrorCode.ERROR_CODE_NOT_IN_CHANNEL;
|
||||
} else {
|
||||
long messageId = MessageShardingUtil.generateId();
|
||||
message = message.toBuilder().setFromUser(fromUser).setMessageId(messageId).setServerTimestamp(timestamp).build();
|
||||
|
||||
if (mForwardUrl != null) {
|
||||
publisher.forwardMessage(message, mForwardUrl);
|
||||
}
|
||||
|
||||
boolean ignoreMsg = false;
|
||||
if (message.getContent().getType() == Text) {
|
||||
Set<String> matched = m_messagesStore.handleSensitiveWord(message.getContent().getSearchableContent());
|
||||
if (matched != null && !matched.isEmpty()) {
|
||||
if (mSensitiveType == 0) {
|
||||
errorCode = ErrorCode.ERROR_CODE_SENSITIVE_MATCHED;
|
||||
} else if(mSensitiveType == 1) {
|
||||
ignoreMsg = true;
|
||||
} else {
|
||||
String text = message.getContent().getSearchableContent();
|
||||
for (String word : matched) {
|
||||
text = text.replace(word, "***");
|
||||
}
|
||||
|
||||
message = message.toBuilder().setContent(message.getContent().toBuilder().setSearchableContent(text).build()).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errorCode == ErrorCode.ERROR_CODE_SUCCESS) {
|
||||
if (!ignoreMsg) {
|
||||
saveAndPublish(fromUser, clientID, message, timestamp);
|
||||
}
|
||||
ackPayload = ackPayload.capacity(20);
|
||||
ackPayload.writeLong(messageId);
|
||||
ackPayload.writeLong(timestamp);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errorCode = ErrorCode.ERROR_CODE_INVALID_MESSAGE;
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import static win.liyufan.im.ErrorCode.ERROR_CODE_SUCCESS;
|
||||
|
||||
@Handler(IMTopic.RriendRequestUnreadSyncTopic)
|
||||
public class SyncFriendRequestUnreadHandler extends GroupHandler<WFCMessage.Version> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.Version request, Qos1PublishHandler.IMCallback callback) {
|
||||
long[] head = new long[1];
|
||||
ErrorCode errorCode = m_messagesStore.SyncFriendRequestUnread(fromUser, request.getVersion(), head);
|
||||
if (errorCode == ERROR_CODE_SUCCESS) {
|
||||
publisher.publishNotification(IMTopic.NotifyFriendRequestTopic, fromUser, head[0]);
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.xiaoleilu.loServer.pojos.GroupNotificationBinaryContent;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import static win.liyufan.im.ErrorCode.ERROR_CODE_SUCCESS;
|
||||
|
||||
@Handler(IMTopic.TransferChannelInfoTopic)
|
||||
public class TransferChannelHandler extends GroupHandler<WFCMessage.TransferChannel> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.TransferChannel request, Qos1PublishHandler.IMCallback callback) {
|
||||
ErrorCode errorCode = m_messagesStore.transferChannel(fromUser, request.getChannelId(), request.getNewOwner());
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.xiaoleilu.loServer.pojos.GroupNotificationBinaryContent;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import static win.liyufan.im.ErrorCode.ERROR_CODE_SUCCESS;
|
||||
|
||||
@Handler(IMTopic.TransferGroupTopic)
|
||||
public class TransferGroupHandler extends GroupHandler<WFCMessage.TransferGroupRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.TransferGroupRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
ErrorCode errorCode = m_messagesStore.transferGroup(fromUser, request.getGroupId(), request.getNewOwner());
|
||||
if (errorCode == ERROR_CODE_SUCCESS) {
|
||||
if (request.hasNotifyContent() && request.getNotifyContent().getType() > 0) {
|
||||
sendGroupNotification(fromUser, request.getGroupId(), request.getToLineList(), request.getNotifyContent());
|
||||
} else {
|
||||
WFCMessage.MessageContent content = new GroupNotificationBinaryContent(fromUser, null, request.getNewOwner()).getTransferGroupNotifyContent();
|
||||
sendGroupNotification(fromUser, request.getGroupId(), request.getToLineList(), content);
|
||||
}
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.ProtoConstants;
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.persistence.MemorySessionStore;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
|
||||
@Handler(IMTopic.UploadDeviceTokenTopic)
|
||||
public class UploadDeviceTokenHandler extends IMHandler<WFCMessage.UploadDeviceTokenRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.UploadDeviceTokenRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
MemorySessionStore.Session session = m_sessionsStore.getSession(clientID);
|
||||
session.setPlatform(request.getPlatform());
|
||||
session.setAppName(request.getAppName());
|
||||
if (request.getPlatform() == ProtoConstants.Platform.Platform_iOS && request.getPushType() == 2) {
|
||||
session.setVoipDeviceToken(request.getDeviceToken());
|
||||
session.setPushType(0);//如果有voip token,肯定是生产环境的推送
|
||||
m_sessionsStore.updateSessionToken(session, true);
|
||||
} else {
|
||||
session.setDeviceToken(request.getDeviceToken());
|
||||
session.setPushType(request.getPushType());
|
||||
m_sessionsStore.updateSessionToken(session, false);
|
||||
}
|
||||
|
||||
return ErrorCode.ERROR_CODE_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.imhandler;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.spi.impl.Qos1PublishHandler;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.IMTopic;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Handler(IMTopic.UserSearchTopic)
|
||||
public class UserSearchHandler extends IMHandler<WFCMessage.SearchUserRequest> {
|
||||
@Override
|
||||
public ErrorCode action(ByteBuf ackPayload, String clientID, String fromUser, WFCMessage.SearchUserRequest request, Qos1PublishHandler.IMCallback callback) {
|
||||
List<WFCMessage.User> users = m_messagesStore.searchUser(request.getKeyword(), request.getFuzzy() > 0, request.getPage());
|
||||
WFCMessage.SearchUserResult.Builder builder = WFCMessage.SearchUserResult.newBuilder();
|
||||
builder.addAllEntry(users);
|
||||
byte[] data = builder.build().toByteArray();
|
||||
ackPayload.ensureWritable(data.length).writeBytes(data);
|
||||
return ErrorCode.ERROR_CODE_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.interception;
|
||||
|
||||
import io.moquette.interception.messages.InterceptAcknowledgedMessage;
|
||||
import io.moquette.interception.messages.InterceptConnectMessage;
|
||||
import io.moquette.interception.messages.InterceptConnectionLostMessage;
|
||||
import io.moquette.interception.messages.InterceptDisconnectMessage;
|
||||
import io.moquette.interception.messages.InterceptPublishMessage;
|
||||
import io.moquette.interception.messages.InterceptSubscribeMessage;
|
||||
import io.moquette.interception.messages.InterceptUnsubscribeMessage;
|
||||
|
||||
/**
|
||||
* Basic abstract class usefull to avoid empty methods creation in subclasses.
|
||||
*/
|
||||
public abstract class AbstractInterceptHandler implements InterceptHandler {
|
||||
|
||||
@Override
|
||||
public Class<?>[] getInterceptedMessageTypes() {
|
||||
return InterceptHandler.ALL_MESSAGE_TYPES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnect(InterceptConnectMessage msg) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect(InterceptDisconnectMessage msg) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionLost(InterceptConnectionLostMessage msg) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPublish(InterceptPublishMessage msg) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubscribe(InterceptSubscribeMessage msg) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnsubscribe(InterceptUnsubscribeMessage msg) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageAcknowledged(InterceptAcknowledgedMessage msg) {
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* 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
|
||||
@@ -17,7 +17,6 @@
|
||||
package io.moquette.interception;
|
||||
|
||||
import io.moquette.interception.messages.*;
|
||||
import io.moquette.broker.subscriptions.Subscription;
|
||||
import io.netty.handler.codec.mqtt.MqttMessage;
|
||||
|
||||
/**
|
||||
@@ -32,17 +31,20 @@ import io.netty.handler.codec.mqtt.MqttMessage;
|
||||
public interface InterceptHandler {
|
||||
|
||||
Class<?>[] ALL_MESSAGE_TYPES = {InterceptConnectMessage.class, InterceptDisconnectMessage.class,
|
||||
InterceptConnectionLostMessage.class, InterceptPublishMessage.class, InterceptSubscribeMessage.class,
|
||||
InterceptUnsubscribeMessage.class, InterceptAcknowledgedMessage.class};
|
||||
InterceptConnectionLostMessage.class, InterceptPublishMessage.class, InterceptAcknowledgedMessage.class};
|
||||
|
||||
/**
|
||||
* @return the identifier of this intercept handler.
|
||||
* Returns the identifier of this intercept handler.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String getID();
|
||||
|
||||
/**
|
||||
* @return the InterceptMessage subtypes that this handler can process. If the result is null or
|
||||
* Returns the InterceptMessage subtypes that this handler can process. If the result is null or
|
||||
* equal to ALL_MESSAGE_TYPES, all the message types will be processed.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Class<?>[] getInterceptedMessageTypes();
|
||||
|
||||
@@ -54,9 +56,5 @@ public interface InterceptHandler {
|
||||
|
||||
void onPublish(InterceptPublishMessage msg);
|
||||
|
||||
void onSubscribe(InterceptSubscribeMessage msg);
|
||||
|
||||
void onUnsubscribe(InterceptUnsubscribeMessage msg);
|
||||
|
||||
void onMessageAcknowledged(InterceptAcknowledgedMessage msg);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* 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
|
||||
@@ -17,7 +17,6 @@
|
||||
package io.moquette.interception;
|
||||
|
||||
import io.moquette.interception.messages.InterceptAcknowledgedMessage;
|
||||
import io.moquette.broker.subscriptions.Subscription;
|
||||
import io.netty.handler.codec.mqtt.MqttConnectMessage;
|
||||
import io.netty.handler.codec.mqtt.MqttPublishMessage;
|
||||
|
||||
@@ -39,12 +38,6 @@ public interface Interceptor {
|
||||
|
||||
void notifyClientConnectionLost(String clientID, String username);
|
||||
|
||||
void notifyTopicPublished(MqttPublishMessage msg, String clientID, String username);
|
||||
|
||||
void notifyTopicSubscribed(Subscription sub, String username);
|
||||
|
||||
void notifyTopicUnsubscribed(String topic, String clientID, String username);
|
||||
|
||||
void notifyMessageAcknowledged(InterceptAcknowledgedMessage msg);
|
||||
|
||||
void addInterceptHandler(InterceptHandler interceptHandler);
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* 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
|
||||
|
||||
+2
-60
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* 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
|
||||
@@ -16,68 +16,10 @@
|
||||
|
||||
package io.moquette.interception.messages;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
|
||||
import java.io.Serializable;
|
||||
import static io.moquette.spi.IMessagesStore.StoredMessage;
|
||||
|
||||
public class InterceptAcknowledgedMessage implements InterceptMessage {
|
||||
|
||||
class StoredMessage implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1755296138639817304L;
|
||||
private MqttQoS m_qos;
|
||||
final byte[] m_payload;
|
||||
final String m_topic;
|
||||
private boolean m_retained;
|
||||
private String m_clientID;
|
||||
|
||||
public StoredMessage(byte[] message, MqttQoS qos, String topic) {
|
||||
m_qos = qos;
|
||||
m_payload = message;
|
||||
m_topic = topic;
|
||||
}
|
||||
|
||||
public void setQos(MqttQoS qos) {
|
||||
this.m_qos = qos;
|
||||
}
|
||||
|
||||
public MqttQoS getQos() {
|
||||
return m_qos;
|
||||
}
|
||||
|
||||
public String getTopic() {
|
||||
return m_topic;
|
||||
}
|
||||
|
||||
public String getClientID() {
|
||||
return m_clientID;
|
||||
}
|
||||
|
||||
public void setClientID(String m_clientID) {
|
||||
this.m_clientID = m_clientID;
|
||||
}
|
||||
|
||||
public ByteBuf getPayload() {
|
||||
return Unpooled.copiedBuffer(m_payload);
|
||||
}
|
||||
|
||||
public void setRetained(boolean retained) {
|
||||
this.m_retained = retained;
|
||||
}
|
||||
|
||||
public boolean isRetained() {
|
||||
return m_retained;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PublishEvent{clientID='" + m_clientID + '\'' + ", m_retain="
|
||||
+ m_retained + ", m_qos=" + m_qos + ", m_topic='" + m_topic + '\'' + '}';
|
||||
}
|
||||
}
|
||||
|
||||
private final StoredMessage msg;
|
||||
private final String username;
|
||||
private final String topic;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* 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
|
||||
@@ -18,8 +18,6 @@ package io.moquette.interception.messages;
|
||||
|
||||
import io.netty.handler.codec.mqtt.MqttConnectMessage;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class InterceptConnectMessage extends InterceptAbstractMessage {
|
||||
|
||||
private final MqttConnectMessage msg;
|
||||
@@ -74,7 +72,7 @@ public class InterceptConnectMessage extends InterceptAbstractMessage {
|
||||
}
|
||||
|
||||
public byte[] getPassword() {
|
||||
return msg.payload().password().getBytes(StandardCharsets.UTF_8);
|
||||
return msg.payload().password();
|
||||
}
|
||||
|
||||
public String getWillTopic() {
|
||||
@@ -82,6 +80,6 @@ public class InterceptConnectMessage extends InterceptAbstractMessage {
|
||||
}
|
||||
|
||||
public byte[] getWillMessage() {
|
||||
return msg.payload().willMessage().getBytes(StandardCharsets.UTF_8);
|
||||
return msg.payload().willMessage().getBytes();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* 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
|
||||
|
||||
+36
-36
@@ -1,36 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.interception.messages;
|
||||
|
||||
public class InterceptDisconnectMessage implements InterceptMessage {
|
||||
|
||||
private final String clientID;
|
||||
private final String username;
|
||||
|
||||
public InterceptDisconnectMessage(String clientID, String username) {
|
||||
this.clientID = clientID;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getClientID() {
|
||||
return clientID;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 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.interception.messages;
|
||||
|
||||
public class InterceptDisconnectMessage implements InterceptMessage {
|
||||
|
||||
private final String clientID;
|
||||
private final String username;
|
||||
|
||||
public InterceptDisconnectMessage(String clientID, String username) {
|
||||
this.clientID = clientID;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getClientID() {
|
||||
return clientID;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* 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
|
||||
|
||||
+50
-50
@@ -1,50 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.interception.messages;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.mqtt.MqttPublishMessage;
|
||||
|
||||
public class InterceptPublishMessage extends InterceptAbstractMessage {
|
||||
|
||||
private final MqttPublishMessage msg;
|
||||
private final String clientID;
|
||||
private final String username;
|
||||
|
||||
public InterceptPublishMessage(MqttPublishMessage msg, String clientID, String username) {
|
||||
super(msg);
|
||||
this.msg = msg;
|
||||
this.clientID = clientID;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getTopicName() {
|
||||
return msg.variableHeader().topicName();
|
||||
}
|
||||
|
||||
public ByteBuf getPayload() {
|
||||
return msg.payload();
|
||||
}
|
||||
|
||||
public String getClientID() {
|
||||
return clientID;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 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.interception.messages;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.mqtt.MqttPublishMessage;
|
||||
|
||||
public class InterceptPublishMessage extends InterceptAbstractMessage {
|
||||
|
||||
private final MqttPublishMessage msg;
|
||||
private final String clientID;
|
||||
private final String username;
|
||||
|
||||
public InterceptPublishMessage(MqttPublishMessage msg, String clientID, String username) {
|
||||
super(msg);
|
||||
this.msg = msg;
|
||||
this.clientID = clientID;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getTopicName() {
|
||||
return msg.variableHeader().topicName();
|
||||
}
|
||||
|
||||
public ByteBuf getPayload() {
|
||||
return msg.payload();
|
||||
}
|
||||
|
||||
public String getClientID() {
|
||||
return clientID;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.interception.messages;
|
||||
|
||||
import io.moquette.broker.subscriptions.Subscription;
|
||||
import io.netty.handler.codec.mqtt.MqttQoS;
|
||||
|
||||
public class InterceptSubscribeMessage implements InterceptMessage {
|
||||
|
||||
private final Subscription subscription;
|
||||
private final String username;
|
||||
|
||||
public InterceptSubscribeMessage(Subscription subscription, String username) {
|
||||
this.subscription = subscription;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getClientID() {
|
||||
return subscription.getClientId();
|
||||
}
|
||||
|
||||
public MqttQoS getRequestedQos() {
|
||||
return subscription.getRequestedQos();
|
||||
}
|
||||
|
||||
public String getTopicFilter() {
|
||||
return subscription.getTopicFilter().toString();
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
|
||||
package io.moquette.interception.messages;
|
||||
|
||||
public class InterceptUnsubscribeMessage implements InterceptMessage {
|
||||
|
||||
private final String topicFilter;
|
||||
private final String clientID;
|
||||
private final String username;
|
||||
|
||||
public InterceptUnsubscribeMessage(String topicFilter, String clientID, String username) {
|
||||
this.topicFilter = topicFilter;
|
||||
this.clientID = clientID;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getTopicFilter() {
|
||||
return topicFilter;
|
||||
}
|
||||
|
||||
public String getClientID() {
|
||||
return clientID;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* 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
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.persistence;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.hazelcast.core.MapStore;
|
||||
import io.moquette.server.Server;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
public class ChannelLoader implements MapStore<String, WFCMessage.ChannelInfo> {
|
||||
@Override
|
||||
public void store(String s, WFCMessage.ChannelInfo channelInfo) {
|
||||
getDatabaseStore().updateChannelInfo(channelInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeAll(Map<String, WFCMessage.ChannelInfo> map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String s) {
|
||||
getDatabaseStore().removeChannelInfo(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAll(Collection<String> collection) {
|
||||
|
||||
}
|
||||
|
||||
private DatabaseStore getDatabaseStore() {
|
||||
return Server.getServer().getStore().messagesStore().getDatabaseStore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WFCMessage.ChannelInfo load(String key) {
|
||||
return getDatabaseStore().getPersistChannelInfo(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, WFCMessage.ChannelInfo> loadAll(Collection<String> keys) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<String> loadAllKeys() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.persistence;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.hazelcast.core.MapStore;
|
||||
import io.moquette.server.Server;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
public class ChatroomLoader implements MapStore<String, WFCMessage.ChatroomInfo> {
|
||||
private DatabaseStore getDatabaseStore() {
|
||||
return Server.getServer().getStore().messagesStore().getDatabaseStore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the value of a given key. If distributed map doesn't contain the value
|
||||
* for the given key then Hazelcast will call implementation's load (key) method
|
||||
* to obtain the value. Implementation can use any means of loading the given key;
|
||||
* such as an O/R mapping tool, simple SQL or reading a file etc.
|
||||
*
|
||||
* @param key@return value of the key, value cannot be null
|
||||
*/
|
||||
@Override
|
||||
public WFCMessage.ChatroomInfo load(String key) {
|
||||
return getDatabaseStore().getPersistChatroomInfo(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads given keys. This is batch load operation so that implementation can
|
||||
* optimize the multiple loads.
|
||||
* <p>
|
||||
* For any key in the input keys, there should be a single mapping in the resulting map. Also the resulting
|
||||
* map should not have any keys that are not part of the input keys.
|
||||
* <p>
|
||||
* The given collection should not contain any <code>null</code> keys.
|
||||
* The returned Map should not contain any <code>null</code> keys or values.
|
||||
*
|
||||
* @param keys keys of the values entries to load
|
||||
* @return map of loaded key-value pairs.
|
||||
*/
|
||||
@Override
|
||||
public Map<String, WFCMessage.ChatroomInfo> loadAll(Collection<String> keys) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<String> loadAllKeys() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void store(String s, WFCMessage.ChatroomInfo chatroomInfo) {
|
||||
getDatabaseStore().updateChatroomInfo(s, chatroomInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeAll(Map<String, WFCMessage.ChatroomInfo> map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String s) {
|
||||
getDatabaseStore().removeChatroomInfo(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAll(Collection<String> collection) {
|
||||
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <[email protected]>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.persistence;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import com.hazelcast.core.MapLoader;
|
||||
import io.moquette.server.Server;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
public class GroupLoader implements MapLoader<String, WFCMessage.GroupInfo> {
|
||||
private DatabaseStore getDatabaseStore() {
|
||||
return Server.getServer().getStore().messagesStore().getDatabaseStore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the value of a given key. If distributed map doesn't contain the value
|
||||
* for the given key then Hazelcast will call implementation's load (key) method
|
||||
* to obtain the value. Implementation can use any means of loading the given key;
|
||||
* such as an O/R mapping tool, simple SQL or reading a file etc.
|
||||
*
|
||||
* @param key@return value of the key, value cannot be null
|
||||
*/
|
||||
@Override
|
||||
public WFCMessage.GroupInfo load(String key) {
|
||||
return getDatabaseStore().getPersistGroupInfo(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads given keys. This is batch load operation so that implementation can
|
||||
* optimize the multiple loads.
|
||||
* <p>
|
||||
* For any key in the input keys, there should be a single mapping in the resulting map. Also the resulting
|
||||
* map should not have any keys that are not part of the input keys.
|
||||
* <p>
|
||||
* The given collection should not contain any <code>null</code> keys.
|
||||
* The returned Map should not contain any <code>null</code> keys or values.
|
||||
*
|
||||
* @param keys keys of the values entries to load
|
||||
* @return map of loaded key-value pairs.
|
||||
*/
|
||||
@Override
|
||||
public Map<String, WFCMessage.GroupInfo> loadAll(Collection<String> keys) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all of the keys from the store. The returned {@link Iterable} may return the keys lazily
|
||||
* by loading them in batches. The {@link Iterator} of this {@link Iterable} may implement the
|
||||
* {@link Closeable} interface in which case it will be closed once iteration is over.
|
||||
* This is intended for releasing resources such as closing a JDBC result set.
|
||||
* <p>
|
||||
* The returned Iterable should not contain any <code>null</code> keys.
|
||||
*
|
||||
* @return all the keys. Keys inside the Iterable cannot be null.
|
||||
*/
|
||||
@Override
|
||||
public Iterable<String> loadAllKeys() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package io.moquette.persistence;
|
||||
|
||||
import io.moquette.BrokerConstants;
|
||||
import io.moquette.broker.IQueueRepository;
|
||||
import io.moquette.broker.IRetainedRepository;
|
||||
import io.moquette.broker.ISubscriptionsRepository;
|
||||
import io.moquette.broker.config.IConfig;
|
||||
import org.h2.mvstore.MVStore;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class H2Builder {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(H2Builder.class);
|
||||
|
||||
private final String storePath;
|
||||
private final int autosaveInterval; // in seconds
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private MVStore mvStore;
|
||||
|
||||
public H2Builder(IConfig props, ScheduledExecutorService scheduler) {
|
||||
this.storePath = props.getProperty(BrokerConstants.PERSISTENT_STORE_PROPERTY_NAME, "");
|
||||
final String autosaveProp = props.getProperty(BrokerConstants.AUTOSAVE_INTERVAL_PROPERTY_NAME, "30");
|
||||
this.autosaveInterval = Integer.parseInt(autosaveProp);
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
@SuppressWarnings("FutureReturnValueIgnored")
|
||||
public H2Builder initStore() {
|
||||
LOG.info("Initializing H2 store");
|
||||
if (storePath == null || storePath.isEmpty()) {
|
||||
throw new IllegalArgumentException("H2 store path can't be null or empty");
|
||||
}
|
||||
mvStore = new MVStore.Builder()
|
||||
.fileName(storePath)
|
||||
.autoCommitDisabled()
|
||||
.open();
|
||||
|
||||
LOG.trace("Scheduling H2 commit task");
|
||||
scheduler.scheduleWithFixedDelay(() -> {
|
||||
LOG.trace("Committing to H2");
|
||||
mvStore.commit();
|
||||
}, autosaveInterval, autosaveInterval, TimeUnit.SECONDS);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISubscriptionsRepository subscriptionsRepository() {
|
||||
return new H2SubscriptionsRepository(mvStore);
|
||||
}
|
||||
|
||||
public void closeStore() {
|
||||
mvStore.close();
|
||||
}
|
||||
|
||||
public IQueueRepository queueRepository() {
|
||||
return new H2QueueRepository(mvStore);
|
||||
}
|
||||
|
||||
public IRetainedRepository retainedRepository() {
|
||||
return new H2RetainedRepository(mvStore);
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.persistence;
|
||||
|
||||
import org.h2.mvstore.MVMap;
|
||||
import org.h2.mvstore.MVStore;
|
||||
|
||||
import java.util.AbstractQueue;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
class H2PersistentQueue<T> extends AbstractQueue<T> {
|
||||
|
||||
private final MVMap<Long, T> queueMap;
|
||||
private final MVMap<String, Long> metadataMap;
|
||||
private final AtomicLong head;
|
||||
private final AtomicLong tail;
|
||||
|
||||
H2PersistentQueue(MVStore store, String queueName) {
|
||||
if (queueName == null || queueName.isEmpty()) {
|
||||
throw new IllegalArgumentException("queueName parameter can't be empty or null");
|
||||
}
|
||||
this.queueMap = store.openMap("queue_" + queueName);
|
||||
this.metadataMap = store.openMap("queue_" + queueName + "_meta");
|
||||
|
||||
//setup head index
|
||||
long headIdx = 0L;
|
||||
if (this.metadataMap.containsKey("head")) {
|
||||
headIdx = this.metadataMap.get("head");
|
||||
} else {
|
||||
this.metadataMap.put("head", headIdx);
|
||||
}
|
||||
this.head = new AtomicLong(headIdx);
|
||||
|
||||
//setup tail index
|
||||
long tailIdx = 0L;
|
||||
if (this.metadataMap.containsKey("tail")) {
|
||||
tailIdx = this.metadataMap.get("tail");
|
||||
} else {
|
||||
this.metadataMap.put("tail", tailIdx);
|
||||
}
|
||||
this.tail = new AtomicLong(tailIdx);
|
||||
}
|
||||
|
||||
static void dropQueue(MVStore store, String queueName) {
|
||||
store.removeMap(store.openMap("queue_" + queueName));
|
||||
store.removeMap(store.openMap("queue_" + queueName + "_meta"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<T> iterator() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return this.head.intValue() - this.tail.intValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean offer(T t) {
|
||||
if (t == null) {
|
||||
throw new NullPointerException("Inserted element can't be null");
|
||||
}
|
||||
final long nextHead = head.getAndIncrement();
|
||||
this.queueMap.put(nextHead, t);
|
||||
this.metadataMap.put("head", nextHead + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T poll() {
|
||||
if (head.equals(tail)) {
|
||||
return null;
|
||||
}
|
||||
final long nextTail = tail.getAndIncrement();
|
||||
final T tail = this.queueMap.get(nextTail);
|
||||
queueMap.remove(nextTail);
|
||||
this.metadataMap.put("tail", nextTail + 1);
|
||||
return tail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T peek() {
|
||||
if (head.equals(tail)) {
|
||||
return null;
|
||||
}
|
||||
return this.queueMap.get(tail.get());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package io.moquette.persistence;
|
||||
|
||||
import io.moquette.broker.IQueueRepository;
|
||||
import io.moquette.broker.SessionRegistry;
|
||||
import org.h2.mvstore.MVStore;
|
||||
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
public class H2QueueRepository implements IQueueRepository {
|
||||
|
||||
private MVStore mvStore;
|
||||
|
||||
public H2QueueRepository(MVStore mvStore) {
|
||||
this.mvStore = mvStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Queue<SessionRegistry.EnqueuedMessage> createQueue(String cli, boolean clean) {
|
||||
if (!clean) {
|
||||
return new H2PersistentQueue<>(mvStore, cli);
|
||||
}
|
||||
return new ConcurrentLinkedQueue<>();
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package io.moquette.persistence;
|
||||
|
||||
import io.moquette.broker.IRetainedRepository;
|
||||
import io.moquette.broker.RetainedMessage;
|
||||
import io.moquette.broker.subscriptions.Topic;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.mqtt.MqttPublishMessage;
|
||||
import org.h2.mvstore.MVMap;
|
||||
import org.h2.mvstore.MVStore;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class H2RetainedRepository implements IRetainedRepository {
|
||||
|
||||
private final MVMap<Topic, RetainedMessage> queueMap;
|
||||
|
||||
public H2RetainedRepository(MVStore mvStore) {
|
||||
this.queueMap = mvStore.openMap("retained_store");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanRetained(Topic topic) {
|
||||
queueMap.remove(topic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void retain(Topic topic, MqttPublishMessage msg) {
|
||||
final ByteBuf payload = msg.content();
|
||||
byte[] rawPayload = new byte[payload.readableBytes()];
|
||||
payload.getBytes(0, rawPayload);
|
||||
final RetainedMessage toStore = new RetainedMessage(msg.fixedHeader().qosLevel(), rawPayload);
|
||||
queueMap.put(topic, toStore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return queueMap.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RetainedMessage> retainedOnTopic(String topic) {
|
||||
final Topic searchTopic = new Topic(topic);
|
||||
final List<RetainedMessage> matchingMessages = new ArrayList<>();
|
||||
for (Map.Entry<Topic, RetainedMessage> entry : queueMap.entrySet()) {
|
||||
final Topic scanTopic = entry.getKey();
|
||||
if (searchTopic.match(scanTopic)) {
|
||||
matchingMessages.add(entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
return matchingMessages;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package io.moquette.persistence;
|
||||
|
||||
import io.moquette.broker.ISubscriptionsRepository;
|
||||
import io.moquette.broker.subscriptions.Subscription;
|
||||
import org.h2.mvstore.Cursor;
|
||||
import org.h2.mvstore.MVMap;
|
||||
import org.h2.mvstore.MVStore;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class H2SubscriptionsRepository implements ISubscriptionsRepository {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(H2SubscriptionsRepository.class);
|
||||
private static final String SUBSCRIPTIONS_MAP = "subscriptions";
|
||||
|
||||
private MVMap<String, Subscription> subscriptions;
|
||||
|
||||
H2SubscriptionsRepository(MVStore mvStore) {
|
||||
this.subscriptions = mvStore.openMap(SUBSCRIPTIONS_MAP);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Subscription> listAllSubscriptions() {
|
||||
LOG.debug("Retrieving existing subscriptions");
|
||||
|
||||
List<Subscription> results = new ArrayList<>();
|
||||
Cursor<String, Subscription> mapCursor = subscriptions.cursor(null);
|
||||
while (mapCursor.hasNext()) {
|
||||
String subscriptionStr = mapCursor.next();
|
||||
results.add(mapCursor.getValue());
|
||||
}
|
||||
LOG.debug("Loaded {} subscriptions", results.size());
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addNewSubscription(Subscription subscription) {
|
||||
subscriptions.put(subscription.getTopicFilter() + "-" + subscription.getClientId(), subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSubscription(String topicFilter, String clientID) {
|
||||
subscriptions.remove(topicFilter + "-" + clientID);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,586 @@
|
||||
/*
|
||||
* 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.persistence;
|
||||
|
||||
import cn.wildfirechat.proto.WFCMessage;
|
||||
import io.moquette.server.Constants;
|
||||
import io.moquette.server.Server;
|
||||
import io.moquette.spi.ClientSession;
|
||||
import io.moquette.spi.IMessagesStore.StoredMessage;
|
||||
import io.moquette.spi.ISessionsStore;
|
||||
|
||||
import io.netty.util.internal.StringUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import win.liyufan.im.UserSettingScope;
|
||||
import win.liyufan.im.Utility;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public class MemorySessionStore implements ISessionsStore {
|
||||
private static int dumy = 1;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MemorySessionStore.class);
|
||||
|
||||
public static class Session implements Comparable<Session>{
|
||||
final String clientID;
|
||||
String username;
|
||||
private String appName;
|
||||
private String deviceToken;
|
||||
private String voipDeviceToken;
|
||||
private String secret;
|
||||
private String dbSecret;
|
||||
|
||||
private Map<String, String> slientConvs = new ConcurrentHashMap<>();
|
||||
private boolean globalSlient = false;
|
||||
private boolean hiddenNotifyDetail = false;
|
||||
|
||||
private long lastActiveTime;
|
||||
|
||||
private long lastChatroomActiveTime;
|
||||
|
||||
private volatile int unReceivedMsgs;
|
||||
|
||||
public long getLastActiveTime() {
|
||||
return lastActiveTime;
|
||||
}
|
||||
|
||||
public long getLastChatroomActiveTime() {
|
||||
return lastChatroomActiveTime;
|
||||
}
|
||||
|
||||
public void refreshLastChatroomActiveTime() {
|
||||
this.lastChatroomActiveTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public void refreshLastActiveTime() {
|
||||
this.lastActiveTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public int getUnReceivedMsgs() {
|
||||
return unReceivedMsgs;
|
||||
}
|
||||
|
||||
public void setUnReceivedMsgs(int unReceivedMsgs) {
|
||||
this.unReceivedMsgs = unReceivedMsgs;
|
||||
}
|
||||
|
||||
public int getPushType() {
|
||||
return pushType;
|
||||
}
|
||||
|
||||
public void setPushType(int pushType) {
|
||||
this.pushType = pushType;
|
||||
}
|
||||
|
||||
public String getDeviceName() {
|
||||
return deviceName;
|
||||
}
|
||||
|
||||
public void setDeviceName(String deviceName) {
|
||||
this.deviceName = deviceName;
|
||||
}
|
||||
|
||||
public String getDeviceVersion() {
|
||||
return deviceVersion;
|
||||
}
|
||||
|
||||
public void setDeviceVersion(String deviceVersion) {
|
||||
this.deviceVersion = deviceVersion;
|
||||
}
|
||||
|
||||
public String getPhoneName() {
|
||||
return phoneName;
|
||||
}
|
||||
|
||||
public void setPhoneName(String phoneName) {
|
||||
this.phoneName = phoneName;
|
||||
}
|
||||
|
||||
public String getLanguage() {
|
||||
return language;
|
||||
}
|
||||
|
||||
public void setLanguage(String language) {
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
public String getCarrierName() {
|
||||
return carrierName;
|
||||
}
|
||||
|
||||
public void setCarrierName(String carrierName) {
|
||||
this.carrierName = carrierName;
|
||||
}
|
||||
|
||||
public long getUpdateDt() {
|
||||
return updateDt;
|
||||
}
|
||||
|
||||
public void setUpdateDt(long updateDt) {
|
||||
this.updateDt = updateDt;
|
||||
}
|
||||
|
||||
private int pushType;
|
||||
private int platform;
|
||||
|
||||
private String deviceName;
|
||||
private String deviceVersion;
|
||||
private String phoneName;
|
||||
private String language;
|
||||
private String carrierName;
|
||||
private long updateDt;
|
||||
|
||||
final ClientSession clientSession;
|
||||
final BlockingQueue<StoredMessage> queue = new ArrayBlockingQueue<>(Constants.MAX_MESSAGE_QUEUE);
|
||||
final Map<Integer, StoredMessage> secondPhaseStore = new ConcurrentHashMap<>();
|
||||
final Map<Integer, StoredMessage> outboundFlightMessages =
|
||||
Collections.synchronizedMap(new HashMap<Integer, StoredMessage>());
|
||||
final Map<Integer, StoredMessage> inboundFlightMessages = new ConcurrentHashMap<>();
|
||||
|
||||
public Session(String username, String clientID, ClientSession clientSession) {
|
||||
this.clientID = clientID;
|
||||
this.clientSession = clientSession;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public boolean isHiddenNotifyDetail() {
|
||||
return hiddenNotifyDetail;
|
||||
}
|
||||
|
||||
public void setHiddenNotifyDetail(boolean hiddenNotifyDetail) {
|
||||
this.hiddenNotifyDetail = hiddenNotifyDetail;
|
||||
}
|
||||
|
||||
public Map<String, String> getSlientConvs() {
|
||||
return slientConvs;
|
||||
}
|
||||
|
||||
public boolean isGlobalSlient() {
|
||||
return globalSlient;
|
||||
}
|
||||
|
||||
public void setGlobalSlient(boolean globalSlient) {
|
||||
this.globalSlient = globalSlient;
|
||||
}
|
||||
|
||||
public String getAppName() {
|
||||
return appName;
|
||||
}
|
||||
|
||||
public void setAppName(String appName) {
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
public String getDeviceToken() {
|
||||
return deviceToken;
|
||||
}
|
||||
|
||||
public void setDeviceToken(String deviceToken) {
|
||||
this.deviceToken = deviceToken;
|
||||
}
|
||||
|
||||
public int getPlatform() {
|
||||
return platform;
|
||||
}
|
||||
|
||||
public void setPlatform(int platform) {
|
||||
this.platform = platform;
|
||||
}
|
||||
|
||||
public String getClientID() {
|
||||
return clientID;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public ClientSession getClientSession() {
|
||||
return clientSession;
|
||||
}
|
||||
|
||||
public String getSecret() {
|
||||
return secret;
|
||||
}
|
||||
|
||||
public void setSecret(String secret) {
|
||||
this.secret = secret;
|
||||
}
|
||||
|
||||
public String getDbSecret() {
|
||||
return dbSecret;
|
||||
}
|
||||
|
||||
public void setDbSecret(String dbSecret) {
|
||||
this.dbSecret = dbSecret;
|
||||
}
|
||||
|
||||
public String getVoipDeviceToken() {
|
||||
return voipDeviceToken;
|
||||
}
|
||||
|
||||
public void setVoipDeviceToken(String voipDeviceToken) {
|
||||
this.voipDeviceToken = voipDeviceToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Session o) {
|
||||
// TODO Auto-generated method stub
|
||||
if (clientID.equals(o.clientID) && username.equals(o.username)) {
|
||||
return 0;
|
||||
}
|
||||
if (clientID.equals(o.clientID)) {
|
||||
return username.compareTo(o.username);
|
||||
} else {
|
||||
return clientID.compareTo(o.clientID);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
private final Map<String, Session> sessions = new ConcurrentHashMap<>();
|
||||
private final Map<String, ConcurrentSkipListSet<String>> userSessions = new ConcurrentHashMap<>();
|
||||
|
||||
private final Server mServer;
|
||||
private final DatabaseStore databaseStore;
|
||||
public MemorySessionStore(Server server, DatabaseStore databaseStore) {
|
||||
mServer = server;
|
||||
this.databaseStore = databaseStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session getSession(String clientID) {
|
||||
Session session = sessions.get(clientID);
|
||||
if (session == null) {
|
||||
LOG.error("Can't find the session for client <{}>", clientID);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initStore() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String clientID) {
|
||||
return sessions.containsKey(clientID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSessionToken(Session session, boolean voip) {
|
||||
databaseStore.updateSessionToken(session.getUsername(), session.getClientID(), voip ? session.getVoipDeviceToken() : session.getDeviceToken(), voip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session createUserSession(String username, String clientID) {
|
||||
LOG.debug("createUserSession for client <{}>, user <{}>", clientID, username);
|
||||
|
||||
ClientSession clientSession = new ClientSession(clientID, this);
|
||||
Session session = databaseStore.getSession(username, clientID, clientSession);
|
||||
|
||||
if (session == null) {
|
||||
session = databaseStore.createSession(username, clientID, clientSession);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ErrorCode createNewSession(String username, String clientID, boolean cleanSession) {
|
||||
LOG.debug("createNewSession for client <{}>", clientID);
|
||||
|
||||
Session session = sessions.get(clientID);
|
||||
if (session != null) {
|
||||
LOG.error("already exists a session for client <{}>, bad condition", clientID);
|
||||
throw new IllegalArgumentException("Can't create a session with the ID of an already existing" + clientID);
|
||||
}
|
||||
|
||||
|
||||
ClientSession clientSession = new ClientSession(clientID, this);
|
||||
session = databaseStore.getSession(username, clientID, clientSession);
|
||||
|
||||
if (session == null) {
|
||||
session = databaseStore.createSession(username, clientID, clientSession);
|
||||
}
|
||||
|
||||
List<WFCMessage.UserSettingEntry> settings = mServer.getStore().messagesStore().getUserSetting(username, UserSettingScope.kUserSettingConversationSilent);
|
||||
for (WFCMessage.UserSettingEntry entry : settings) {
|
||||
if (!StringUtil.isNullOrEmpty(entry.getValue()) && !entry.getValue().equals("0")) {
|
||||
session.slientConvs.put(entry.getKey(), entry.getValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
List<WFCMessage.UserSettingEntry> settings2 = mServer.getStore().messagesStore().getUserSetting(username, UserSettingScope.kUserSettingGlobalSilent);
|
||||
for (WFCMessage.UserSettingEntry entry : settings2) {
|
||||
if (!StringUtil.isNullOrEmpty(entry.getValue()) && !entry.getValue().equals("0")) {
|
||||
session.globalSlient = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
sessions.put(clientID, session);
|
||||
ConcurrentSkipListSet<String> sessionSet = userSessions.get(username);
|
||||
if (sessionSet == null) {
|
||||
sessionSet = new ConcurrentSkipListSet<>();
|
||||
userSessions.put(username, sessionSet);
|
||||
}
|
||||
sessionSet = userSessions.get(username);
|
||||
sessionSet.add(clientID);
|
||||
|
||||
return ErrorCode.ERROR_CODE_SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientSession updateExistSession(String username, String clientID, WFCMessage.RouteRequest endpoint, boolean cleanSession) {
|
||||
LOG.debug("updateExistSession for client <{}>", clientID);
|
||||
Session session = sessions.get(clientID);
|
||||
if (session == null) {
|
||||
LOG.error("already exists a session for client <{}>, bad condition", clientID);
|
||||
throw new IllegalArgumentException("Can't create a session with the ID of an already existing" + clientID);
|
||||
}
|
||||
if (!session.getUsername().equals(username)) {
|
||||
ConcurrentSkipListSet<String> sessionSet = userSessions.get(session.getUsername());
|
||||
if(sessionSet != null) {
|
||||
sessionSet.remove(clientID);
|
||||
}
|
||||
}
|
||||
|
||||
session.setUsername(username);
|
||||
sessions.put(clientID, session);
|
||||
|
||||
ConcurrentSkipListSet<String> sessionSet = userSessions.get(username);
|
||||
if (sessionSet == null) {
|
||||
sessionSet = new ConcurrentSkipListSet<>();
|
||||
userSessions.put(username, sessionSet);
|
||||
}
|
||||
sessionSet = userSessions.get(username);
|
||||
sessionSet.add(clientID);
|
||||
|
||||
if (endpoint != null) {
|
||||
databaseStore.updateSession(username, clientID, session, endpoint);
|
||||
}
|
||||
|
||||
return session.clientSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session sessionForClientAndUser(String username, String clientID) {
|
||||
Session session = sessions.get(clientID);
|
||||
if (session != null) {
|
||||
if (session.getUsername().equals(username)) {
|
||||
return session;
|
||||
} else {
|
||||
cleanSession(clientID);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientSession sessionForClient(String clientID) {
|
||||
if (!sessions.containsKey(clientID)) {
|
||||
LOG.error("Can't find the session for client <{}>", clientID);
|
||||
return null;
|
||||
}
|
||||
|
||||
Session session = sessions.get(clientID);
|
||||
ClientSession clientSession = new ClientSession(clientID, this);
|
||||
return clientSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Session> sessionForUser(String username) {
|
||||
ConcurrentSkipListSet<String> sessionSet = userSessions.get(username);
|
||||
if (sessionSet == null) {
|
||||
sessionSet = new ConcurrentSkipListSet<String>();
|
||||
userSessions.put(username, sessionSet);
|
||||
}
|
||||
sessionSet = userSessions.get(username);
|
||||
ArrayList<Session> out = new ArrayList<>();
|
||||
for (String clientId : sessionSet
|
||||
) {
|
||||
Session session = sessions.get(clientId);
|
||||
if (session != null && session.getUsername().equals(username)) {
|
||||
out.add(session);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ClientSession> getAllSessions() {
|
||||
Collection<ClientSession> result = new ArrayList<>();
|
||||
for (Session entry : sessions.values()) {
|
||||
result.add(new ClientSession(entry.clientID, this));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoredMessage inFlightAck(String clientID, int messageID) {
|
||||
return getSession(clientID).outboundFlightMessages.remove(messageID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inFlight(String clientID, int messageID, StoredMessage msg) {
|
||||
Session session = sessions.get(clientID);
|
||||
if (session == null) {
|
||||
LOG.error("Can't find the session for client <{}>", clientID);
|
||||
return;
|
||||
}
|
||||
|
||||
session.outboundFlightMessages.put(messageID, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the next valid packetIdentifier for the given client session.
|
||||
*/
|
||||
@Override
|
||||
public int nextPacketID(String clientID) {
|
||||
if (!sessions.containsKey(clientID)) {
|
||||
LOG.error("Can't find the session for client <{}>", clientID);
|
||||
return -1;
|
||||
}
|
||||
|
||||
Map<Integer, StoredMessage> m = sessions.get(clientID).outboundFlightMessages;
|
||||
int maxId = m.keySet().isEmpty() ? 0 : Collections.max(m.keySet());
|
||||
int nextPacketId = (maxId + 1) % 0xFFFF;
|
||||
m.put(nextPacketId, null);
|
||||
return nextPacketId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockingQueue<StoredMessage> queue(String clientID) {
|
||||
if (!sessions.containsKey(clientID)) {
|
||||
LOG.error("Can't find the session for client <{}>", clientID);
|
||||
return null;
|
||||
}
|
||||
|
||||
return sessions.get(clientID).queue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dropQueue(String clientID) {
|
||||
sessions.get(clientID).queue.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveInFlightToSecondPhaseAckWaiting(String clientID, int messageID, StoredMessage msg) {
|
||||
LOG.info("Moving msg inflight second phase store, clientID <{}> messageID {}", clientID, messageID);
|
||||
Session session = sessions.get(clientID);
|
||||
if (session == null) {
|
||||
LOG.error("Can't find the session for client <{}>", clientID);
|
||||
return;
|
||||
}
|
||||
|
||||
session.secondPhaseStore.put(messageID, msg);
|
||||
session.outboundFlightMessages.put(messageID, msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoredMessage secondPhaseAcknowledged(String clientID, int messageID) {
|
||||
LOG.info("Acknowledged message in second phase, clientID <{}> messageID {}", clientID, messageID);
|
||||
return getSession(clientID).secondPhaseStore.remove(messageID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInflightMessagesNo(String clientID) {
|
||||
Session session = sessions.get(clientID);
|
||||
if (session == null) {
|
||||
LOG.error("Can't find the session for client <{}>", clientID);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return session.inboundFlightMessages.size() + session.secondPhaseStore.size()
|
||||
+ session.outboundFlightMessages.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoredMessage inboundInflight(String clientID, int messageID) {
|
||||
return getSession(clientID).inboundFlightMessages.get(messageID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsInboundInflight(String clientID, int messageID, StoredMessage msg) {
|
||||
if (!sessions.containsKey(clientID))
|
||||
LOG.error("Can't find the session for client <{}>", clientID);
|
||||
|
||||
sessions.get(clientID).inboundFlightMessages.put(messageID, msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPendingPublishMessagesNo(String clientID) {
|
||||
if (!sessions.containsKey(clientID)) {
|
||||
LOG.error("Can't find the session for client <{}>", clientID);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return sessions.get(clientID).queue.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSecondPhaseAckPendingMessages(String clientID) {
|
||||
if (!sessions.containsKey(clientID)) {
|
||||
LOG.error("Can't find the session for client <{}>", clientID);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return sessions.get(clientID).secondPhaseStore.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanSession(String clientID) {
|
||||
LOG.error("Fooooooooo <{}>", clientID);
|
||||
|
||||
Session session = sessions.get(clientID);
|
||||
if (session == null) {
|
||||
LOG.error("Can't find the session for client <{}>", clientID);
|
||||
return;
|
||||
}
|
||||
ConcurrentSkipListSet<String> sessionSet = userSessions.get(session.username);
|
||||
if (sessionSet == null) {
|
||||
sessionSet = new ConcurrentSkipListSet<>();
|
||||
userSessions.put(session.username, sessionSet);
|
||||
}
|
||||
sessionSet = userSessions.get(session.username);
|
||||
sessionSet.remove(clientID);
|
||||
|
||||
// remove also the messages stored of type QoS1/2
|
||||
LOG.info("Removing stored messages with QoS 1 and 2. ClientId={}", clientID);
|
||||
|
||||
session.secondPhaseStore.clear();
|
||||
session.outboundFlightMessages.clear();
|
||||
session.inboundFlightMessages.clear();
|
||||
|
||||
LOG.info("Wiping existing subscriptions. ClientId={}", clientID);
|
||||
|
||||
//remove also the enqueued messages
|
||||
dropQueue(clientID);
|
||||
|
||||
// TODO this missing last step breaks the junit test
|
||||
sessions.remove(clientID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.persistence;
|
||||
|
||||
import cn.wildfirechat.server.ThreadPoolExecutorWrapper;
|
||||
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 java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class MemoryStorageService implements IStore {
|
||||
|
||||
private MemorySessionStore m_sessionsStore;
|
||||
private MemoryMessagesStore m_messagesStore;
|
||||
|
||||
public MemoryStorageService(IConfig props, ThreadPoolExecutorWrapper scheduler, Server server) {
|
||||
DatabaseStore databaseStore = new DatabaseStore(scheduler);
|
||||
m_messagesStore = new MemoryMessagesStore(server, databaseStore);
|
||||
m_sessionsStore = new MemorySessionStore(server, databaseStore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IMessagesStore messagesStore() {
|
||||
return m_messagesStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ISessionsStore sessionsStore() {
|
||||
return m_sessionsStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initStore() {
|
||||
m_messagesStore.initStore();
|
||||
m_sessionsStore.initStore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012-2018 The original author or authors
|
||||
* ------------------------------------------------------
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Apache License v2.0 which accompanies this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* The Apache License v2.0 is available at
|
||||
* http://www.opensource.org/licenses/apache2.0.php
|
||||
*
|
||||
* You may elect to redistribute this code under either of these licenses.
|
||||
*/
|
||||
package io.moquette.persistence;
|
||||
|
||||
import io.moquette.broker.ISubscriptionsRepository;
|
||||
import io.moquette.broker.subscriptions.Subscription;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class MemorySubscriptionsRepository implements ISubscriptionsRepository {
|
||||
|
||||
private final List<Subscription> subscriptions = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public List<Subscription> listAllSubscriptions() {
|
||||
return Collections.unmodifiableList(subscriptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addNewSubscription(Subscription subscription) {
|
||||
subscriptions.add(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSubscription(String topic, String clientID) {
|
||||
subscriptions.stream()
|
||||
.filter(s -> s.getTopicFilter().toString().equals(topic) && s.getClientId().equals(clientID))
|
||||
.findFirst()
|
||||
.ifPresent(subscriptions::remove);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <heavyrain.lee@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.persistence;
|
||||
|
||||
import com.hazelcast.core.MapStore;
|
||||
import io.moquette.server.Server;
|
||||
import win.liyufan.im.MessageBundle;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
public class MessageLoader implements MapStore<Long, MessageBundle> {
|
||||
private DatabaseStore getDatabaseStore() {
|
||||
return Server.getServer().getStore().messagesStore().getDatabaseStore();
|
||||
}
|
||||
/**
|
||||
* Loads the value of a given key. If distributed map doesn't contain the value
|
||||
* for the given key then Hazelcast will call implementation's load (key) method
|
||||
* to obtain the value. Implementation can use any means of loading the given key;
|
||||
* such as an O/R mapping tool, simple SQL or reading a file etc.
|
||||
*
|
||||
* @param key@return value of the key, value cannot be null
|
||||
*/
|
||||
@Override
|
||||
public MessageBundle load(Long key) {
|
||||
return getDatabaseStore().getMessage(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads given keys. This is batch load operation so that implementation can
|
||||
* optimize the multiple loads.
|
||||
* <p>
|
||||
* For any key in the input keys, there should be a single mapping in the resulting map. Also the resulting
|
||||
* map should not have any keys that are not part of the input keys.
|
||||
* <p>
|
||||
* The given collection should not contain any <code>null</code> keys.
|
||||
* The returned Map should not contain any <code>null</code> keys or values.
|
||||
*
|
||||
* @param keys keys of the values entries to load
|
||||
* @return map of loaded key-value pairs.
|
||||
*/
|
||||
@Override
|
||||
public Map<Long, MessageBundle> loadAll(Collection<Long> keys) {
|
||||
return getDatabaseStore().getMessages(keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all of the keys from the store. The returned {@link Iterable} may return the keys lazily
|
||||
* by loading them in batches. The {@link Iterator} of this {@link Iterable} may implement the
|
||||
* {@link Closeable} interface in which case it will be closed once iteration is over.
|
||||
* This is intended for releasing resources such as closing a JDBC result set.
|
||||
* <p>
|
||||
* The returned Iterable should not contain any <code>null</code> keys.
|
||||
*
|
||||
* @return all the keys. Keys inside the Iterable cannot be null.
|
||||
*/
|
||||
@Override
|
||||
public Iterable<Long> loadAllKeys() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void store(Long aLong, MessageBundle messageBundle) {
|
||||
getDatabaseStore().persistMessage(messageBundle.getMessage(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeAll(Map<Long, MessageBundle> map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Long aLong) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAll(Collection<Long> collection) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* This file is part of the Wildfire Chat package.
|
||||
* (c) Heavyrain2012 <heavyrain.lee@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
package io.moquette.persistence;
|
||||
|
||||
import io.moquette.persistence.remote.*;
|
||||
import io.moquette.server.Server;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import win.liyufan.im.ErrorCode;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class RPCCenter {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RPCCenter.class);
|
||||
public static final String CHECK_USER_ONLINE_REQUEST = "check_user_online";
|
||||
public static final String KICKOFF_USER_REQUEST = "kickoff_user";
|
||||
|
||||
private Server server;
|
||||
public ConcurrentHashMap<Integer, RequestInfo> requestMap = new ConcurrentHashMap<>();
|
||||
private AtomicInteger aiRequestId = new AtomicInteger(1);
|
||||
public ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
|
||||
|
||||
public void init(Server server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onSuccess(byte[] response);
|
||||
void onError(ErrorCode errorCode);
|
||||
void onTimeout();
|
||||
Executor getResponseExecutor();
|
||||
}
|
||||
|
||||
private static RPCCenter instance;
|
||||
|
||||
public static RPCCenter getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new RPCCenter();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
protected RPCCenter() {}
|
||||
|
||||
public void sendRequest(String fromUser, String clientId, String request, byte[] message, String target, TargetEntry.Type type, Callback callback) {
|
||||
int requestId = 0;
|
||||
|
||||
if (callback != null) {
|
||||
requestId = aiRequestId.incrementAndGet();
|
||||
if (requestId == Integer.MAX_VALUE) {
|
||||
if(!aiRequestId.compareAndSet(Integer.MAX_VALUE, 1)) {
|
||||
requestId = aiRequestId.incrementAndGet();
|
||||
}
|
||||
}
|
||||
requestMap.put(requestId, new RequestInfo(fromUser, clientId, callback, message, requestId, request));
|
||||
}
|
||||
|
||||
server.internalRpcMsg(fromUser, clientId, message, requestId, "", request);
|
||||
}
|
||||
|
||||
public void sendResponse(int errorCode, byte[] message, String toUuid, int requestId) {
|
||||
LOG.debug("send async reponse to {} with requestId {}", toUuid, requestId);
|
||||
if (requestId > 0) {
|
||||
RequestInfo info = requestMap.remove(requestId);
|
||||
LOG.debug("receive async reponse requestId {}, errorCode {}", requestId, errorCode);
|
||||
if(info != null) {
|
||||
info.future.cancel(true);
|
||||
if (info.callback != null) {
|
||||
info.callback.getResponseExecutor().execute(() -> {
|
||||
if (errorCode == 0) {
|
||||
info.callback.onSuccess(message);
|
||||
} else {
|
||||
info.callback.onError(ErrorCode.fromCode(errorCode));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user