From b64e21dfbccaca0465c5a5fe9169754145f01904 Mon Sep 17 00:00:00 2001 From: wuyiadmin Date: Wed, 27 Apr 2016 11:17:19 +0800 Subject: [PATCH 01/18] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AF=B9=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E7=9A=84=E6=93=8D=E4=BD=9C=E7=9A=84=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../power/PowerAuthenticator.java | 64 +++++++++ mithqtt-broker/build.gradle | 2 + mithqtt-storage-mongo/build.gradle | 5 + .../power/storage/mongo/MongoStorage.java | 124 ++++++++++++++++++ .../power/storage/mongo/entity/Agent.java | 113 ++++++++++++++++ .../storage/mongo/entity/Permission.java | 28 ++++ .../storage/mongo/entity/PermissionLevel.java | 29 ++++ .../j1st/power/storage/mongo/entity/User.java | 84 ++++++++++++ .../power/storage/mongo/entity/UserRole.java | 28 ++++ settings.gradle | 1 + 10 files changed, 478 insertions(+) create mode 100644 mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java create mode 100644 mithqtt-storage-mongo/build.gradle create mode 100644 mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java create mode 100644 mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/Agent.java create mode 100644 mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/Permission.java create mode 100644 mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/PermissionLevel.java create mode 100644 mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/User.java create mode 100644 mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/UserRole.java diff --git a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java new file mode 100644 index 0000000..514ee80 --- /dev/null +++ b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java @@ -0,0 +1,64 @@ +package io.j1st.mithqtt.authenticator.power; + +import com.github.longkerdandy.mithqtt.api.auth.Authenticator; +import com.github.longkerdandy.mithqtt.api.auth.AuthorizeResult; +import io.j1st.power.storage.mongo.MongoStorage; +import io.netty.handler.codec.mqtt.MqttGrantedQoS; +import io.netty.handler.codec.mqtt.MqttTopicSubscription; +import org.apache.commons.configuration.AbstractConfiguration; + +import java.util.ArrayList; +import java.util.List; + +/** + * Dummy Authenticator + * This authenticator basically authorize everything, it should only been used for test purpose + */ +@SuppressWarnings("unused") +public class PowerAuthenticator implements Authenticator { + + private boolean allowDollar; // allow $ in topic + private String deniedTopic; // topic will be rejected + protected MongoStorage mongoStorage; + + @Override + public void init(AbstractConfiguration config) { + this.allowDollar = config.getBoolean("allowDollar", true); + this.deniedTopic = config.getString("deniedTopic", null); + } + + @Override + public void destroy() { + } + + @Override + public AuthorizeResult authConnect(String clientId, String userName, String password) { + if(mongoStorage.isAgentExists(clientId)) { + return AuthorizeResult.OK; + } + return AuthorizeResult.FORBIDDEN; + } + + @Override + public AuthorizeResult authPublish(String clientId, String userName, String topicName, int qos, boolean retain) { + if (!this.allowDollar && topicName.startsWith("$")) return AuthorizeResult.FORBIDDEN; + if (topicName.equals(this.deniedTopic)) return AuthorizeResult.FORBIDDEN; + return AuthorizeResult.OK; + } + + @Override + public List authSubscribe(String clientId, String userName, List requestSubscriptions) { + List r = new ArrayList<>(); + requestSubscriptions.forEach(subscription -> { + if (!this.allowDollar && subscription.topic().startsWith("$")) r.add(MqttGrantedQoS.FAILURE); + if (subscription.topic().equals(this.deniedTopic)) r.add(MqttGrantedQoS.FAILURE); + r.add(MqttGrantedQoS.valueOf(subscription.requestedQos().value())); + }); + return r; + } + + @Override + public String oauth(String credentials) { + return "dummy"; + } +} diff --git a/mithqtt-broker/build.gradle b/mithqtt-broker/build.gradle index a41f539..180fca8 100644 --- a/mithqtt-broker/build.gradle +++ b/mithqtt-broker/build.gradle @@ -11,6 +11,8 @@ dependencies { compile project(':mithqtt-api') compile project(':mithqtt-storage-redis') + compile project(':mithqtt-storage-mongo') + // authenticator runtime project(':mithqtt-authenticator-dummy') diff --git a/mithqtt-storage-mongo/build.gradle b/mithqtt-storage-mongo/build.gradle new file mode 100644 index 0000000..1b72f3d --- /dev/null +++ b/mithqtt-storage-mongo/build.gradle @@ -0,0 +1,5 @@ +dependencies { + // mongodb + compile 'org.mongodb:mongodb-driver:3.2.2' + +} diff --git a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java new file mode 100644 index 0000000..174cd2c --- /dev/null +++ b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java @@ -0,0 +1,124 @@ +package io.j1st.power.storage.mongo; + +import com.mongodb.MongoClient; +import com.mongodb.MongoCredential; +import com.mongodb.ServerAddress; +import com.mongodb.client.AggregateIterable; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.UpdateOptions; +import io.j1st.power.storage.mongo.entity.Agent; +import io.j1st.power.storage.mongo.entity.Permission; +import org.apache.commons.configuration.AbstractConfiguration; +import org.apache.commons.lang3.RandomStringUtils; +import org.bson.Document; +import org.bson.types.ObjectId; + +import java.util.*; +import java.util.function.Consumer; + +import static com.mongodb.client.model.Accumulators.sum; +import static com.mongodb.client.model.Aggregates.group; +import static com.mongodb.client.model.Aggregates.match; +import static com.mongodb.client.model.Filters.*; +import static com.mongodb.client.model.Filters.eq; +import static com.mongodb.client.model.Filters.exists; +import static com.mongodb.client.model.Projections.include; +import static com.mongodb.client.model.Sorts.ascending; +import static com.mongodb.client.model.Sorts.descending; + + +/** + * Created by Administrator on 2016/4/27. + */ +public class MongoStorage { + + protected MongoClient client; + protected MongoDatabase database; + + public void init(AbstractConfiguration config) { + // MongoClient + List addresses = parseAddresses(config.getString("mongo.address")); + List credentials = parseCredentials( + config.getString("mongo.userName"), + "admin", + config.getString("mongo.password")); + if (addresses.size() == 1) { + this.client = new MongoClient(addresses.get(0), credentials); + } else { + this.client = new MongoClient(addresses, credentials); + } + this.database = this.client.getDatabase(config.getString("mongo.database")); + } + + + public void destroy() { + if (this.client != null) this.client.close(); + } + + private ServerAddress parseAddress(String address) { + int idx = address.indexOf(':'); + return (idx == -1) ? + new ServerAddress(address) : + new ServerAddress(address.substring(0, idx), Integer.parseInt(address.substring(idx + 1))); + } + + private List parseAddresses(String addresses) { + List result = new ArrayList<>(); + String[] addrs = addresses.split(" *, *"); + for (String addr : addrs) { + result.add(parseAddress(addr)); + } + return result; + } + + private List parseCredentials(String userName, String database, String password) { + List result = new ArrayList<>(); + result.add(MongoCredential.createCredential(userName, database, password.toCharArray())); + return result; + } + + + /* =========================================== Agent Operations ===============================================*/ + + + /** + * 判断 采集器 权限 + * + * @param id 采集器Id + * @param permission 最低权限 + * @return True 权限满足 + */ + public boolean isAgentdByUser(String id, Permission permission) { + return this.database.getCollection("agents") + .find(and(eq("_id", new ObjectId(id)), eq("permissions", new Document("$elemMatch", new Document() + .append("user_id", permission.getUserId()))))) + .first() != null; + } + + + /** + * 判断Agent是否存在 + * + * @param id 采集器Id + * @return 采集器 or Null + */ + public boolean isAgentExists(String id) { + return this.database.getCollection("agents") + .find(eq("_id", new ObjectId(id))).first() != null ; + } + + /** + * 获取 产品 是否被激活 + * 激活的定义为:旗下采集器至少有一个被激活 + * + * @param productId 产品Id + * @return True 被激活 + */ + public boolean isProductActivated(String productId) { + return this.database.getCollection("agents") + .find(and(eq("product_id", new ObjectId(productId)), exists("activated_at", true))) + .projection(include("_id")) + .first() != null; + + } +} diff --git a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/Agent.java b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/Agent.java new file mode 100644 index 0000000..7a13d79 --- /dev/null +++ b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/Agent.java @@ -0,0 +1,113 @@ +package io.j1st.power.storage.mongo.entity; + +import org.bson.types.ObjectId; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * Agent + */ +public class Agent { + + private ObjectId id; + private ObjectId productId; + private String name; + private String token; + private boolean connected; + private long msgCount; + private long msgSizeSum; + private Map attributes; + private List permissions; + private Date activatedAt; + private Date updatedAt; + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public ObjectId getProductId() { + return productId; + } + + public void setProductId(ObjectId productId) { + this.productId = productId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public boolean isConnected() { + return connected; + } + + public void setConnected(boolean connected) { + this.connected = connected; + } + + public long getMsgCount() { + return msgCount; + } + + public void setMsgCount(long msgCount) { + this.msgCount = msgCount; + } + + public long getMsgSizeSum() { + return msgSizeSum; + } + + public void setMsgSizeSum(long msgSizeSum) { + this.msgSizeSum = msgSizeSum; + } + + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + public List getPermissions() { + return permissions; + } + + public void setPermissions(List permissions) { + this.permissions = permissions; + } + + public Date getActivatedAt() { + return activatedAt; + } + + public void setActivatedAt(Date activatedAt) { + this.activatedAt = activatedAt; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/Permission.java b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/Permission.java new file mode 100644 index 0000000..7ae20be --- /dev/null +++ b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/Permission.java @@ -0,0 +1,28 @@ +package io.j1st.power.storage.mongo.entity; + +import org.bson.types.ObjectId; + +/** + * Permission + */ +public class Permission { + + private ObjectId userId; + private PermissionLevel level; + + public ObjectId getUserId() { + return userId; + } + + public void setUserId(ObjectId userId) { + this.userId = userId; + } + + public PermissionLevel getLevel() { + return level; + } + + public void setLevel(PermissionLevel level) { + this.level = level; + } +} diff --git a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/PermissionLevel.java b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/PermissionLevel.java new file mode 100644 index 0000000..eb082d7 --- /dev/null +++ b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/PermissionLevel.java @@ -0,0 +1,29 @@ +package io.j1st.power.storage.mongo.entity; + +/** + * Permission Level + */ +public enum PermissionLevel { + OWNER(9), + READ_WRITE(6), + READ(3); + + private final int value; + + PermissionLevel(int value) { + this.value = value; + } + + public static PermissionLevel valueOf(int value) { + for (PermissionLevel l : values()) { + if (l.value == value) { + return l; + } + } + throw new IllegalArgumentException("invalid permission level " + value); + } + + public int value() { + return value; + } +} diff --git a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/User.java b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/User.java new file mode 100644 index 0000000..1104eea --- /dev/null +++ b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/User.java @@ -0,0 +1,84 @@ +package io.j1st.power.storage.mongo.entity; + +import org.bson.types.ObjectId; + +import java.util.Date; + +/** + * User + */ +public class User { + + private ObjectId id; + private String name; + private String password; + private String token; + private String email; + private String mobile; + private UserRole role; + private Date updatedAt; + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getMobile() { + return mobile; + } + + public void setMobile(String mobile) { + this.mobile = mobile; + } + + public UserRole getRole() { + return role; + } + + public void setRole(UserRole role) { + this.role = role; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/UserRole.java b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/UserRole.java new file mode 100644 index 0000000..7fb9fb5 --- /dev/null +++ b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/UserRole.java @@ -0,0 +1,28 @@ +package io.j1st.power.storage.mongo.entity; + +/** + * User Role + */ +public enum UserRole { + DEVELOPER(1), + PLAYER(2); + + private final int value; + + UserRole(int value) { + this.value = value; + } + + public static UserRole valueOf(int value) { + for (UserRole r : values()) { + if (r.value == value) { + return r; + } + } + throw new IllegalArgumentException("invalid user role: " + value); + } + + public int value() { + return value; + } +} diff --git a/settings.gradle b/settings.gradle index 148d2f5..8c65720 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,4 +9,5 @@ include 'mithqtt-communicator-kafka' include 'mithqtt-communicator-rabbitmq' include 'mithqtt-authenticator-dummy' include 'mithqtt-metrics-influxdb' +include 'mithqtt-storage-mongo' From cb75bb502d7cdb899b56ce60ed33606a664574b3 Mon Sep 17 00:00:00 2001 From: wuyiadmin Date: Wed, 27 Apr 2016 14:58:55 +0800 Subject: [PATCH 02/18] =?UTF-8?q?=E5=A2=9E=E5=8A=A0mongo=E7=9A=84=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E5=9F=BA=E7=A1=80=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../authenticator/power/PowerAuthenticator.java | 2 ++ .../io/j1st/power/storage/mongo/MongoStorage.java | 11 ----------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java index 514ee80..7e10b7d 100644 --- a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java +++ b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java @@ -25,6 +25,8 @@ public class PowerAuthenticator implements Authenticator { public void init(AbstractConfiguration config) { this.allowDollar = config.getBoolean("allowDollar", true); this.deniedTopic = config.getString("deniedTopic", null); + mongoStorage = new MongoStorage(); + mongoStorage.init(config); } @Override diff --git a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java index 174cd2c..863ab20 100644 --- a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java +++ b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java @@ -3,28 +3,17 @@ import com.mongodb.MongoClient; import com.mongodb.MongoCredential; import com.mongodb.ServerAddress; -import com.mongodb.client.AggregateIterable; import com.mongodb.client.MongoDatabase; -import com.mongodb.client.model.UpdateOptions; -import io.j1st.power.storage.mongo.entity.Agent; import io.j1st.power.storage.mongo.entity.Permission; import org.apache.commons.configuration.AbstractConfiguration; -import org.apache.commons.lang3.RandomStringUtils; import org.bson.Document; import org.bson.types.ObjectId; import java.util.*; -import java.util.function.Consumer; - -import static com.mongodb.client.model.Accumulators.sum; -import static com.mongodb.client.model.Aggregates.group; -import static com.mongodb.client.model.Aggregates.match; import static com.mongodb.client.model.Filters.*; import static com.mongodb.client.model.Filters.eq; import static com.mongodb.client.model.Filters.exists; import static com.mongodb.client.model.Projections.include; -import static com.mongodb.client.model.Sorts.ascending; -import static com.mongodb.client.model.Sorts.descending; /** From 045f59a6e12b5102d935d1d5427e4a1a266aeb88 Mon Sep 17 00:00:00 2001 From: wuyiadmin Date: Thu, 28 Apr 2016 16:55:58 +0800 Subject: [PATCH 03/18] =?UTF-8?q?=E8=B0=83=E6=95=B4broker=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E7=9A=84=E9=85=8D=E7=BD=AE=E6=9D=83=E9=99=90=E4=B8=AD?= =?UTF-8?q?=E5=8A=A0=E5=85=A5mongo=E7=9A=84=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mithqtt-authenticator-dummy/build.gradle | 2 ++ .../power/PowerAuthenticator.java | 25 ++++++++++++++++--- .../src/dist/config/authenticator.properties | 2 +- .../src/dist/config/mongo.properties | 13 ++++++++++ .../mithqtt/broker/MqttBroker.java | 7 +++++- .../power/storage/mongo/MongoStorage.java | 12 +++++++++ 6 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 mithqtt-broker/src/dist/config/mongo.properties diff --git a/mithqtt-authenticator-dummy/build.gradle b/mithqtt-authenticator-dummy/build.gradle index 93d4815..1de3c69 100644 --- a/mithqtt-authenticator-dummy/build.gradle +++ b/mithqtt-authenticator-dummy/build.gradle @@ -1,4 +1,6 @@ dependencies { // project api compile project(':mithqtt-api') + + compile project(':mithqtt-storage-mongo') } \ No newline at end of file diff --git a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java index 7e10b7d..63d0cff 100644 --- a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java +++ b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java @@ -6,6 +6,10 @@ import io.netty.handler.codec.mqtt.MqttGrantedQoS; import io.netty.handler.codec.mqtt.MqttTopicSubscription; import org.apache.commons.configuration.AbstractConfiguration; +import org.apache.commons.configuration.ConfigurationException; +import org.apache.commons.configuration.PropertiesConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; @@ -17,6 +21,8 @@ @SuppressWarnings("unused") public class PowerAuthenticator implements Authenticator { + Logger logger = LoggerFactory.getLogger(PowerAuthenticator.class); + private boolean allowDollar; // allow $ in topic private String deniedTopic; // topic will be rejected protected MongoStorage mongoStorage; @@ -27,6 +33,7 @@ public void init(AbstractConfiguration config) { this.deniedTopic = config.getString("deniedTopic", null); mongoStorage = new MongoStorage(); mongoStorage.init(config); + } @Override @@ -35,16 +42,28 @@ public void destroy() { @Override public AuthorizeResult authConnect(String clientId, String userName, String password) { - if(mongoStorage.isAgentExists(clientId)) { - return AuthorizeResult.OK; + //验证clentId是否有效 + if(!mongoStorage.isAgentExists(clientId)) { + return AuthorizeResult.FORBIDDEN; + } + //验证用户名密码是否合法 + if(!mongoStorage.isAgentAuth(userName, password)) { + return AuthorizeResult.FORBIDDEN; } - return AuthorizeResult.FORBIDDEN; + return AuthorizeResult.OK; } @Override public AuthorizeResult authPublish(String clientId, String userName, String topicName, int qos, boolean retain) { if (!this.allowDollar && topicName.startsWith("$")) return AuthorizeResult.FORBIDDEN; if (topicName.equals(this.deniedTopic)) return AuthorizeResult.FORBIDDEN; + //判断topic是否包括自己的clientId + if(topicName.indexOf(clientId) == -1){ + return AuthorizeResult.FORBIDDEN; + } + if(!topicName.endsWith("upstream")){ + return AuthorizeResult.FORBIDDEN; + } return AuthorizeResult.OK; } diff --git a/mithqtt-broker/src/dist/config/authenticator.properties b/mithqtt-broker/src/dist/config/authenticator.properties index 198cb2f..1d1949b 100644 --- a/mithqtt-broker/src/dist/config/authenticator.properties +++ b/mithqtt-broker/src/dist/config/authenticator.properties @@ -3,7 +3,7 @@ # Authenticator # Authenticator implementation (full qualified class name) -authenticator.class = com.github.longkerdandy.mithqtt.authenticator.dummy.DummyAuthenticator +authenticator.class = io.j1st.mithqtt.authenticator.power.PowerAuthenticator # Dummy diff --git a/mithqtt-broker/src/dist/config/mongo.properties b/mithqtt-broker/src/dist/config/mongo.properties new file mode 100644 index 0000000..e386e48 --- /dev/null +++ b/mithqtt-broker/src/dist/config/mongo.properties @@ -0,0 +1,13 @@ +# Redis storage configuration + +# Storage + +# Storage implementation (full qualified class name) +storage.class = io.j1st.power.storage.mongo.MongoStorage + + +# MongoDB storage configuration +mongo.address = 139.198.0.174:27019 +mongo.database = power +mongo.userName = root +mongo.password = zxcvASDF123$ \ No newline at end of file diff --git a/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/MqttBroker.java b/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/MqttBroker.java index b8c62bf..6555995 100644 --- a/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/MqttBroker.java +++ b/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/MqttBroker.java @@ -12,6 +12,7 @@ import com.github.longkerdandy.mithqtt.broker.session.SessionRegistry; import com.github.longkerdandy.mithqtt.broker.util.Validator; import com.lambdaworks.redis.ValueScanCursor; +import io.j1st.power.storage.mongo.MongoStorage; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; import io.netty.channel.epoll.EpollEventLoopGroup; @@ -52,18 +53,21 @@ public static void main(String[] args) throws Exception { PropertiesConfiguration communicatorConfig; PropertiesConfiguration authenticatorConfig; PropertiesConfiguration metricsConfig; + PropertiesConfiguration mongodbConfig; if (args.length >= 5) { brokerConfig = new PropertiesConfiguration(args[0]); redisConfig = new PropertiesConfiguration(args[1]); communicatorConfig = new PropertiesConfiguration(args[2]); authenticatorConfig = new PropertiesConfiguration(args[3]); metricsConfig = new PropertiesConfiguration(args[4]); + mongodbConfig = new PropertiesConfiguration(args[5]); } else { brokerConfig = new PropertiesConfiguration("config/broker.properties"); redisConfig = new PropertiesConfiguration("config/redis.properties"); communicatorConfig = new PropertiesConfiguration("config/communicator.properties"); authenticatorConfig = new PropertiesConfiguration("config/authenticator.properties"); metricsConfig = new PropertiesConfiguration("config/metrics.properties"); + mongodbConfig = new PropertiesConfiguration("config/mongo.properties"); } final String brokerId = brokerConfig.getString("broker.id"); @@ -93,7 +97,7 @@ public static void main(String[] args) throws Exception { // authenticator logger.debug("Initializing authenticator..."); Authenticator authenticator = (Authenticator) Class.forName(authenticatorConfig.getString("authenticator.class")).newInstance(); - authenticator.init(authenticatorConfig); + authenticator.init(mongodbConfig); // metrics logger.debug("Initializing metrics ..."); @@ -101,6 +105,7 @@ public static void main(String[] args) throws Exception { MetricsService metrics = metricsEnabled ? (MetricsService) Class.forName(metricsConfig.getString("metrics.class")).newInstance() : null; if (metricsEnabled) metrics.init(metricsConfig); + // broker final int keepAlive = brokerConfig.getInt("mqtt.keepalive.default"); final int keepAliveMax = brokerConfig.getInt("mqtt.keepalive.max"); diff --git a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java index 863ab20..a489d01 100644 --- a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java +++ b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java @@ -96,6 +96,18 @@ public boolean isAgentExists(String id) { .find(eq("_id", new ObjectId(id))).first() != null ; } + + /** + * 判断Agent是否存在 + * + * @param userName 采集器Id + * @return 采集器 or Null + */ + public boolean isAgentAuth(String userName , String password) { + return this.database.getCollection("agents") + .find(and(eq("_id", new ObjectId(userName)),eq("token", password))).first() != null ; + } + /** * 获取 产品 是否被激活 * 激活的定义为:旗下采集器至少有一个被激活 From 2838f03a153c3c78a0ed5a2f43646a50dc867260 Mon Sep 17 00:00:00 2001 From: wuyiadmin Date: Wed, 4 May 2016 15:14:48 +0800 Subject: [PATCH 04/18] =?UTF-8?q?=E6=B7=BB=E5=8A=A0broker=E5=AF=B9ssl?= =?UTF-8?q?=E7=9A=84=E6=94=AF=E6=8C=81=EF=BC=8C=E6=94=BE=E5=85=A5=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E8=AF=81=E4=B9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/dist/config/authenticator.properties | 4 +- .../src/dist/config/broker.properties | 9 +-- .../src/dist/config/mongo.properties | 12 +++- mithqtt-broker/src/dist/ssl/server.jks | Bin 0 -> 3209 bytes .../mithqtt/broker/MqttBroker.java | 68 ++++++++++++++++-- mithqtt-http/src/dist/config/http.yml | 6 +- .../longkerdandy/mithqtt/http/MqttHttp.java | 1 + .../http/resources/MqttPublishResource.java | 3 +- 8 files changed, 83 insertions(+), 20 deletions(-) create mode 100644 mithqtt-broker/src/dist/ssl/server.jks diff --git a/mithqtt-broker/src/dist/config/authenticator.properties b/mithqtt-broker/src/dist/config/authenticator.properties index 1d1949b..8fbc8e2 100644 --- a/mithqtt-broker/src/dist/config/authenticator.properties +++ b/mithqtt-broker/src/dist/config/authenticator.properties @@ -3,7 +3,9 @@ # Authenticator # Authenticator implementation (full qualified class name) -authenticator.class = io.j1st.mithqtt.authenticator.power.PowerAuthenticator +#com.github.longkerdandy.mithqtt.authenticator.dummy.DummyAuthenticator + #io.j1st.mithqtt.authenticator.power.PowerAuthenticator +authenticator.class =io.j1st.mithqtt.authenticator.power.PowerAuthenticator # Dummy diff --git a/mithqtt-broker/src/dist/config/broker.properties b/mithqtt-broker/src/dist/config/broker.properties index 1c74bc4..2a64147 100644 --- a/mithqtt-broker/src/dist/config/broker.properties +++ b/mithqtt-broker/src/dist/config/broker.properties @@ -16,20 +16,17 @@ mqtt.port = 1883 # To use ssl in the connection, set this to true # Must provide an X.509 certificate chain file in PEM format # Must provide a PKCS#8 private key file in PEM format -mqtt.ssl.enabled = false +mqtt.ssl.enabled = true # This is the network port the broker will bind to when ssl is used # The MQTT Protocol Specification recommended using port 8883 mqtt.ssl.port = 8883 # X.509 certificate chain file path -# mqtt.ssl.certPath = - -# A PKCS#8 private key file path -# mqtt.ssl.keyPath = +mqtt.ssl.certPath =ssl/server.jks # The password of the key File -# mqtt.ssl.keyPassword = +mqtt.ssl.password = zenintec # These are the default and maximum time interval that client is permitted to be idled # Time interval measured in seconds diff --git a/mithqtt-broker/src/dist/config/mongo.properties b/mithqtt-broker/src/dist/config/mongo.properties index e386e48..c8bea7f 100644 --- a/mithqtt-broker/src/dist/config/mongo.properties +++ b/mithqtt-broker/src/dist/config/mongo.properties @@ -7,7 +7,15 @@ storage.class = io.j1st.power.storage.mongo.MongoStorage # MongoDB storage configuration -mongo.address = 139.198.0.174:27019 +mongo.address = 139.198.0.174:27011 mongo.database = power mongo.userName = root -mongo.password = zxcvASDF123$ \ No newline at end of file +mongo.password = zxcvASDF123$ + + + +# Allow '$' in topic +allowDollar = false + +# Topic will be rejected in PUBLISH and SUBSCRIBE +deniedTopic = nosubscribe \ No newline at end of file diff --git a/mithqtt-broker/src/dist/ssl/server.jks b/mithqtt-broker/src/dist/ssl/server.jks new file mode 100644 index 0000000000000000000000000000000000000000..0cce162b54ddfac00cf5648cfa6bcce14cc4e478 GIT binary patch literal 3209 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 P() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + // ssl + p.addLast("ssl", sslContext.newHandler(ch.alloc())); + + // idle + p.addFirst("idleHandler", new IdleStateHandler(0, 0, keepAlive)); + // metrics + if (metricsEnabled) { + p.addLast("bytesMetrics", new BytesMetricsHandler(metrics, brokerId)); + } + // mqtt encoder & decoder + p.addLast("encoder", new MqttEncoder()); + p.addLast("decoder", new MqttDecoder()); + // metrics + if (metricsEnabled) { + p.addLast("msgMetrics", new MessageMetricsHandler(metrics, brokerId)); + } + // logic handler + p.addLast(handlerGroup, "logicHandler", new SyncRedisHandler(authenticator, communicator, redis, registry, validator, brokerId, keepAlive, keepAliveMax)); + } + }) + .option(ChannelOption.SO_BACKLOG, brokerConfig.getInt("netty.soBacklog")) + .childOption(ChannelOption.SO_KEEPALIVE, brokerConfig.getBoolean("netty.soKeepAlive")); + + ChannelFuture sf = sslb.bind(host, sslport).sync(); + + } + logger.info("MQTT broker is up and running."); // Wait until the server socket is closed. // Do this to gracefully shut down the server. f.channel().closeFuture().sync(); + } /** diff --git a/mithqtt-http/src/dist/config/http.yml b/mithqtt-http/src/dist/config/http.yml index e92fc18..5752ca3 100644 --- a/mithqtt-http/src/dist/config/http.yml +++ b/mithqtt-http/src/dist/config/http.yml @@ -8,9 +8,9 @@ serverId: 1 # These are regexp validator for MQTT packet field # Fields will validate against the regexp whenever a related request is received # Leave empty to skip the validation -clientIdValidator = ^[ -~]+$ -topicNameValidator = ^[ -~]+$ -topicFilterValidator = ^[ -~]+$ +clientIdValidator : ^[ -~]+$ +topicNameValidator : ^[ -~]+$ +topicFilterValidator : ^[ -~]+$ # DropWizard diff --git a/mithqtt-http/src/main/java/com/github/longkerdandy/mithqtt/http/MqttHttp.java b/mithqtt-http/src/main/java/com/github/longkerdandy/mithqtt/http/MqttHttp.java index 4e43b75..a27fdb2 100644 --- a/mithqtt-http/src/main/java/com/github/longkerdandy/mithqtt/http/MqttHttp.java +++ b/mithqtt-http/src/main/java/com/github/longkerdandy/mithqtt/http/MqttHttp.java @@ -147,6 +147,7 @@ public void stop() throws Exception { environment.jersey().register(new MqttSubscribeResource(configuration.getServerId(), validator, redis, communicator, authenticator, metrics)); environment.jersey().register(new MqttUnsubscribeResource(configuration.getServerId(), validator, redis, communicator, authenticator, metrics)); + // config jackson environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); environment.getObjectMapper().configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); diff --git a/mithqtt-http/src/main/java/com/github/longkerdandy/mithqtt/http/resources/MqttPublishResource.java b/mithqtt-http/src/main/java/com/github/longkerdandy/mithqtt/http/resources/MqttPublishResource.java index 293b82a..2fbec06 100644 --- a/mithqtt-http/src/main/java/com/github/longkerdandy/mithqtt/http/resources/MqttPublishResource.java +++ b/mithqtt-http/src/main/java/com/github/longkerdandy/mithqtt/http/resources/MqttPublishResource.java @@ -50,7 +50,7 @@ public MqttPublishResource(String serverId, Validator validator, RedisSyncStorag public ResultEntity publish(@PathParam("clientId") String clientId, @Auth UserPrincipal user, @QueryParam("protocol") @DefaultValue("4") byte protocol, @QueryParam("dup") @DefaultValue("false") boolean dup, @QueryParam("qos") @DefaultValue("0") int qos, @QueryParam("topicName") String topicName, @QueryParam("packetId") @DefaultValue("0") int packetId, - String body) throws UnsupportedEncodingException { + String body) throws UnsupportedEncodingException { String userName = user.getName(); MqttVersion version = MqttVersion.fromProtocolLevel(protocol); byte[] payload = body == null ? null : body.getBytes("ISO-8859-1"); @@ -147,4 +147,5 @@ public ResultEntity publish(@PathParam("clientId") String clientId, @Au throw new AuthorizeException(new ErrorEntity(ErrorCode.UNAUTHORIZED)); } } + } From bf3f53c602f543c00b3977cd5c00c2099ee12c69 Mon Sep 17 00:00:00 2001 From: wuyiadmin Date: Mon, 30 May 2016 15:10:21 +0800 Subject: [PATCH 05/18] =?UTF-8?q?=E4=BF=AE=E6=94=B9mqtt=E7=9A=84=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=EF=BC=8Chttp=E7=9B=B8=E5=85=B3=E4=B8=8B=E5=8F=91?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=8E=A5=E5=8F=A3=E7=9A=84=E8=B0=83=E6=95=B4?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E6=95=B0=E6=8D=AE=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dummy/DummyAuthenticator.java | 2 +- .../power/PowerAuthenticator.java | 14 +++++----- .../src/dist/config/broker.properties | 2 +- .../src/dist/config/communicator.properties | 27 ++++++++++++++++--- .../mithqtt/broker/MqttBroker.java | 4 +-- .../src/dist/config/communicator.properties | 27 ++++++++++++++++--- mithqtt-http/src/dist/config/http.yml | 22 ++++++++++++++- .../http/resources/MqttPublishResource.java | 2 +- .../power/storage/mongo/MongoStorage.java | 18 +++++++++++++ 9 files changed, 99 insertions(+), 19 deletions(-) diff --git a/mithqtt-authenticator-dummy/src/main/java/com/github/longkerdandy/mithqtt/authenticator/dummy/DummyAuthenticator.java b/mithqtt-authenticator-dummy/src/main/java/com/github/longkerdandy/mithqtt/authenticator/dummy/DummyAuthenticator.java index 108e1f1..3a3d5ab 100644 --- a/mithqtt-authenticator-dummy/src/main/java/com/github/longkerdandy/mithqtt/authenticator/dummy/DummyAuthenticator.java +++ b/mithqtt-authenticator-dummy/src/main/java/com/github/longkerdandy/mithqtt/authenticator/dummy/DummyAuthenticator.java @@ -54,6 +54,6 @@ public List authSubscribe(String clientId, String userName, List @Override public String oauth(String credentials) { - return "dummy"; + return credentials; } } diff --git a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java index 63d0cff..847c4d5 100644 --- a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java +++ b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java @@ -58,12 +58,12 @@ public AuthorizeResult authPublish(String clientId, String userName, String topi if (!this.allowDollar && topicName.startsWith("$")) return AuthorizeResult.FORBIDDEN; if (topicName.equals(this.deniedTopic)) return AuthorizeResult.FORBIDDEN; //判断topic是否包括自己的clientId - if(topicName.indexOf(clientId) == -1){ - return AuthorizeResult.FORBIDDEN; - } - if(!topicName.endsWith("upstream")){ - return AuthorizeResult.FORBIDDEN; - } +// if(topicName.indexOf(clientId) == -1){ +// return AuthorizeResult.FORBIDDEN; +// } +// if(!topicName.endsWith("upstream")){ +// return AuthorizeResult.FORBIDDEN; +// } return AuthorizeResult.OK; } @@ -80,6 +80,6 @@ public List authSubscribe(String clientId, String userName, List @Override public String oauth(String credentials) { - return "dummy"; + return mongoStorage.getUserByToken(credentials); } } diff --git a/mithqtt-broker/src/dist/config/broker.properties b/mithqtt-broker/src/dist/config/broker.properties index 2a64147..4ac7fd5 100644 --- a/mithqtt-broker/src/dist/config/broker.properties +++ b/mithqtt-broker/src/dist/config/broker.properties @@ -23,7 +23,7 @@ mqtt.ssl.enabled = true mqtt.ssl.port = 8883 # X.509 certificate chain file path -mqtt.ssl.certPath =ssl/server.jks +mqtt.ssl.certPath =E://SSL/ze/server.jks # The password of the key File mqtt.ssl.password = zenintec diff --git a/mithqtt-broker/src/dist/config/communicator.properties b/mithqtt-broker/src/dist/config/communicator.properties index 1251612..17c8957 100644 --- a/mithqtt-broker/src/dist/config/communicator.properties +++ b/mithqtt-broker/src/dist/config/communicator.properties @@ -1,9 +1,9 @@ -# Hazelcast communicator configuration +# RabbitMQ communicator configuration # Communicator # Communicator implementation (full qualified class name) -communicator.class = com.github.longkerdandy.mithqtt.communicator.hazelcast.broker.HazelcastBrokerCommunicator +communicator.class = com.github.longkerdandy.mithqtt.communicator.rabbitmq.broker.RabbitMQBrokerCommunicator # This is the topic prefix that broker instance consume. (full topic is like mithqtt.broker.{brokerId}) communicator.broker.topic = mithqtt.broker @@ -11,6 +11,27 @@ communicator.broker.topic = mithqtt.broker # This is the topic that processor will pass message to 3rd party application communicator.application.topic = mithqtt.application -# Hazelcast +# RabbitMQ + +# User name +rabbitmq.userName = guest + +# Password +rabbitmq.password = guest + +# Virtual host +rabbitmq.virtualHost = / + +# Server addresses +# In the format like host1[:port1],host2[:port2] +rabbitmq.addresses = localhost + +# Queue name for application communicator +# ONLY APPLIES TO RabbitMQApplicationCommunicator +rabbitmq.app.queueName = appQueue + +# Routing key for application communicator +# ONLY APPLIES TO RabbitMQApplicationCommunicator +rabbitmq.app.routingKey = # diff --git a/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/MqttBroker.java b/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/MqttBroker.java index 7628ee1..c4a6794 100644 --- a/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/MqttBroker.java +++ b/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/MqttBroker.java @@ -186,8 +186,8 @@ public void initChannel(SocketChannel ch) throws Exception { KeyStore ks = KeyStore.getInstance("JKS"); ks.load(new FileInputStream(new File(brokerConfig.getString("mqtt.ssl.certPath"))), password.toCharArray()); -// TrustManagerFactory tmFactory = TrustManagerFactory.getInstance("SunX509"); -// tmFactory.init(ks); + TrustManagerFactory tmFactory = TrustManagerFactory.getInstance("SunX509"); + tmFactory.init(ks); KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(ks, password.toCharArray()); diff --git a/mithqtt-http/src/dist/config/communicator.properties b/mithqtt-http/src/dist/config/communicator.properties index cba8d3f..469bbb4 100644 --- a/mithqtt-http/src/dist/config/communicator.properties +++ b/mithqtt-http/src/dist/config/communicator.properties @@ -1,9 +1,9 @@ -# Hazelcast communicator configuration +# RabbitMQ communicator configuration # Communicator # Communicator implementation (full qualified class name) -communicator.class = com.github.longkerdandy.mithqtt.communicator.hazelcast.http.HazelcastHttpCommunicator +communicator.class = com.github.longkerdandy.mithqtt.communicator.rabbitmq.http.RabbitMQHttpCommunicator # This is the topic prefix that broker instance consume. (full topic is like mithqtt.broker.{brokerId}) communicator.broker.topic = mithqtt.broker @@ -11,6 +11,27 @@ communicator.broker.topic = mithqtt.broker # This is the topic that processor will pass message to 3rd party application communicator.application.topic = mithqtt.application -# Hazelcast +# RabbitMQ + +# User name +rabbitmq.userName = guest + +# Password +rabbitmq.password = guest + +# Virtual host +rabbitmq.virtualHost = / + +# Server addresses +# In the format like host1[:port1],host2[:port2] +rabbitmq.addresses = localhost + +# Queue name for application communicator +# ONLY APPLIES TO RabbitMQApplicationCommunicator +rabbitmq.app.queueName = appQueue + +# Routing key for application communicator +# ONLY APPLIES TO RabbitMQApplicationCommunicator +rabbitmq.app.routingKey = # diff --git a/mithqtt-http/src/dist/config/http.yml b/mithqtt-http/src/dist/config/http.yml index 5752ca3..fa4a042 100644 --- a/mithqtt-http/src/dist/config/http.yml +++ b/mithqtt-http/src/dist/config/http.yml @@ -14,6 +14,26 @@ topicFilterValidator : ^[ -~]+$ # DropWizard +logging: + level: INFO + loggers: + "mithqtt-http": DEBUG + appenders: + - type: console + threshold: ALL + target: stdout + timeZone: Asia/Shanghai + logFormat: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %c{20} - %msg%n %ex{full}" + - type: file + currentLogFilename: log/mqhttp.log + threshold: ALL + archive: true + archivedLogFilenamePattern: /log/http-%d.log + archivedFileCount: 5 + timeZone: Asia/Shanghai + logFormat: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %c{20} - %msg%n %ex{full}" + + server: type: simple @@ -21,4 +41,4 @@ server: adminContextPath: /admin connector: type: http - port: 8080 \ No newline at end of file + port: 8081 \ No newline at end of file diff --git a/mithqtt-http/src/main/java/com/github/longkerdandy/mithqtt/http/resources/MqttPublishResource.java b/mithqtt-http/src/main/java/com/github/longkerdandy/mithqtt/http/resources/MqttPublishResource.java index 2fbec06..79f690e 100644 --- a/mithqtt-http/src/main/java/com/github/longkerdandy/mithqtt/http/resources/MqttPublishResource.java +++ b/mithqtt-http/src/main/java/com/github/longkerdandy/mithqtt/http/resources/MqttPublishResource.java @@ -51,7 +51,7 @@ public ResultEntity publish(@PathParam("clientId") String clientId, @Au @QueryParam("dup") @DefaultValue("false") boolean dup, @QueryParam("qos") @DefaultValue("0") int qos, @QueryParam("topicName") String topicName, @QueryParam("packetId") @DefaultValue("0") int packetId, String body) throws UnsupportedEncodingException { - String userName = user.getName(); + String userName = clientId; MqttVersion version = MqttVersion.fromProtocolLevel(protocol); byte[] payload = body == null ? null : body.getBytes("ISO-8859-1"); diff --git a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java index a489d01..c8343b3 100644 --- a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java +++ b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java @@ -5,6 +5,7 @@ import com.mongodb.ServerAddress; import com.mongodb.client.MongoDatabase; import io.j1st.power.storage.mongo.entity.Permission; +import io.j1st.power.storage.mongo.entity.User; import org.apache.commons.configuration.AbstractConfiguration; import org.bson.Document; import org.bson.types.ObjectId; @@ -13,6 +14,7 @@ import static com.mongodb.client.model.Filters.*; import static com.mongodb.client.model.Filters.eq; import static com.mongodb.client.model.Filters.exists; +import static com.mongodb.client.model.Projections.exclude; import static com.mongodb.client.model.Projections.include; @@ -108,6 +110,22 @@ public boolean isAgentAuth(String userName , String password) { .find(and(eq("_id", new ObjectId(userName)),eq("token", password))).first() != null ; } + + /** + * 获取 用户信息,根据 Token + * + * @param token Token + * @return 用户信息 or Null + */ + public String getUserByToken(String token) { + Document d = this.database.getCollection("users") + .find(eq("token", token)) + .projection(exclude("password")) + .first(); + if (d == null) return null; + return d.getObjectId("_id").toString(); + } + /** * 获取 产品 是否被激活 * 激活的定义为:旗下采集器至少有一个被激活 From 5d1d54c7569d66ac87d8831c5e805da0044c27a8 Mon Sep 17 00:00:00 2001 From: wuyiadmin Date: Mon, 20 Jun 2016 10:10:50 +0800 Subject: [PATCH 06/18] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8E=88=E6=9D=83?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E6=97=B6userName=E4=B8=8D=E6=98=AFObjectId?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E6=97=A0=E8=BF=94=E5=9B=9E=E7=A0=81=E7=9A=84?= =?UTF-8?q?bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../netty/handler/codec/mqtt/MqttMessageFactory.java | 1 + .../io/j1st/power/storage/mongo/MongoStorage.java | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/mithqtt-api/src/main/java/io/netty/handler/codec/mqtt/MqttMessageFactory.java b/mithqtt-api/src/main/java/io/netty/handler/codec/mqtt/MqttMessageFactory.java index df7a3d3..a32f4cc 100644 --- a/mithqtt-api/src/main/java/io/netty/handler/codec/mqtt/MqttMessageFactory.java +++ b/mithqtt-api/src/main/java/io/netty/handler/codec/mqtt/MqttMessageFactory.java @@ -35,6 +35,7 @@ public static MqttMessage newMessage(MqttFixedHeader mqttFixedHeader, Object var (MqttConnectVariableHeader) variableHeader, (MqttConnectPayload) payload); + case CONNACK: return new MqttConnAckMessage(mqttFixedHeader, (MqttConnAckVariableHeader) variableHeader); diff --git a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java index c8343b3..270346f 100644 --- a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java +++ b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java @@ -80,6 +80,9 @@ private List parseCredentials(String userName, String database, * @return True 权限满足 */ public boolean isAgentdByUser(String id, Permission permission) { + if(!ObjectId.isValid(id)){ + return false; + } return this.database.getCollection("agents") .find(and(eq("_id", new ObjectId(id)), eq("permissions", new Document("$elemMatch", new Document() .append("user_id", permission.getUserId()))))) @@ -94,6 +97,9 @@ public boolean isAgentdByUser(String id, Permission permission) { * @return 采集器 or Null */ public boolean isAgentExists(String id) { + if(!ObjectId.isValid(id)){ + return false; + } return this.database.getCollection("agents") .find(eq("_id", new ObjectId(id))).first() != null ; } @@ -106,6 +112,9 @@ public boolean isAgentExists(String id) { * @return 采集器 or Null */ public boolean isAgentAuth(String userName , String password) { + if(!ObjectId.isValid(userName)){ + return false; + } return this.database.getCollection("agents") .find(and(eq("_id", new ObjectId(userName)),eq("token", password))).first() != null ; } @@ -134,6 +143,9 @@ public String getUserByToken(String token) { * @return True 被激活 */ public boolean isProductActivated(String productId) { + if(!ObjectId.isValid(productId)){ + return false; + } return this.database.getCollection("agents") .find(and(eq("product_id", new ObjectId(productId)), exists("activated_at", true))) .projection(include("_id")) From 48de41c510e1f41827a1250a3f0e72c1f851764f Mon Sep 17 00:00:00 2001 From: wuyiadmin Date: Fri, 15 Jul 2016 16:28:49 +0800 Subject: [PATCH 07/18] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=AF=B9agent?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E6=B6=88=E6=81=AF=E5=92=8C=E8=AE=A2=E9=98=85?= =?UTF-8?q?=E7=9A=84topic=E9=99=90=E5=88=B6=EF=BC=8C=E9=98=B2=E6=AD=A2agen?= =?UTF-8?q?t=E4=B9=8B=E9=97=B4=E7=9B=B8=E4=BA=92=E9=80=9A=E8=AE=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../authenticator/power/PowerAuthenticator.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java index 847c4d5..6fe6882 100644 --- a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java +++ b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java @@ -58,12 +58,12 @@ public AuthorizeResult authPublish(String clientId, String userName, String topi if (!this.allowDollar && topicName.startsWith("$")) return AuthorizeResult.FORBIDDEN; if (topicName.equals(this.deniedTopic)) return AuthorizeResult.FORBIDDEN; //判断topic是否包括自己的clientId -// if(topicName.indexOf(clientId) == -1){ -// return AuthorizeResult.FORBIDDEN; -// } -// if(!topicName.endsWith("upstream")){ -// return AuthorizeResult.FORBIDDEN; -// } + if(topicName.indexOf(clientId) == -1){ + return AuthorizeResult.FORBIDDEN; + } + if(!topicName.endsWith("upstream")){ + return AuthorizeResult.FORBIDDEN; + } return AuthorizeResult.OK; } @@ -73,6 +73,7 @@ public List authSubscribe(String clientId, String userName, List requestSubscriptions.forEach(subscription -> { if (!this.allowDollar && subscription.topic().startsWith("$")) r.add(MqttGrantedQoS.FAILURE); if (subscription.topic().equals(this.deniedTopic)) r.add(MqttGrantedQoS.FAILURE); + if (!subscription.topic().endsWith("downstream")) r.add(MqttGrantedQoS.FAILURE); r.add(MqttGrantedQoS.valueOf(subscription.requestedQos().value())); }); return r; From c5291c5c0db4e329877e6b07cc3175020beb19ea Mon Sep 17 00:00:00 2001 From: wuyiadmin Date: Fri, 15 Jul 2016 17:46:48 +0800 Subject: [PATCH 08/18] =?UTF-8?q?=E8=B0=83=E6=95=B4=E9=99=90=E5=88=B6?= =?UTF-8?q?=E8=AE=A2=E9=98=85=E6=97=B6=E5=BF=85=E9=A1=BB=E5=8C=85=E6=8B=AC?= =?UTF-8?q?=E8=87=AA=E5=B7=B1=E7=9A=84agentId=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java | 1 + 1 file changed, 1 insertion(+) diff --git a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java index 6fe6882..bbb7551 100644 --- a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java +++ b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java @@ -74,6 +74,7 @@ public List authSubscribe(String clientId, String userName, List if (!this.allowDollar && subscription.topic().startsWith("$")) r.add(MqttGrantedQoS.FAILURE); if (subscription.topic().equals(this.deniedTopic)) r.add(MqttGrantedQoS.FAILURE); if (!subscription.topic().endsWith("downstream")) r.add(MqttGrantedQoS.FAILURE); + if (subscription.topic().indexOf(clientId) == -1) r.add(MqttGrantedQoS.FAILURE); r.add(MqttGrantedQoS.valueOf(subscription.requestedQos().value())); }); return r; From f57dea44401f4c92478534c8d40675d2a1a1f154 Mon Sep 17 00:00:00 2001 From: wuyiadmin Date: Thu, 21 Jul 2016 15:46:38 +0800 Subject: [PATCH 09/18] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BA=86=E5=AF=B9mqtt?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E5=92=8C=E8=AE=A2=E9=98=85=EF=BC=8C=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E6=B6=88=E6=81=AF=E7=9A=84=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../power/PowerAuthenticator.java | 11 +++++++ .../power/storage/mongo/MongoStorage.java | 32 +++++++++++++++++++ .../storage/mongo/entity/ProductStatus.java | 29 +++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/ProductStatus.java diff --git a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java index bbb7551..d57c38a 100644 --- a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java +++ b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java @@ -3,6 +3,7 @@ import com.github.longkerdandy.mithqtt.api.auth.Authenticator; import com.github.longkerdandy.mithqtt.api.auth.AuthorizeResult; import io.j1st.power.storage.mongo.MongoStorage; +import io.j1st.power.storage.mongo.entity.ProductStatus; import io.netty.handler.codec.mqtt.MqttGrantedQoS; import io.netty.handler.codec.mqtt.MqttTopicSubscription; import org.apache.commons.configuration.AbstractConfiguration; @@ -50,6 +51,11 @@ public AuthorizeResult authConnect(String clientId, String userName, String pass if(!mongoStorage.isAgentAuth(userName, password)) { return AuthorizeResult.FORBIDDEN; } + //验证product状态是否正常 + Integer status = this.mongoStorage.getProductStatusByAgentId(clientId); + if(status == null || !status.equals(ProductStatus.SERVICE.value())){ + return AuthorizeResult.FORBIDDEN; + } return AuthorizeResult.OK; } @@ -64,6 +70,11 @@ public AuthorizeResult authPublish(String clientId, String userName, String topi if(!topicName.endsWith("upstream")){ return AuthorizeResult.FORBIDDEN; } + //验证product状态是否正常 + Integer status = this.mongoStorage.getProductStatusByAgentId(clientId); + if(status == null || !status.equals(ProductStatus.SERVICE.value())){ + return AuthorizeResult.FORBIDDEN; + } return AuthorizeResult.OK; } diff --git a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java index 270346f..bfb05bb 100644 --- a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java +++ b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java @@ -4,6 +4,7 @@ import com.mongodb.MongoCredential; import com.mongodb.ServerAddress; import com.mongodb.client.MongoDatabase; +import io.j1st.power.storage.mongo.entity.Agent; import io.j1st.power.storage.mongo.entity.Permission; import io.j1st.power.storage.mongo.entity.User; import org.apache.commons.configuration.AbstractConfiguration; @@ -152,4 +153,35 @@ public boolean isProductActivated(String productId) { .first() != null; } + + + /** + * 获取 产品 是否被激活 + * 激活的定义为:旗下采集器至少有一个被激活 + * + * @param agentId + * @return True 被激活 + */ + public Integer getProductStatusByAgentId(String agentId) { + Integer status = null; + if(!ObjectId.isValid(agentId)){ + return null; + } + Document agentDocument = this.database.getCollection("agents") + .find(eq("_id", new ObjectId(agentId))) + .first(); + if(agentDocument != null) { + ObjectId productId = agentDocument.getObjectId("product_id"); + if(productId != null) { + Document productDocument = this.database.getCollection("products") + .find(eq("_id",productId)) + .first(); + if(productDocument != null) { + status = productDocument.getInteger("status"); + } + } + } + + return status; + } } diff --git a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/ProductStatus.java b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/ProductStatus.java new file mode 100644 index 0000000..44196ec --- /dev/null +++ b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/ProductStatus.java @@ -0,0 +1,29 @@ +package io.j1st.power.storage.mongo.entity; + +/** + * Product status description + */ +public enum ProductStatus { + SERVICE(1), //服务 + SUSPEND(2), //暂停 + ARREARS(3); //欠费 + + private final int value; + + ProductStatus(int value) { + this.value = value; + } + + public static ProductStatus valueOf(int value) { + for (ProductStatus r : values()) { + if (r.value == value) { + return r; + } + } + throw new IllegalArgumentException("invalid product status: " + value); + } + + public int value() { + return value; + } +} From db3fc41bd7490299683a3c99617cce2676bf6d3d Mon Sep 17 00:00:00 2001 From: xiaopeng <18971903450@163.com> Date: Mon, 25 Jul 2016 14:56:27 +0800 Subject: [PATCH 10/18] update push --- gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 0 bytes gradle/wrapper/gradle-wrapper.properties | 6 ------ .../src/dist/config/communicator.properties | 6 +++--- mithqtt-broker/src/dist/config/mongo.properties | 6 +++--- 4 files changed, 6 insertions(+), 12 deletions(-) delete mode 100644 gradle/wrapper/gradle-wrapper.jar delete mode 100644 gradle/wrapper/gradle-wrapper.properties diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index fd7e590e5154e82211909581e71018372134fb27..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53636 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⪚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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index edc91be..0000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Thu Mar 03 16:48:37 CST 2016 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-2.11-all.zip diff --git a/mithqtt-broker/src/dist/config/communicator.properties b/mithqtt-broker/src/dist/config/communicator.properties index 17c8957..0e1f7b9 100644 --- a/mithqtt-broker/src/dist/config/communicator.properties +++ b/mithqtt-broker/src/dist/config/communicator.properties @@ -14,17 +14,17 @@ communicator.application.topic = mithqtt.application # RabbitMQ # User name -rabbitmq.userName = guest +rabbitmq.userName = zenin # Password -rabbitmq.password = guest +rabbitmq.password = zenin # Virtual host rabbitmq.virtualHost = / # Server addresses # In the format like host1[:port1],host2[:port2] -rabbitmq.addresses = localhost +rabbitmq.addresses = 192.168.1.222 # Queue name for application communicator # ONLY APPLIES TO RabbitMQApplicationCommunicator diff --git a/mithqtt-broker/src/dist/config/mongo.properties b/mithqtt-broker/src/dist/config/mongo.properties index c8bea7f..84271b0 100644 --- a/mithqtt-broker/src/dist/config/mongo.properties +++ b/mithqtt-broker/src/dist/config/mongo.properties @@ -7,10 +7,10 @@ storage.class = io.j1st.power.storage.mongo.MongoStorage # MongoDB storage configuration -mongo.address = 139.198.0.174:27011 +mongo.address = localhost:27017 mongo.database = power -mongo.userName = root -mongo.password = zxcvASDF123$ +mongo.userName = accountUser +mongo.password = password From 887774361c545fc55d6dc60f291feea8ed0e29bc Mon Sep 17 00:00:00 2001 From: Cactus Date: Tue, 26 Jul 2016 10:48:30 +0800 Subject: [PATCH 11/18] modify config file in last merge --- mithqtt-broker/src/dist/config/broker.properties | 2 +- mithqtt-broker/src/dist/config/communicator.properties | 6 +++--- mithqtt-broker/src/dist/config/mongo.properties | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mithqtt-broker/src/dist/config/broker.properties b/mithqtt-broker/src/dist/config/broker.properties index 4ac7fd5..3ee3517 100644 --- a/mithqtt-broker/src/dist/config/broker.properties +++ b/mithqtt-broker/src/dist/config/broker.properties @@ -55,7 +55,7 @@ netty.useEpoll = false netty.soBacklog = 511 # this parameter configures the "TCP keepalive" behavior for the listening socket. -# If this parameter is omitted then the operating systems settings will be in effect for the socket. +# If this parameter is omitted then the operating system��s settings will be in effect for the socket. # If it is set to the value "true", the SO_KEEPALIVE option is turned on for the socket. # If it is set to the value "off", the SO_KEEPALIVE option is turned off for the socket. netty.soKeepAlive = true diff --git a/mithqtt-broker/src/dist/config/communicator.properties b/mithqtt-broker/src/dist/config/communicator.properties index 0e1f7b9..17c8957 100644 --- a/mithqtt-broker/src/dist/config/communicator.properties +++ b/mithqtt-broker/src/dist/config/communicator.properties @@ -14,17 +14,17 @@ communicator.application.topic = mithqtt.application # RabbitMQ # User name -rabbitmq.userName = zenin +rabbitmq.userName = guest # Password -rabbitmq.password = zenin +rabbitmq.password = guest # Virtual host rabbitmq.virtualHost = / # Server addresses # In the format like host1[:port1],host2[:port2] -rabbitmq.addresses = 192.168.1.222 +rabbitmq.addresses = localhost # Queue name for application communicator # ONLY APPLIES TO RabbitMQApplicationCommunicator diff --git a/mithqtt-broker/src/dist/config/mongo.properties b/mithqtt-broker/src/dist/config/mongo.properties index 84271b0..c8bea7f 100644 --- a/mithqtt-broker/src/dist/config/mongo.properties +++ b/mithqtt-broker/src/dist/config/mongo.properties @@ -7,10 +7,10 @@ storage.class = io.j1st.power.storage.mongo.MongoStorage # MongoDB storage configuration -mongo.address = localhost:27017 +mongo.address = 139.198.0.174:27011 mongo.database = power -mongo.userName = accountUser -mongo.password = password +mongo.userName = root +mongo.password = zxcvASDF123$ From 61559fedb2decd9ff67666814aaca1e1a8275f0a Mon Sep 17 00:00:00 2001 From: xiangdong Date: Fri, 9 Sep 2016 14:04:48 +0800 Subject: [PATCH 12/18] =?UTF-8?q?=E4=BF=AE=E6=94=B9publish=E6=97=B6topic?= =?UTF-8?q?=E4=B8=BAjsonUp=E7=9A=84=E8=BD=AC=E4=B8=BA=E6=A0=87=E5=87=86?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E7=9A=84topic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mithqtt/api/internal/InternalMessage.java | 13 +++++++++++++ .../authenticator/power/PowerAuthenticator.java | 12 ++++++------ mithqtt-broker/src/dist/config/broker.properties | 4 ++-- mithqtt-broker/src/dist/config/mongo.properties | 4 ++-- mithqtt-broker/src/dist/config/redis.properties | 3 ++- .../mithqtt/broker/handler/SyncRedisHandler.java | 8 +++++--- 6 files changed, 30 insertions(+), 14 deletions(-) diff --git a/mithqtt-api/src/main/java/com/github/longkerdandy/mithqtt/api/internal/InternalMessage.java b/mithqtt-api/src/main/java/com/github/longkerdandy/mithqtt/api/internal/InternalMessage.java index c2397ac..9c41ff9 100644 --- a/mithqtt-api/src/main/java/com/github/longkerdandy/mithqtt/api/internal/InternalMessage.java +++ b/mithqtt-api/src/main/java/com/github/longkerdandy/mithqtt/api/internal/InternalMessage.java @@ -125,6 +125,19 @@ public static InternalMessage fromMqttMessage(MqttVersion version, Stri return msg; } + public static InternalMessage fromMqttMessage(String topic,MqttVersion version, String clientId, String userName, + String brokerId, + MqttPublishMessage mqtt) { + InternalMessage msg = fromMqttMessage(version, clientId, userName, brokerId, mqtt.fixedHeader()); + // forge bytes payload + ByteBuf buf = mqtt.payload().duplicate(); + byte[] bytes = new byte[buf.readableBytes()]; + buf.readBytes(bytes); + msg.payload = new Publish(topic, mqtt.variableHeader().packetId(), bytes); + return msg; + } + + public static InternalMessage fromMqttMessage(MqttVersion version, String clientId, String userName, String brokerId, boolean cleanSession, boolean cleanExit) { diff --git a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java index d57c38a..2bd63f4 100644 --- a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java +++ b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java @@ -64,12 +64,12 @@ public AuthorizeResult authPublish(String clientId, String userName, String topi if (!this.allowDollar && topicName.startsWith("$")) return AuthorizeResult.FORBIDDEN; if (topicName.equals(this.deniedTopic)) return AuthorizeResult.FORBIDDEN; //判断topic是否包括自己的clientId - if(topicName.indexOf(clientId) == -1){ - return AuthorizeResult.FORBIDDEN; - } - if(!topicName.endsWith("upstream")){ - return AuthorizeResult.FORBIDDEN; - } +// if(topicName.indexOf(clientId) == -1){ +// return AuthorizeResult.FORBIDDEN; +// } +// if(!topicName.endsWith("upstream")){ +// return AuthorizeResult.FORBIDDEN; +// } //验证product状态是否正常 Integer status = this.mongoStorage.getProductStatusByAgentId(clientId); if(status == null || !status.equals(ProductStatus.SERVICE.value())){ diff --git a/mithqtt-broker/src/dist/config/broker.properties b/mithqtt-broker/src/dist/config/broker.properties index 3ee3517..2d8d4cc 100644 --- a/mithqtt-broker/src/dist/config/broker.properties +++ b/mithqtt-broker/src/dist/config/broker.properties @@ -3,7 +3,7 @@ # Broker # This is the broker id, please make sure each broker instance used a different id -broker.id = 1 +broker.id = test-2 # This is the ip address the broker will bind to # Use 0.0.0.0 to bind to all possible ip addresses @@ -16,7 +16,7 @@ mqtt.port = 1883 # To use ssl in the connection, set this to true # Must provide an X.509 certificate chain file in PEM format # Must provide a PKCS#8 private key file in PEM format -mqtt.ssl.enabled = true +mqtt.ssl.enabled = false # This is the network port the broker will bind to when ssl is used # The MQTT Protocol Specification recommended using port 8883 diff --git a/mithqtt-broker/src/dist/config/mongo.properties b/mithqtt-broker/src/dist/config/mongo.properties index c8bea7f..cab88e8 100644 --- a/mithqtt-broker/src/dist/config/mongo.properties +++ b/mithqtt-broker/src/dist/config/mongo.properties @@ -7,9 +7,9 @@ storage.class = io.j1st.power.storage.mongo.MongoStorage # MongoDB storage configuration -mongo.address = 139.198.0.174:27011 +mongo.address = 139.196.235.75:27017 mongo.database = power -mongo.userName = root +mongo.userName = zenin mongo.password = zxcvASDF123$ diff --git a/mithqtt-broker/src/dist/config/redis.properties b/mithqtt-broker/src/dist/config/redis.properties index 85fc16f..55e68cd 100644 --- a/mithqtt-broker/src/dist/config/redis.properties +++ b/mithqtt-broker/src/dist/config/redis.properties @@ -42,7 +42,8 @@ redis.type = single # 2. 'master_slave' : host[:port][,host2[:port2]] # the 1st should be the master node # 3. 'sentinel' : host[:port][,host2[:port2]] # the 1st should be the master node, this is the sentinel address # 4. 'cluster' : host[:port][,host2[:port2]] -redis.address = 127.0.0.1:6379 +#redis.address = 139.198.0.174:6379 +redis.address = 127.0.0.1 # Redis database number redis.database = 0 diff --git a/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/handler/SyncRedisHandler.java b/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/handler/SyncRedisHandler.java index bfd0342..e1c0b8d 100644 --- a/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/handler/SyncRedisHandler.java +++ b/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/handler/SyncRedisHandler.java @@ -405,7 +405,6 @@ protected void onPublish(ChannelHandlerContext ctx, MqttPublishMessage msg) { ctx.close(); return; } - // boolean dup = msg.fixedHeader().dup(); MqttQoS qos = msg.fixedHeader().qos(); boolean retain = msg.fixedHeader().retain(); @@ -526,9 +525,12 @@ else if (qos == MqttQoS.EXACTLY_ONCE) { onwardRecipients(msg); } } - + //转换topic为平台标准topic + if(topicName.equals("jsonUp")) { + topicName = "agents/"+userName+"/upstream"; + } // Pass message to 3rd party application - this.communicator.sendToApplication(InternalMessage.fromMqttMessage(this.version, this.clientId, this.userName, this.brokerId, msg)); + this.communicator.sendToApplication(InternalMessage.fromMqttMessage(topicName,this.version, this.clientId, this.userName, this.brokerId, msg)); } else { logger.trace("Authorization failed: Publish to topic {} unauthorized for client {}", topicName, this.clientId); From ef197f10d66a19b2ae8c0591a532d0b541a32aa3 Mon Sep 17 00:00:00 2001 From: wuyiadmin Date: Mon, 12 Sep 2016 08:46:05 +0800 Subject: [PATCH 13/18] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=8C=E5=8F=8A=E6=9D=83=E9=99=90=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=B1=BB=E7=9A=84=E8=B0=83=E6=95=B4=E3=80=82=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E4=BD=BF=E7=94=A8=E4=BF=AE=E6=94=B9=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dummy/DummyAuthenticator.java | 21 +++++++++++++++++++ .../src/dist/config/authenticator.properties | 2 +- .../src/dist/config/communicator.properties | 6 +++--- .../config/communicator.rabbitmq.properties | 6 +++--- .../src/dist/config/redis.properties | 2 +- mithqtt-http/src/dist/config/logback.xml | 8 ++++++- .../http/resources/MqttPublishResource.java | 8 ++++++- 7 files changed, 43 insertions(+), 10 deletions(-) diff --git a/mithqtt-authenticator-dummy/src/main/java/com/github/longkerdandy/mithqtt/authenticator/dummy/DummyAuthenticator.java b/mithqtt-authenticator-dummy/src/main/java/com/github/longkerdandy/mithqtt/authenticator/dummy/DummyAuthenticator.java index 3a3d5ab..719dd08 100644 --- a/mithqtt-authenticator-dummy/src/main/java/com/github/longkerdandy/mithqtt/authenticator/dummy/DummyAuthenticator.java +++ b/mithqtt-authenticator-dummy/src/main/java/com/github/longkerdandy/mithqtt/authenticator/dummy/DummyAuthenticator.java @@ -2,6 +2,8 @@ import com.github.longkerdandy.mithqtt.api.auth.AuthorizeResult; import com.github.longkerdandy.mithqtt.api.auth.Authenticator; +import io.j1st.power.storage.mongo.MongoStorage; +import io.j1st.power.storage.mongo.entity.ProductStatus; import io.netty.handler.codec.mqtt.MqttGrantedQoS; import io.netty.handler.codec.mqtt.MqttTopicSubscription; import org.apache.commons.configuration.AbstractConfiguration; @@ -19,10 +21,16 @@ public class DummyAuthenticator implements Authenticator { private boolean allowDollar; // allow $ in topic private String deniedTopic; // topic will be rejected + +// protected MongoStorage mongoStorage; + @Override public void init(AbstractConfiguration config) { this.allowDollar = config.getBoolean("allowDollar", true); this.deniedTopic = config.getString("deniedTopic", null); +// mongoStorage = new MongoStorage(); +// mongoStorage.init(config); + } @Override @@ -31,6 +39,19 @@ public void destroy() { @Override public AuthorizeResult authConnect(String clientId, String userName, String password) { + //验证clentId是否有效 +// if(!mongoStorage.isAgentExists(clientId)) { +// return AuthorizeResult.FORBIDDEN; +// } +// //验证用户名密码是否合法 +// if(!mongoStorage.isAgentAuth(userName, password)) { +// return AuthorizeResult.FORBIDDEN; +// } +// //验证product状态是否正常 +// Integer status = this.mongoStorage.getProductStatusByAgentId(clientId); +// if(status == null || !status.equals(ProductStatus.SERVICE.value())){ +// return AuthorizeResult.FORBIDDEN; +// } return AuthorizeResult.OK; } diff --git a/mithqtt-broker/src/dist/config/authenticator.properties b/mithqtt-broker/src/dist/config/authenticator.properties index 8fbc8e2..cb03962 100644 --- a/mithqtt-broker/src/dist/config/authenticator.properties +++ b/mithqtt-broker/src/dist/config/authenticator.properties @@ -5,7 +5,7 @@ # Authenticator implementation (full qualified class name) #com.github.longkerdandy.mithqtt.authenticator.dummy.DummyAuthenticator #io.j1st.mithqtt.authenticator.power.PowerAuthenticator -authenticator.class =io.j1st.mithqtt.authenticator.power.PowerAuthenticator +authenticator.class =com.github.longkerdandy.mithqtt.authenticator.dummy.DummyAuthenticator # Dummy diff --git a/mithqtt-broker/src/dist/config/communicator.properties b/mithqtt-broker/src/dist/config/communicator.properties index 17c8957..e1aafb7 100644 --- a/mithqtt-broker/src/dist/config/communicator.properties +++ b/mithqtt-broker/src/dist/config/communicator.properties @@ -14,17 +14,17 @@ communicator.application.topic = mithqtt.application # RabbitMQ # User name -rabbitmq.userName = guest +rabbitmq.userName = zenin # Password -rabbitmq.password = guest +rabbitmq.password = zenin # Virtual host rabbitmq.virtualHost = / # Server addresses # In the format like host1[:port1],host2[:port2] -rabbitmq.addresses = localhost +rabbitmq.addresses = 139.196.235.75 # Queue name for application communicator # ONLY APPLIES TO RabbitMQApplicationCommunicator diff --git a/mithqtt-broker/src/dist/config/communicator.rabbitmq.properties b/mithqtt-broker/src/dist/config/communicator.rabbitmq.properties index 17c8957..144a558 100644 --- a/mithqtt-broker/src/dist/config/communicator.rabbitmq.properties +++ b/mithqtt-broker/src/dist/config/communicator.rabbitmq.properties @@ -14,17 +14,17 @@ communicator.application.topic = mithqtt.application # RabbitMQ # User name -rabbitmq.userName = guest +rabbitmq.userName = zenin # Password -rabbitmq.password = guest +rabbitmq.password = zenin # Virtual host rabbitmq.virtualHost = / # Server addresses # In the format like host1[:port1],host2[:port2] -rabbitmq.addresses = localhost +rabbitmq.addresses = 127.0.0.1 # Queue name for application communicator # ONLY APPLIES TO RabbitMQApplicationCommunicator diff --git a/mithqtt-broker/src/dist/config/redis.properties b/mithqtt-broker/src/dist/config/redis.properties index 55e68cd..b69622e 100644 --- a/mithqtt-broker/src/dist/config/redis.properties +++ b/mithqtt-broker/src/dist/config/redis.properties @@ -42,9 +42,9 @@ redis.type = single # 2. 'master_slave' : host[:port][,host2[:port2]] # the 1st should be the master node # 3. 'sentinel' : host[:port][,host2[:port2]] # the 1st should be the master node, this is the sentinel address # 4. 'cluster' : host[:port][,host2[:port2]] + #redis.address = 139.198.0.174:6379 redis.address = 127.0.0.1 - # Redis database number redis.database = 0 diff --git a/mithqtt-http/src/dist/config/logback.xml b/mithqtt-http/src/dist/config/logback.xml index 1354651..7d34b80 100644 --- a/mithqtt-http/src/dist/config/logback.xml +++ b/mithqtt-http/src/dist/config/logback.xml @@ -7,7 +7,7 @@ - log/http.log + log/mqhttp.log log/archived/http.%d{yyyy-MM-dd}.log @@ -53,6 +53,12 @@ + + + + + + diff --git a/mithqtt-http/src/main/java/com/github/longkerdandy/mithqtt/http/resources/MqttPublishResource.java b/mithqtt-http/src/main/java/com/github/longkerdandy/mithqtt/http/resources/MqttPublishResource.java index 79f690e..e892212 100644 --- a/mithqtt-http/src/main/java/com/github/longkerdandy/mithqtt/http/resources/MqttPublishResource.java +++ b/mithqtt-http/src/main/java/com/github/longkerdandy/mithqtt/http/resources/MqttPublishResource.java @@ -45,12 +45,15 @@ public MqttPublishResource(String serverId, Validator validator, RedisSyncStorag super(serverId, validator, redis, communicator, authenticator, metrics); } - @PermitAll + //@PermitAll @POST public ResultEntity publish(@PathParam("clientId") String clientId, @Auth UserPrincipal user, @QueryParam("protocol") @DefaultValue("4") byte protocol, @QueryParam("dup") @DefaultValue("false") boolean dup, @QueryParam("qos") @DefaultValue("0") int qos, @QueryParam("topicName") String topicName, @QueryParam("packetId") @DefaultValue("0") int packetId, String body) throws UnsupportedEncodingException { + + logger.info("clientId {} publish message to rabbitmq ,topic = {}",clientId,topicName); + String userName = clientId; MqttVersion version = MqttVersion.fromProtocolLevel(protocol); byte[] payload = body == null ? null : body.getBytes("ISO-8859-1"); @@ -125,6 +128,7 @@ public ResultEntity publish(@PathParam("clientId") String clientId, @Au logger.trace("Communicator sending: Send PUBLISH message to broker {} for client {} subscription", bid, cid); d = true; this.communicator.sendToBroker(bid, m); + logger.info("clientId {} publish message to broker success . message topic = {}",clientId,topicName); } // In the QoS 1 delivery protocol, the Sender @@ -142,8 +146,10 @@ public ResultEntity publish(@PathParam("clientId") String clientId, @Au // Pass message to 3rd party application this.communicator.sendToApplication(msg); + logger.info("clientId {} publish message to rabbitmq success . message topic = {}",clientId,topicName); return new ResultEntity<>(true); } else { + logger.info("clientId {} publish message to rabbitmq Authorize fail publish out . message topic = {}",clientId,topicName); throw new AuthorizeException(new ErrorEntity(ErrorCode.UNAUTHORIZED)); } } From 3674d8a0e1d99239ca451d85dd6404ca778a5974 Mon Sep 17 00:00:00 2001 From: 12315jack Date: Sun, 28 May 2017 13:27:01 +0800 Subject: [PATCH 14/18] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E6=9C=8D=E5=8A=A1=E5=99=A8=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradlew | 10 +- .../power/PowerAuthenticator.java | 29 ++++-- .../src/dist/config/authenticator.properties | 3 +- .../src/dist/config/communicator.properties | 2 +- .../src/dist/config/mongo.properties | 8 +- .../src/dist/config/redis.properties | 7 +- .../broker/handler/SyncRedisHandler.java | 1 - .../power/storage/mongo/MongoStorage.java | 91 +++++++++++++++---- .../storage/mongo/entity/AgentStatus.java | 36 ++++++++ .../storage/mongo/entity/ServiceType.java | 32 +++++++ 10 files changed, 177 insertions(+), 42 deletions(-) create mode 100644 mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/AgentStatus.java create mode 100644 mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/ServiceType.java diff --git a/gradlew b/gradlew index 91a7e26..9d82f78 100644 --- a/gradlew +++ b/gradlew @@ -42,11 +42,6 @@ case "`uname`" in ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" @@ -61,9 +56,9 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- +cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" -cd "$SAVED" >&- +cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -114,6 +109,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` diff --git a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java index 2bd63f4..1182ea0 100644 --- a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java +++ b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java @@ -3,12 +3,13 @@ import com.github.longkerdandy.mithqtt.api.auth.Authenticator; import com.github.longkerdandy.mithqtt.api.auth.AuthorizeResult; import io.j1st.power.storage.mongo.MongoStorage; +import io.j1st.power.storage.mongo.entity.AgentStatus; import io.j1st.power.storage.mongo.entity.ProductStatus; +import io.j1st.power.storage.mongo.entity.ServiceType; import io.netty.handler.codec.mqtt.MqttGrantedQoS; import io.netty.handler.codec.mqtt.MqttTopicSubscription; import org.apache.commons.configuration.AbstractConfiguration; -import org.apache.commons.configuration.ConfigurationException; -import org.apache.commons.configuration.PropertiesConfiguration; +import org.bson.types.ObjectId; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,18 +45,32 @@ public void destroy() { @Override public AuthorizeResult authConnect(String clientId, String userName, String password) { //验证clentId是否有效 - if(!mongoStorage.isAgentExists(clientId)) { + if (!mongoStorage.isAgentExists(clientId)) { return AuthorizeResult.FORBIDDEN; } //验证用户名密码是否合法 - if(!mongoStorage.isAgentAuth(userName, password)) { + if (!mongoStorage.isAgentAuth(userName, password)) { return AuthorizeResult.FORBIDDEN; } //验证product状态是否正常 Integer status = this.mongoStorage.getProductStatusByAgentId(clientId); - if(status == null || !status.equals(ProductStatus.SERVICE.value())){ + if (status == null || !status.equals(ProductStatus.SERVICE.value())) { return AuthorizeResult.FORBIDDEN; } + /* //验证所在的service是否链接已满 + String userId = this.mongoStorage.getOperatorIdByAgent(clientId); + if (userId != null) { + //link count + long count = this.mongoStorage.getServiceCountByOperatorId(ServiceType.HARDWARE_MANAGER.value(), new ObjectId(userId)); + if (count <= 0) { + return AuthorizeResult.FORBIDDEN; + } + }*/ + // Validate Agent Connect Privilege + if (this.mongoStorage.isDisableAgent(new ObjectId(clientId), AgentStatus.DISABLED.value())) { + return AuthorizeResult.FORBIDDEN; + } + return AuthorizeResult.OK; } @@ -72,7 +87,7 @@ public AuthorizeResult authPublish(String clientId, String userName, String topi // } //验证product状态是否正常 Integer status = this.mongoStorage.getProductStatusByAgentId(clientId); - if(status == null || !status.equals(ProductStatus.SERVICE.value())){ + if (status == null || !status.equals(ProductStatus.SERVICE.value())) { return AuthorizeResult.FORBIDDEN; } return AuthorizeResult.OK; @@ -85,7 +100,7 @@ public List authSubscribe(String clientId, String userName, List if (!this.allowDollar && subscription.topic().startsWith("$")) r.add(MqttGrantedQoS.FAILURE); if (subscription.topic().equals(this.deniedTopic)) r.add(MqttGrantedQoS.FAILURE); if (!subscription.topic().endsWith("downstream")) r.add(MqttGrantedQoS.FAILURE); - if (subscription.topic().indexOf(clientId) == -1) r.add(MqttGrantedQoS.FAILURE); + if (!subscription.topic().contains(clientId)) r.add(MqttGrantedQoS.FAILURE); r.add(MqttGrantedQoS.valueOf(subscription.requestedQos().value())); }); return r; diff --git a/mithqtt-broker/src/dist/config/authenticator.properties b/mithqtt-broker/src/dist/config/authenticator.properties index cb03962..6b22136 100644 --- a/mithqtt-broker/src/dist/config/authenticator.properties +++ b/mithqtt-broker/src/dist/config/authenticator.properties @@ -5,7 +5,8 @@ # Authenticator implementation (full qualified class name) #com.github.longkerdandy.mithqtt.authenticator.dummy.DummyAuthenticator #io.j1st.mithqtt.authenticator.power.PowerAuthenticator -authenticator.class =com.github.longkerdandy.mithqtt.authenticator.dummy.DummyAuthenticator +#authenticator.class =com.github.longkerdandy.mithqtt.authenticator.dummy.DummyAuthenticator +authenticator.class=io.j1st.mithqtt.authenticator.power.PowerAuthenticator # Dummy diff --git a/mithqtt-broker/src/dist/config/communicator.properties b/mithqtt-broker/src/dist/config/communicator.properties index e1aafb7..53d21dd 100644 --- a/mithqtt-broker/src/dist/config/communicator.properties +++ b/mithqtt-broker/src/dist/config/communicator.properties @@ -24,7 +24,7 @@ rabbitmq.virtualHost = / # Server addresses # In the format like host1[:port1],host2[:port2] -rabbitmq.addresses = 139.196.235.75 +rabbitmq.addresses=139.196.235.75:5672 # Queue name for application communicator # ONLY APPLIES TO RabbitMQApplicationCommunicator diff --git a/mithqtt-broker/src/dist/config/mongo.properties b/mithqtt-broker/src/dist/config/mongo.properties index cab88e8..8cd57af 100644 --- a/mithqtt-broker/src/dist/config/mongo.properties +++ b/mithqtt-broker/src/dist/config/mongo.properties @@ -7,10 +7,10 @@ storage.class = io.j1st.power.storage.mongo.MongoStorage # MongoDB storage configuration -mongo.address = 139.196.235.75:27017 -mongo.database = power -mongo.userName = zenin -mongo.password = zxcvASDF123$ +mongo.address=139.196.235.75:27017 +mongo.database=power +mongo.userName=zenin +mongo.password=NaaMW2id diff --git a/mithqtt-broker/src/dist/config/redis.properties b/mithqtt-broker/src/dist/config/redis.properties index b69622e..13a12d1 100644 --- a/mithqtt-broker/src/dist/config/redis.properties +++ b/mithqtt-broker/src/dist/config/redis.properties @@ -16,7 +16,7 @@ storage.sync.class = com.github.longkerdandy.mithqtt.storage.redis.sync.RedisSyn mqtt.inflight.queue.size = 0 # The unacknowledged QoS 2 messages' id were stored in order for each client -# Including: +# Including:S # QoS 2 PUBLISH messages received but not acknowledged by PUBREL # If the queue size limit is reached, the oldest QoS 2 message id will be dropped. # Default and 0 means no limit. @@ -27,8 +27,7 @@ mqtt.qos2.queue.size = 0 # If the queue size limit is reached, the oldest retain message will be dropped. # Default and 0 means no limit. mqtt.retain.queue.size = 0 - -# Redis +# RedisS # Redis server type, could be: # 1. 'single' : http://redis.io/topics/config @@ -44,7 +43,7 @@ redis.type = single # 4. 'cluster' : host[:port][,host2[:port2]] #redis.address = 139.198.0.174:6379 -redis.address = 127.0.0.1 +redis.address=localhost:6379 # Redis database number redis.database = 0 diff --git a/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/handler/SyncRedisHandler.java b/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/handler/SyncRedisHandler.java index e1c0b8d..6a9883a 100644 --- a/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/handler/SyncRedisHandler.java +++ b/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/handler/SyncRedisHandler.java @@ -965,7 +965,6 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception { } } } - /** * Handle connection lost condition * Both when received DISCONNECT message or not diff --git a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java index bfb05bb..0cd59d8 100644 --- a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java +++ b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java @@ -3,18 +3,18 @@ import com.mongodb.MongoClient; import com.mongodb.MongoCredential; import com.mongodb.ServerAddress; +import com.mongodb.client.FindIterable; import com.mongodb.client.MongoDatabase; -import io.j1st.power.storage.mongo.entity.Agent; import io.j1st.power.storage.mongo.entity.Permission; -import io.j1st.power.storage.mongo.entity.User; import org.apache.commons.configuration.AbstractConfiguration; import org.bson.Document; import org.bson.types.ObjectId; -import java.util.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + import static com.mongodb.client.model.Filters.*; -import static com.mongodb.client.model.Filters.eq; -import static com.mongodb.client.model.Filters.exists; import static com.mongodb.client.model.Projections.exclude; import static com.mongodb.client.model.Projections.include; @@ -81,7 +81,7 @@ private List parseCredentials(String userName, String database, * @return True 权限满足 */ public boolean isAgentdByUser(String id, Permission permission) { - if(!ObjectId.isValid(id)){ + if (!ObjectId.isValid(id)) { return false; } return this.database.getCollection("agents") @@ -98,11 +98,11 @@ public boolean isAgentdByUser(String id, Permission permission) { * @return 采集器 or Null */ public boolean isAgentExists(String id) { - if(!ObjectId.isValid(id)){ + if (!ObjectId.isValid(id)) { return false; } return this.database.getCollection("agents") - .find(eq("_id", new ObjectId(id))).first() != null ; + .find(eq("_id", new ObjectId(id))).first() != null; } @@ -112,12 +112,12 @@ public boolean isAgentExists(String id) { * @param userName 采集器Id * @return 采集器 or Null */ - public boolean isAgentAuth(String userName , String password) { - if(!ObjectId.isValid(userName)){ + public boolean isAgentAuth(String userName, String password) { + if (!ObjectId.isValid(userName)) { return false; } return this.database.getCollection("agents") - .find(and(eq("_id", new ObjectId(userName)),eq("token", password))).first() != null ; + .find(and(eq("_id", new ObjectId(userName)), eq("token", password))).first() != null; } @@ -144,7 +144,7 @@ public String getUserByToken(String token) { * @return True 被激活 */ public boolean isProductActivated(String productId) { - if(!ObjectId.isValid(productId)){ + if (!ObjectId.isValid(productId)) { return false; } return this.database.getCollection("agents") @@ -154,6 +154,19 @@ public boolean isProductActivated(String productId) { } + /** + * 判断agent是否可连接 + * + * @param agentId agent id + * @param status status + * @return is exist + */ + public boolean isDisableAgent(ObjectId agentId, int status) { + return this.database.getCollection("agents") + .find(and(eq("_id", agentId), eq("status", status))) + .first() != null; + + } /** * 获取 产品 是否被激活 @@ -164,19 +177,19 @@ public boolean isProductActivated(String productId) { */ public Integer getProductStatusByAgentId(String agentId) { Integer status = null; - if(!ObjectId.isValid(agentId)){ + if (!ObjectId.isValid(agentId)) { return null; } Document agentDocument = this.database.getCollection("agents") .find(eq("_id", new ObjectId(agentId))) .first(); - if(agentDocument != null) { + if (agentDocument != null) { ObjectId productId = agentDocument.getObjectId("product_id"); - if(productId != null) { + if (productId != null) { Document productDocument = this.database.getCollection("products") - .find(eq("_id",productId)) + .find(eq("_id", productId)) .first(); - if(productDocument != null) { + if (productDocument != null) { status = productDocument.getInteger("status"); } } @@ -184,4 +197,48 @@ public Integer getProductStatusByAgentId(String agentId) { return status; } + + /** + * 根据Operator订购的服务类型来查询可用数量(未过期的) + * + * @param serviceType service type + * @param operatorId operator ID + * @return number of service + */ + public long getServiceCountByOperatorId(Integer serviceType, ObjectId operatorId) { + long count = 0; + Document query = new Document(); + query.append("user_id", operatorId); + query.append("serviceType", serviceType); + query.append("expired_at", new Document("$gte", new Date())); + FindIterable ds = this.database.getCollection("user_services").find(query); + if (ds != null) { + for (Document d : ds) { + if (d.getLong("used") != null) { + count += d.getLong("count") - d.getLong("used"); + } else { + count += d.getLong("count"); + } + } + } + return count; + } + + /** + * Get Operator Id + * + * @param agentId Agent id + * @return Operator id + */ + public String getOperatorIdByAgent(String agentId) { + if (!ObjectId.isValid(agentId)) { + return null; + } + Document doc = this.database.getCollection("user_assets_info").find(eq("agent_id", new ObjectId(agentId))) + .projection(include("user_id")) + .first(); + if (doc == null) return null; + return doc.getObjectId("user_id").toString(); + } + } diff --git a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/AgentStatus.java b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/AgentStatus.java new file mode 100644 index 0000000..77ee03c --- /dev/null +++ b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/AgentStatus.java @@ -0,0 +1,36 @@ +package io.j1st.power.storage.mongo.entity; + +/** + * Agent status + */ +public enum AgentStatus { + + INIT(1), //初始化 + IN_DEVELOPER_ORDER(2), //初始化 + TO_OPERATOR(3), //分配到operator + TO_INSTALLER(4), //分配到installerd + INSTALL_ING(5), //安装中 + INSTALL_SUCCESS(6), //安装完成 + TEST(7), //测试中() + COMPLETE(8), //验收完成 + DISABLED(10); //停用 + + private final int value; + + AgentStatus(int value) { + this.value = value; + } + + public static AgentStatus valueOf(int value) { + for (AgentStatus t : values()) { + if (t.value == value) { + return t; + } + } + return null; + } + + public int value() { + return value; + } +} diff --git a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/ServiceType.java b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/ServiceType.java new file mode 100644 index 0000000..a9cdd58 --- /dev/null +++ b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/entity/ServiceType.java @@ -0,0 +1,32 @@ +package io.j1st.power.storage.mongo.entity; + +/** + * service type 服务类型 + */ +public enum ServiceType { + + HARDWARE_MANAGER(0), + ANALYSIS(1), + AUTO_ENGINE(2), + WEBHOOKER(3); + + private final int value; + + ServiceType(int value) { + this.value = value; + } + + public static ServiceType valueOf(int value) { + for (ServiceType r : values()) { + if (r.value == value) { + return r; + } + } + throw new IllegalArgumentException("invalid service type: " + value); + } + + public int value() { + return value; + } + +} From 8f318a69efdf92de1702f91f1c2aa51154b5744d Mon Sep 17 00:00:00 2001 From: 12315jack Date: Mon, 26 Jun 2017 16:11:59 +0800 Subject: [PATCH 15/18] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E6=9C=8D=E5=8A=A1=E5=99=A8=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E9=80=BB=E8=BE=91,=E6=B7=BB=E5=8A=A0=E4=B8=BB=E5=8A=A8?= =?UTF-8?q?=E6=96=AD=E5=BC=80=E8=AE=BE=E5=A4=87=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../authenticator/power/PowerAuthenticator.java | 10 +++++++--- mithqtt-broker/src/dist/config/broker.properties | 4 ++-- mithqtt-broker/src/dist/config/communicator.properties | 6 +++--- mithqtt-broker/src/dist/config/mongo.properties | 6 +++--- .../mithqtt/broker/handler/SyncRedisHandler.java | 5 +++-- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java index 1182ea0..acea51f 100644 --- a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java +++ b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java @@ -85,9 +85,13 @@ public AuthorizeResult authPublish(String clientId, String userName, String topi // if(!topicName.endsWith("upstream")){ // return AuthorizeResult.FORBIDDEN; // } - //验证product状态是否正常 - Integer status = this.mongoStorage.getProductStatusByAgentId(clientId); - if (status == null || !status.equals(ProductStatus.SERVICE.value())) { +// //验证product状态是否正常 +// Integer status = this.mongoStorage.getProductStatusByAgentId(clientId); +// if (status == null || !status.equals(ProductStatus.SERVICE.value())) { +// return AuthorizeResult.FORBIDDEN; +// } + // Validate Agent Connect Privilege + if (this.mongoStorage.isDisableAgent(new ObjectId(clientId), AgentStatus.DISABLED.value())) { return AuthorizeResult.FORBIDDEN; } return AuthorizeResult.OK; diff --git a/mithqtt-broker/src/dist/config/broker.properties b/mithqtt-broker/src/dist/config/broker.properties index 2d8d4cc..ac0294d 100644 --- a/mithqtt-broker/src/dist/config/broker.properties +++ b/mithqtt-broker/src/dist/config/broker.properties @@ -3,7 +3,7 @@ # Broker # This is the broker id, please make sure each broker instance used a different id -broker.id = test-2 +broker.id=test-1 # This is the ip address the broker will bind to # Use 0.0.0.0 to bind to all possible ip addresses @@ -11,7 +11,7 @@ mqtt.host = 0.0.0.0 # This is the network port the broker will bind to # The MQTT Protocol Specification recommended using port 1883 -mqtt.port = 1883 +mqtt.port=1884 # To use ssl in the connection, set this to true # Must provide an X.509 certificate chain file in PEM format diff --git a/mithqtt-broker/src/dist/config/communicator.properties b/mithqtt-broker/src/dist/config/communicator.properties index 53d21dd..a65a207 100644 --- a/mithqtt-broker/src/dist/config/communicator.properties +++ b/mithqtt-broker/src/dist/config/communicator.properties @@ -14,17 +14,17 @@ communicator.application.topic = mithqtt.application # RabbitMQ # User name -rabbitmq.userName = zenin +rabbitmq.userName=guest # Password -rabbitmq.password = zenin +rabbitmq.password=guest # Virtual host rabbitmq.virtualHost = / # Server addresses # In the format like host1[:port1],host2[:port2] -rabbitmq.addresses=139.196.235.75:5672 +rabbitmq.addresses=localhost:5672 # Queue name for application communicator # ONLY APPLIES TO RabbitMQApplicationCommunicator diff --git a/mithqtt-broker/src/dist/config/mongo.properties b/mithqtt-broker/src/dist/config/mongo.properties index 8cd57af..a0f68a3 100644 --- a/mithqtt-broker/src/dist/config/mongo.properties +++ b/mithqtt-broker/src/dist/config/mongo.properties @@ -7,10 +7,10 @@ storage.class = io.j1st.power.storage.mongo.MongoStorage # MongoDB storage configuration -mongo.address=139.196.235.75:27017 +mongo.address=localhost:27017 mongo.database=power -mongo.userName=zenin -mongo.password=NaaMW2id +mongo.userName=jack +mongo.password=123456 diff --git a/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/handler/SyncRedisHandler.java b/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/handler/SyncRedisHandler.java index 6a9883a..c719c0c 100644 --- a/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/handler/SyncRedisHandler.java +++ b/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/handler/SyncRedisHandler.java @@ -534,6 +534,7 @@ else if (qos == MqttQoS.EXACTLY_ONCE) { } else { logger.trace("Authorization failed: Publish to topic {} unauthorized for client {}", topicName, this.clientId); + ctx.close(); } } @@ -1034,9 +1035,9 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { if (this.connected) { if (cause instanceof IOException) { - logger.debug("Exception caught: Exception caught from client {} user {}: ", this.clientId, this.userName, ExceptionUtils.getMessage(cause)); + logger.debug("Exception caught: Exception caught from client {} user {}: {}", this.clientId, this.userName, ExceptionUtils.getMessage(cause)); } else { - logger.debug("Exception caught: Exception caught from client {} user {}: ", this.clientId, this.userName, cause); + logger.debug("Exception caught: Exception caught from client {} user {}: {}", this.clientId, this.userName, cause); } } ctx.close(); From 324ed10705eda9ed1dbc3009baa4fee8ed203b58 Mon Sep 17 00:00:00 2001 From: WY Date: Mon, 26 Jun 2017 16:26:55 +0800 Subject: [PATCH 16/18] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=83=A8=E5=88=86?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E9=80=BB=E8=BE=91=E5=92=8C=E5=9F=BA=E6=9C=AC?= =?UTF-8?q?=E7=9A=84=E6=95=B0=E6=8D=AE=E5=A4=84=E7=90=86=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../power/PowerAuthenticator.java | 37 ++++++-- .../src/dist/config/authenticator.properties | 3 +- .../src/dist/config/broker.properties | 4 +- .../src/dist/config/communicator.properties | 6 +- .../src/dist/config/mongo.properties | 8 +- .../src/dist/config/redis.properties | 7 +- .../broker/handler/SyncRedisHandler.java | 6 +- .../power/storage/mongo/MongoStorage.java | 91 +++++++++++++++---- 8 files changed, 119 insertions(+), 43 deletions(-) diff --git a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java index 2bd63f4..acea51f 100644 --- a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java +++ b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java @@ -3,12 +3,13 @@ import com.github.longkerdandy.mithqtt.api.auth.Authenticator; import com.github.longkerdandy.mithqtt.api.auth.AuthorizeResult; import io.j1st.power.storage.mongo.MongoStorage; +import io.j1st.power.storage.mongo.entity.AgentStatus; import io.j1st.power.storage.mongo.entity.ProductStatus; +import io.j1st.power.storage.mongo.entity.ServiceType; import io.netty.handler.codec.mqtt.MqttGrantedQoS; import io.netty.handler.codec.mqtt.MqttTopicSubscription; import org.apache.commons.configuration.AbstractConfiguration; -import org.apache.commons.configuration.ConfigurationException; -import org.apache.commons.configuration.PropertiesConfiguration; +import org.bson.types.ObjectId; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,18 +45,32 @@ public void destroy() { @Override public AuthorizeResult authConnect(String clientId, String userName, String password) { //验证clentId是否有效 - if(!mongoStorage.isAgentExists(clientId)) { + if (!mongoStorage.isAgentExists(clientId)) { return AuthorizeResult.FORBIDDEN; } //验证用户名密码是否合法 - if(!mongoStorage.isAgentAuth(userName, password)) { + if (!mongoStorage.isAgentAuth(userName, password)) { return AuthorizeResult.FORBIDDEN; } //验证product状态是否正常 Integer status = this.mongoStorage.getProductStatusByAgentId(clientId); - if(status == null || !status.equals(ProductStatus.SERVICE.value())){ + if (status == null || !status.equals(ProductStatus.SERVICE.value())) { return AuthorizeResult.FORBIDDEN; } + /* //验证所在的service是否链接已满 + String userId = this.mongoStorage.getOperatorIdByAgent(clientId); + if (userId != null) { + //link count + long count = this.mongoStorage.getServiceCountByOperatorId(ServiceType.HARDWARE_MANAGER.value(), new ObjectId(userId)); + if (count <= 0) { + return AuthorizeResult.FORBIDDEN; + } + }*/ + // Validate Agent Connect Privilege + if (this.mongoStorage.isDisableAgent(new ObjectId(clientId), AgentStatus.DISABLED.value())) { + return AuthorizeResult.FORBIDDEN; + } + return AuthorizeResult.OK; } @@ -70,9 +85,13 @@ public AuthorizeResult authPublish(String clientId, String userName, String topi // if(!topicName.endsWith("upstream")){ // return AuthorizeResult.FORBIDDEN; // } - //验证product状态是否正常 - Integer status = this.mongoStorage.getProductStatusByAgentId(clientId); - if(status == null || !status.equals(ProductStatus.SERVICE.value())){ +// //验证product状态是否正常 +// Integer status = this.mongoStorage.getProductStatusByAgentId(clientId); +// if (status == null || !status.equals(ProductStatus.SERVICE.value())) { +// return AuthorizeResult.FORBIDDEN; +// } + // Validate Agent Connect Privilege + if (this.mongoStorage.isDisableAgent(new ObjectId(clientId), AgentStatus.DISABLED.value())) { return AuthorizeResult.FORBIDDEN; } return AuthorizeResult.OK; @@ -85,7 +104,7 @@ public List authSubscribe(String clientId, String userName, List if (!this.allowDollar && subscription.topic().startsWith("$")) r.add(MqttGrantedQoS.FAILURE); if (subscription.topic().equals(this.deniedTopic)) r.add(MqttGrantedQoS.FAILURE); if (!subscription.topic().endsWith("downstream")) r.add(MqttGrantedQoS.FAILURE); - if (subscription.topic().indexOf(clientId) == -1) r.add(MqttGrantedQoS.FAILURE); + if (!subscription.topic().contains(clientId)) r.add(MqttGrantedQoS.FAILURE); r.add(MqttGrantedQoS.valueOf(subscription.requestedQos().value())); }); return r; diff --git a/mithqtt-broker/src/dist/config/authenticator.properties b/mithqtt-broker/src/dist/config/authenticator.properties index cb03962..6b22136 100644 --- a/mithqtt-broker/src/dist/config/authenticator.properties +++ b/mithqtt-broker/src/dist/config/authenticator.properties @@ -5,7 +5,8 @@ # Authenticator implementation (full qualified class name) #com.github.longkerdandy.mithqtt.authenticator.dummy.DummyAuthenticator #io.j1st.mithqtt.authenticator.power.PowerAuthenticator -authenticator.class =com.github.longkerdandy.mithqtt.authenticator.dummy.DummyAuthenticator +#authenticator.class =com.github.longkerdandy.mithqtt.authenticator.dummy.DummyAuthenticator +authenticator.class=io.j1st.mithqtt.authenticator.power.PowerAuthenticator # Dummy diff --git a/mithqtt-broker/src/dist/config/broker.properties b/mithqtt-broker/src/dist/config/broker.properties index 2d8d4cc..ac0294d 100644 --- a/mithqtt-broker/src/dist/config/broker.properties +++ b/mithqtt-broker/src/dist/config/broker.properties @@ -3,7 +3,7 @@ # Broker # This is the broker id, please make sure each broker instance used a different id -broker.id = test-2 +broker.id=test-1 # This is the ip address the broker will bind to # Use 0.0.0.0 to bind to all possible ip addresses @@ -11,7 +11,7 @@ mqtt.host = 0.0.0.0 # This is the network port the broker will bind to # The MQTT Protocol Specification recommended using port 1883 -mqtt.port = 1883 +mqtt.port=1884 # To use ssl in the connection, set this to true # Must provide an X.509 certificate chain file in PEM format diff --git a/mithqtt-broker/src/dist/config/communicator.properties b/mithqtt-broker/src/dist/config/communicator.properties index e1aafb7..a65a207 100644 --- a/mithqtt-broker/src/dist/config/communicator.properties +++ b/mithqtt-broker/src/dist/config/communicator.properties @@ -14,17 +14,17 @@ communicator.application.topic = mithqtt.application # RabbitMQ # User name -rabbitmq.userName = zenin +rabbitmq.userName=guest # Password -rabbitmq.password = zenin +rabbitmq.password=guest # Virtual host rabbitmq.virtualHost = / # Server addresses # In the format like host1[:port1],host2[:port2] -rabbitmq.addresses = 139.196.235.75 +rabbitmq.addresses=localhost:5672 # Queue name for application communicator # ONLY APPLIES TO RabbitMQApplicationCommunicator diff --git a/mithqtt-broker/src/dist/config/mongo.properties b/mithqtt-broker/src/dist/config/mongo.properties index cab88e8..a0f68a3 100644 --- a/mithqtt-broker/src/dist/config/mongo.properties +++ b/mithqtt-broker/src/dist/config/mongo.properties @@ -7,10 +7,10 @@ storage.class = io.j1st.power.storage.mongo.MongoStorage # MongoDB storage configuration -mongo.address = 139.196.235.75:27017 -mongo.database = power -mongo.userName = zenin -mongo.password = zxcvASDF123$ +mongo.address=localhost:27017 +mongo.database=power +mongo.userName=jack +mongo.password=123456 diff --git a/mithqtt-broker/src/dist/config/redis.properties b/mithqtt-broker/src/dist/config/redis.properties index b69622e..13a12d1 100644 --- a/mithqtt-broker/src/dist/config/redis.properties +++ b/mithqtt-broker/src/dist/config/redis.properties @@ -16,7 +16,7 @@ storage.sync.class = com.github.longkerdandy.mithqtt.storage.redis.sync.RedisSyn mqtt.inflight.queue.size = 0 # The unacknowledged QoS 2 messages' id were stored in order for each client -# Including: +# Including:S # QoS 2 PUBLISH messages received but not acknowledged by PUBREL # If the queue size limit is reached, the oldest QoS 2 message id will be dropped. # Default and 0 means no limit. @@ -27,8 +27,7 @@ mqtt.qos2.queue.size = 0 # If the queue size limit is reached, the oldest retain message will be dropped. # Default and 0 means no limit. mqtt.retain.queue.size = 0 - -# Redis +# RedisS # Redis server type, could be: # 1. 'single' : http://redis.io/topics/config @@ -44,7 +43,7 @@ redis.type = single # 4. 'cluster' : host[:port][,host2[:port2]] #redis.address = 139.198.0.174:6379 -redis.address = 127.0.0.1 +redis.address=localhost:6379 # Redis database number redis.database = 0 diff --git a/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/handler/SyncRedisHandler.java b/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/handler/SyncRedisHandler.java index e1c0b8d..c719c0c 100644 --- a/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/handler/SyncRedisHandler.java +++ b/mithqtt-broker/src/main/java/com/github/longkerdandy/mithqtt/broker/handler/SyncRedisHandler.java @@ -534,6 +534,7 @@ else if (qos == MqttQoS.EXACTLY_ONCE) { } else { logger.trace("Authorization failed: Publish to topic {} unauthorized for client {}", topicName, this.clientId); + ctx.close(); } } @@ -965,7 +966,6 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception { } } } - /** * Handle connection lost condition * Both when received DISCONNECT message or not @@ -1035,9 +1035,9 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { if (this.connected) { if (cause instanceof IOException) { - logger.debug("Exception caught: Exception caught from client {} user {}: ", this.clientId, this.userName, ExceptionUtils.getMessage(cause)); + logger.debug("Exception caught: Exception caught from client {} user {}: {}", this.clientId, this.userName, ExceptionUtils.getMessage(cause)); } else { - logger.debug("Exception caught: Exception caught from client {} user {}: ", this.clientId, this.userName, cause); + logger.debug("Exception caught: Exception caught from client {} user {}: {}", this.clientId, this.userName, cause); } } ctx.close(); diff --git a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java index bfb05bb..0cd59d8 100644 --- a/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java +++ b/mithqtt-storage-mongo/src/main/java/io/j1st/power/storage/mongo/MongoStorage.java @@ -3,18 +3,18 @@ import com.mongodb.MongoClient; import com.mongodb.MongoCredential; import com.mongodb.ServerAddress; +import com.mongodb.client.FindIterable; import com.mongodb.client.MongoDatabase; -import io.j1st.power.storage.mongo.entity.Agent; import io.j1st.power.storage.mongo.entity.Permission; -import io.j1st.power.storage.mongo.entity.User; import org.apache.commons.configuration.AbstractConfiguration; import org.bson.Document; import org.bson.types.ObjectId; -import java.util.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + import static com.mongodb.client.model.Filters.*; -import static com.mongodb.client.model.Filters.eq; -import static com.mongodb.client.model.Filters.exists; import static com.mongodb.client.model.Projections.exclude; import static com.mongodb.client.model.Projections.include; @@ -81,7 +81,7 @@ private List parseCredentials(String userName, String database, * @return True 权限满足 */ public boolean isAgentdByUser(String id, Permission permission) { - if(!ObjectId.isValid(id)){ + if (!ObjectId.isValid(id)) { return false; } return this.database.getCollection("agents") @@ -98,11 +98,11 @@ public boolean isAgentdByUser(String id, Permission permission) { * @return 采集器 or Null */ public boolean isAgentExists(String id) { - if(!ObjectId.isValid(id)){ + if (!ObjectId.isValid(id)) { return false; } return this.database.getCollection("agents") - .find(eq("_id", new ObjectId(id))).first() != null ; + .find(eq("_id", new ObjectId(id))).first() != null; } @@ -112,12 +112,12 @@ public boolean isAgentExists(String id) { * @param userName 采集器Id * @return 采集器 or Null */ - public boolean isAgentAuth(String userName , String password) { - if(!ObjectId.isValid(userName)){ + public boolean isAgentAuth(String userName, String password) { + if (!ObjectId.isValid(userName)) { return false; } return this.database.getCollection("agents") - .find(and(eq("_id", new ObjectId(userName)),eq("token", password))).first() != null ; + .find(and(eq("_id", new ObjectId(userName)), eq("token", password))).first() != null; } @@ -144,7 +144,7 @@ public String getUserByToken(String token) { * @return True 被激活 */ public boolean isProductActivated(String productId) { - if(!ObjectId.isValid(productId)){ + if (!ObjectId.isValid(productId)) { return false; } return this.database.getCollection("agents") @@ -154,6 +154,19 @@ public boolean isProductActivated(String productId) { } + /** + * 判断agent是否可连接 + * + * @param agentId agent id + * @param status status + * @return is exist + */ + public boolean isDisableAgent(ObjectId agentId, int status) { + return this.database.getCollection("agents") + .find(and(eq("_id", agentId), eq("status", status))) + .first() != null; + + } /** * 获取 产品 是否被激活 @@ -164,19 +177,19 @@ public boolean isProductActivated(String productId) { */ public Integer getProductStatusByAgentId(String agentId) { Integer status = null; - if(!ObjectId.isValid(agentId)){ + if (!ObjectId.isValid(agentId)) { return null; } Document agentDocument = this.database.getCollection("agents") .find(eq("_id", new ObjectId(agentId))) .first(); - if(agentDocument != null) { + if (agentDocument != null) { ObjectId productId = agentDocument.getObjectId("product_id"); - if(productId != null) { + if (productId != null) { Document productDocument = this.database.getCollection("products") - .find(eq("_id",productId)) + .find(eq("_id", productId)) .first(); - if(productDocument != null) { + if (productDocument != null) { status = productDocument.getInteger("status"); } } @@ -184,4 +197,48 @@ public Integer getProductStatusByAgentId(String agentId) { return status; } + + /** + * 根据Operator订购的服务类型来查询可用数量(未过期的) + * + * @param serviceType service type + * @param operatorId operator ID + * @return number of service + */ + public long getServiceCountByOperatorId(Integer serviceType, ObjectId operatorId) { + long count = 0; + Document query = new Document(); + query.append("user_id", operatorId); + query.append("serviceType", serviceType); + query.append("expired_at", new Document("$gte", new Date())); + FindIterable ds = this.database.getCollection("user_services").find(query); + if (ds != null) { + for (Document d : ds) { + if (d.getLong("used") != null) { + count += d.getLong("count") - d.getLong("used"); + } else { + count += d.getLong("count"); + } + } + } + return count; + } + + /** + * Get Operator Id + * + * @param agentId Agent id + * @return Operator id + */ + public String getOperatorIdByAgent(String agentId) { + if (!ObjectId.isValid(agentId)) { + return null; + } + Document doc = this.database.getCollection("user_assets_info").find(eq("agent_id", new ObjectId(agentId))) + .projection(include("user_id")) + .first(); + if (doc == null) return null; + return doc.getObjectId("user_id").toString(); + } + } From b0fda3b95a6b15ad64bea176a4d9a3dad6b41c8d Mon Sep 17 00:00:00 2001 From: 12315jack Date: Wed, 18 Oct 2017 13:01:25 +0800 Subject: [PATCH 17/18] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E6=9C=8D=E5=8A=A1=E5=99=A8=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E9=80=BB=E8=BE=91,=E6=B7=BB=E5=8A=A0=E4=B8=BB=E5=8A=A8?= =?UTF-8?q?=E6=96=AD=E5=BC=80=E8=AE=BE=E5=A4=87=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mithqtt-broker/src/dist/config/broker.properties | 4 ++-- mithqtt-broker/src/dist/config/mongo.properties | 14 ++++++++------ .../src/dist/config/authenticator.properties | 7 ++++++- .../src/dist/config/communicator.properties | 2 +- mithqtt-http/src/dist/config/redis.properties | 2 +- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/mithqtt-broker/src/dist/config/broker.properties b/mithqtt-broker/src/dist/config/broker.properties index ac0294d..92ea89a 100644 --- a/mithqtt-broker/src/dist/config/broker.properties +++ b/mithqtt-broker/src/dist/config/broker.properties @@ -11,7 +11,7 @@ mqtt.host = 0.0.0.0 # This is the network port the broker will bind to # The MQTT Protocol Specification recommended using port 1883 -mqtt.port=1884 +mqtt.port=1883 # To use ssl in the connection, set this to true # Must provide an X.509 certificate chain file in PEM format @@ -23,7 +23,7 @@ mqtt.ssl.enabled = false mqtt.ssl.port = 8883 # X.509 certificate chain file path -mqtt.ssl.certPath =E://SSL/ze/server.jks +mqtt.ssl.certPath=ssl/server.jks # The password of the key File mqtt.ssl.password = zenintec diff --git a/mithqtt-broker/src/dist/config/mongo.properties b/mithqtt-broker/src/dist/config/mongo.properties index a0f68a3..81891b5 100644 --- a/mithqtt-broker/src/dist/config/mongo.properties +++ b/mithqtt-broker/src/dist/config/mongo.properties @@ -4,15 +4,17 @@ # Storage implementation (full qualified class name) storage.class = io.j1st.power.storage.mongo.MongoStorage - +## MongoDB storage configuration +#mongo.address=localhost:27017 +#mongo.database=power +#mongo.userName=jack +#mongo.password=123456 # MongoDB storage configuration -mongo.address=localhost:27017 +mongo.address=139.196.235.75:27017 mongo.database=power -mongo.userName=jack -mongo.password=123456 - - +mongo.userName=test +mongo.password=test # Allow '$' in topic allowDollar = false diff --git a/mithqtt-http/src/dist/config/authenticator.properties b/mithqtt-http/src/dist/config/authenticator.properties index 198cb2f..cc4c02f 100644 --- a/mithqtt-http/src/dist/config/authenticator.properties +++ b/mithqtt-http/src/dist/config/authenticator.properties @@ -3,7 +3,7 @@ # Authenticator # Authenticator implementation (full qualified class name) -authenticator.class = com.github.longkerdandy.mithqtt.authenticator.dummy.DummyAuthenticator +authenticator.class=io.j1st.mithqtt.authenticator.power.PowerAuthenticator # Dummy @@ -13,3 +13,8 @@ allowDollar = false # Topic will be rejected in PUBLISH and SUBSCRIBE deniedTopic = nosubscribe +#Mongodb +mongo.address=localhost:27017 +mongo.database=power +mongo.userName=zenin +mongo.password=zenin \ No newline at end of file diff --git a/mithqtt-http/src/dist/config/communicator.properties b/mithqtt-http/src/dist/config/communicator.properties index 469bbb4..d6c79b7 100644 --- a/mithqtt-http/src/dist/config/communicator.properties +++ b/mithqtt-http/src/dist/config/communicator.properties @@ -24,7 +24,7 @@ rabbitmq.virtualHost = / # Server addresses # In the format like host1[:port1],host2[:port2] -rabbitmq.addresses = localhost +rabbitmq.addresses=localhost:5672 # Queue name for application communicator # ONLY APPLIES TO RabbitMQApplicationCommunicator diff --git a/mithqtt-http/src/dist/config/redis.properties b/mithqtt-http/src/dist/config/redis.properties index 85fc16f..1cfd1fc 100644 --- a/mithqtt-http/src/dist/config/redis.properties +++ b/mithqtt-http/src/dist/config/redis.properties @@ -42,7 +42,7 @@ redis.type = single # 2. 'master_slave' : host[:port][,host2[:port2]] # the 1st should be the master node # 3. 'sentinel' : host[:port][,host2[:port2]] # the 1st should be the master node, this is the sentinel address # 4. 'cluster' : host[:port][,host2[:port2]] -redis.address = 127.0.0.1:6379 +redis.address=localhost:6379 # Redis database number redis.database = 0 From 4ebb49f930f7483cb20f2c54ae0c6a108f1196bb Mon Sep 17 00:00:00 2001 From: 12315jack Date: Wed, 18 Oct 2017 13:23:17 +0800 Subject: [PATCH 18/18] Merge branch 'master' of https://github.com/zenin-tech/mithqtt into #connectionLogic Conflicts: mithqtt-broker/src/dist/config/broker.properties mithqtt-broker/src/dist/config/mongo.properties --- .../io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java | 1 + 1 file changed, 1 insertion(+) diff --git a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java index acea51f..c0876ba 100644 --- a/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java +++ b/mithqtt-authenticator-dummy/src/main/java/io/j1st/mithqtt/authenticator/power/PowerAuthenticator.java @@ -94,6 +94,7 @@ public AuthorizeResult authPublish(String clientId, String userName, String topi if (this.mongoStorage.isDisableAgent(new ObjectId(clientId), AgentStatus.DISABLED.value())) { return AuthorizeResult.FORBIDDEN; } + return AuthorizeResult.OK; }