diff --git a/distribution/conf/meta.conf b/distribution/conf/meta.conf index d4ee41852..2774315b6 100644 --- a/distribution/conf/meta.conf +++ b/distribution/conf/meta.conf @@ -14,5 +14,5 @@ # limitations under the License. -selfAddress= -membersAddress= +selfAddress=11.159.23.108:25000 +membersAddress=11.159.23.108:25000,11.159.23.111:25000,11.159.23.104:25000 diff --git a/distribution/conf/service.conf b/distribution/conf/service.conf index 7ac8f51a3..07dd285ac 100644 --- a/distribution/conf/service.conf +++ b/distribution/conf/service.conf @@ -14,11 +14,11 @@ # limitations under the License. -username= -secretKey= +username=test +secretKey=test -NAMESRV_ADDR= -eventNotifyRetryTopic= -clientRetryTopic= +NAMESRV_ADDR=11.159.23.108:9876 +eventNotifyRetryTopic=eventNotifyRetryTopic +clientRetryTopic=clientRetryTopic -metaAddr= \ No newline at end of file +metaAddr=11.159.23.108:25000,11.159.23.111:25000,11.159.23.104:25000 \ No newline at end of file diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapMessage.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapMessage.java new file mode 100644 index 000000000..562b61b8b --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapMessage.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.common.model; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class CoapMessage { + private int version; + private CoapMessageType type; + private int tokenLength; + private CoapMessageCode code; + private int messageId; + private byte[] token; + private List options = new ArrayList<>(); + private byte[] payload; + private InetSocketAddress remoteAddress; + + public CoapMessage(int version, CoapMessageType type, int tokenLength, CoapMessageCode code, int messageId, byte[] token, byte[] payload, InetSocketAddress remoteAddress) { + this.version = version; + this.type = type; + this.tokenLength = tokenLength; + this.code = code; + this.messageId = messageId; + this.token = token; + this.payload = payload; + this.remoteAddress = remoteAddress; + } + + public CoapMessage(int version, CoapMessageType type, int tokenLength, CoapMessageCode code, int messageId, byte[] token, InetSocketAddress remoteAddress) { + this.version = version; + this.type = type; + this.tokenLength = tokenLength; + this.code = code; + this.messageId = messageId; + this.token = token; + this.remoteAddress = remoteAddress; + } + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + public CoapMessageType getType() { + return type; + } + + public void setType(CoapMessageType type) { + this.type = type; + } + + public int getTokenLength() { + return tokenLength; + } + + public void setTokenLength(int tokenLength) { + this.tokenLength = tokenLength; + } + + public CoapMessageCode getCode() { + return code; + } + + public void setCode(CoapMessageCode code) { + this.code = code; + } + + public int getMessageId() { + return messageId; + } + + public void setMessageId(int messageId) { + this.messageId = messageId; + } + + public byte[] getToken() { + return token; + } + + public void setToken(byte[] token) { + this.token = token; + } + + public List getOptions() { + return options; + } + + public void clearOptions() { + this.options.clear(); + } + + public void setOptions(List options) { + this.options = options; + } + + public void addOption(CoapMessageOption option) { + this.options.add(option); + } + + public void addObserveOption(int value) { + this.options.add(new CoapMessageOption(CoapMessageOptionNumber.OBSERVE, intToByteArray(value))); + } + + public byte[] getPayload() { + return payload; + } + + public void setPayload(byte[] payload) { + this.payload = payload; + } + + public InetSocketAddress getRemoteAddress() { + return remoteAddress; + } + + public void setRemoteAddress(InetSocketAddress remoteAddress) { + this.remoteAddress = remoteAddress; + } + + private byte[] intToByteArray(int value) { + byte[] byteArray = new byte[3]; + byteArray[0] = (byte) (value >> 16); + byteArray[1] = (byte) (value >> 8); + byteArray[2] = (byte) value; + return byteArray; + } + + + @Override + public String toString() { + return "CoapMessage{" + + "version=" + version + + ", type=" + type + + ", tokenLength=" + tokenLength + + ", code=" + code + + ", messageId=" + messageId + + ", token=" + Arrays.toString(token) + + ", options=" + options + + ", payload=" + Arrays.toString(payload) + + ", remoteAddress=" + remoteAddress + + '}'; + } +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapMessageCode.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapMessageCode.java new file mode 100644 index 000000000..22c8b54b8 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapMessageCode.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.common.model; + +public enum CoapMessageCode { + EMPTY(0), + + // Request Code, 0.xx + GET(1), + POST(2), + PUT(3), + DELETE(4), + + // Response Success Code, 2.xx + CREATED(65), + DELETED(66), + Valid(67), + CHANGED(68), + CONTENT(69), + + // Response Client Error Code, 4.xx + BAD_REQUEST(128), + UNAUTHORIZED(129), + BAD_OPTION(130), + FORBIDDEN(131), + NOT_FOUND(132), + METHOD_NOT_ALLOWED(133), + NOT_ACCEPTABLE(134), + PRECONDITION_FAILED(140), + REQUEST_ENTITY_TOO_LARGE(141), + UNSUPPORTED_CONTENT_FORMAT(143), + + // Response Server Error Code, 5.xx + INTERNAL_SERVER_ERROR(160), + NOT_IMPLEMENTED(161), + BAD_GATEWAY(162), + SERVICE_UNAVAILABLE(163), + GATEWAY_TIMEOUT(164), + PROXYING_NOT_SUPPORTED(165); + + private static final CoapMessageCode[] VALUES; + private final int value; + + private CoapMessageCode(int value) { + this.value = value; + } + + public int value() { + return this.value; + } + + public static CoapMessageCode valueOf(int code) { + if (code >= 0 && code < VALUES.length && VALUES[code] != null) { + return VALUES[code]; + } else { + throw new IllegalArgumentException("Unknown CoapMessageCode " + code); + } + } + + public static boolean isRequestCode(CoapMessageCode code) { + return (code == GET) || (code == POST) || (code == PUT) || (code == DELETE); + } + + public static boolean isEmptyCode(CoapMessageCode code) { + return code == EMPTY; + } + + static { + CoapMessageCode[] values = values(); + VALUES = new CoapMessageCode[192]; // Using 192 since the highest defined code is 192 + + for (CoapMessageCode code : values) { + int value = code.value; + if (VALUES[value] != null) { + throw new AssertionError("Value already in use: " + value + " by " + VALUES[value]); + } + VALUES[value] = code; + } + } + + +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapMessageOption.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapMessageOption.java new file mode 100644 index 000000000..3d38a8a8d --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapMessageOption.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.common.model; + +public class CoapMessageOption { + private CoapMessageOptionNumber optionNumber; + private byte[] optionValue; + + public CoapMessageOption(CoapMessageOptionNumber optionNumber, byte[] optionValue) { + this.optionNumber = optionNumber; + this.optionValue = optionValue; + } + + public CoapMessageOption(int optionNumber, byte[] optionValue) { + this(CoapMessageOptionNumber.valueOf(optionNumber), optionValue); + } + + public CoapMessageOptionNumber getOptionNumber() { + return optionNumber; + } + + public void setOptionNumber(CoapMessageOptionNumber optionNumber) { + this.optionNumber = optionNumber; + } + + public byte[] getOptionValue() { + return optionValue; + } + + public void setOptionValue(byte[] optionValue) { + this.optionValue = optionValue; + } +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapMessageOptionNumber.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapMessageOptionNumber.java new file mode 100644 index 000000000..7c4dd07d5 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapMessageOptionNumber.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.common.model; + +public enum CoapMessageOptionNumber { + IF_MATCH(1), + URI_HOST(3), + ETAG(4), + IF_NONE_MATCH(5), + OBSERVE(6), + URI_PORT(7), + LOCATION_PATH(8), + URI_PATH(11), + CONTENT_FORMAT(12), + MAX_AGE(14), + URI_QUERY(15), + ACCEPT(17), + LOCATION_QUERY(20), + BLOCK_2(23), + BLOCK_1(27), + SIZE_2(28), + PROXY_URI(35), + PROXY_SCHEME(39), + SIZE_1(60), + REQUST_TAG(292); + + private static final CoapMessageOptionNumber[] VALUES; + private final int value; + + private CoapMessageOptionNumber(int value) { + this.value = value; + } + + public int value() { + return this.value; + } + + public static boolean isValid(int number) { + return number > 0 && number < 293 && VALUES[number] != null; + } + + public static CoapMessageOptionNumber valueOf(int number) { + if (number > 0 && number < 293 && VALUES[number] != null) { + return VALUES[number]; + } else { + throw new IllegalArgumentException("Unknown CoapMessageOptionNumber " + number); + } + } + + static { + CoapMessageOptionNumber[] values = values(); + VALUES = new CoapMessageOptionNumber[293]; + + for (CoapMessageOptionNumber number : values) { + int value = number.value; + if (VALUES[value] != null) { + throw new AssertionError("Value already in use: " + value + " by " + VALUES[value]); + } + VALUES[value] = number; + } + } +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapMessageType.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapMessageType.java new file mode 100644 index 000000000..744942238 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapMessageType.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.common.model; + +public enum CoapMessageType { + CON(0), + NON(1), + ACK(2), + RST(3); + + private static final CoapMessageType[] VALUES; + private final int value; + + private CoapMessageType(int value) { + this.value = value; + } + + public int value() { + return this.value; + } + + public static CoapMessageType valueOf(int type) { + if (type >= 0 && type < VALUES.length) { + return VALUES[type]; + } else { + throw new IllegalArgumentException("Unknown CoapMessageType " + type); + } + } + + static { + CoapMessageType[] values = values(); + VALUES = new CoapMessageType[values.length + 1]; + + for (CoapMessageType type : values) { + int value = type.value; + if (VALUES[value] != null) { + throw new AssertionError("Value already in use: " + value + " by " + VALUES[value]); + } + VALUES[value] = type; + } + } + +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapRequestMessage.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapRequestMessage.java new file mode 100644 index 000000000..086b8caea --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapRequestMessage.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.common.model; + +import io.netty.handler.codec.mqtt.MqttQoS; + +import java.net.InetSocketAddress; + +public class CoapRequestMessage extends CoapMessage { + + private CoapRequestType requestType; + private String topic; + private String clientId; + private MqttQoS qosLevel = MqttQoS.AT_MOST_ONCE; + private boolean isReatin; + private int expiry; + private String authToken; + private String userName; + private String password; + + + public CoapRequestMessage(int version, CoapMessageType type, int tokenLength, CoapMessageCode code, int messageId, byte[] token, byte[] payload, InetSocketAddress remoteAddress) { + super(version, type, tokenLength, code, messageId, token, payload, remoteAddress); + } + + public CoapRequestMessage(int version, CoapMessageType type, int tokenLength, CoapMessageCode code, int messageId, byte[] token, InetSocketAddress remoteAddress) { + super(version, type, tokenLength, code, messageId, token, remoteAddress); + } + + public CoapRequestMessage copy() { + CoapRequestMessage msg = new CoapRequestMessage(getVersion(), getType(), getTokenLength(), getCode(), getMessageId(), getToken(), getPayload(), getRemoteAddress()); + msg.setRequestType(requestType); + msg.setTopic(topic); + msg.setClientId(clientId); + msg.setQosLevel(qosLevel); + msg.setReatin(isReatin); + msg.setExpiry(expiry); + msg.setAuthToken(authToken); + msg.setUserName(userName); + msg.setPassword(password); + return msg; + } + + public CoapRequestType getRequestType() { + return requestType; + } + + public void setRequestType(CoapRequestType requestType) { + this.requestType = requestType; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public MqttQoS getQosLevel() { + return qosLevel; + } + + public void setQosLevel(MqttQoS qosLevel) { + this.qosLevel = qosLevel; + } + + public boolean isReatin() { + return isReatin; + } + + public void setReatin(boolean reatin) { + isReatin = reatin; + } + + public int getExpiry() { + return expiry; + } + + public void setExpiry(int expiry) { + this.expiry = expiry; + } + + public String getAuthToken() { + return authToken; + } + + public void setAuthToken(String authToken) { + this.authToken = authToken; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapRequestType.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapRequestType.java new file mode 100644 index 000000000..fefe46e77 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/CoapRequestType.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.common.model; + +public enum CoapRequestType { + ACK(0), + + PUBLISH(1), + SUBSCRIBE(2), + + CONNECT(3), + HEARTBEAT(4), + DISCONNECT(5); + + private static final CoapRequestType[] VALUES; + private final int value; + + private CoapRequestType(int value) { + this.value = value; + } + + public int value() { + return this.value; + } + + public static CoapRequestType valueOf(int type) { + if (type >= 0 && type < VALUES.length) { + return VALUES[type]; + } else { + throw new IllegalArgumentException("Unknown CoapRequestType " + type); + } + } + + static { + CoapRequestType[] values = values(); + VALUES = new CoapRequestType[values.length + 1]; + + for (CoapRequestType type : values) { + int value = type.value; + if (VALUES[value] != null) { + throw new AssertionError("Value already in use: " + value + " by " + VALUES[value]); + } + VALUES[value] = type; + } + } + +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Constants.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Constants.java index abc86d3b2..12e23ab4d 100644 --- a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Constants.java +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/Constants.java @@ -73,4 +73,19 @@ public class Constants { public static final String CLIENT_EVENT_ORIGIN_TOPIC = MQTT_SYSTEM_TOPIC + MQTT_TOPIC_DELIMITER + CLIENT_EVENT_SECOND_TOPIC; public static final int CLIENT_EVENT_BATCH_SIZE = 100; + + public static final int COAP_VERSION = 1; + public static final int COAP_PAYLOAD_MARKER = 0xFF; + public static final int COAP_MAX_TOKEN_LENGTH = 8; + public static final String COAP_QUERY_DELIMITER = "="; + public static final String COAP_PS_PREFIX = "ps"; + public static final String COAP_CONNECTION_PREFIX_1 = "mqtt"; + public static final String COAP_CONNECTION_PREFIX_2 = "connection"; + public static final String COAP_QUERY_CLIENT_ID = "clientid"; + public static final String COAP_QUERY_QOS = "qos"; + public static final String COAP_QUERY_RETAIN = "retain"; + public static final String COAP_QUERY_EXPIRY = "expiry"; + public static final String COAP_QUERY_USER_NAME = "username"; + public static final String COAP_QUERY_PASSWORD = "password"; + public static final String COAP_AUTH_TOKEN = "token"; } diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/RpcCode.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/RpcCode.java index d08d36a17..326cbfdef 100644 --- a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/RpcCode.java +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/model/RpcCode.java @@ -23,4 +23,6 @@ public class RpcCode { public static final int CMD_NOTIFY_MQTT_MESSAGE = 201; public static final int CMD_CLOSE_CHANNEL = 203; + + public static final int COM_NOTIFY_COAP_MESSAGE = 301; } diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/CoapTokenUtil.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/CoapTokenUtil.java new file mode 100644 index 000000000..f41508067 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/CoapTokenUtil.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.mqtt.common.util; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class CoapTokenUtil { + + private static final String ALGORITHM = "AES"; + private static final String SECRET_KEY = "mySecretKey12345"; + private static final long EXPIRATION_TIME = 600000; + + public static String generateToken(String clientId) throws Exception { + long timestamp = System.currentTimeMillis(); + String data = clientId + ":" + timestamp; + return encrypt(data, SECRET_KEY); + } + + public static boolean isValid(String clientId, String token) { + try { + String decryptedData = decrypt(token, SECRET_KEY); + String[] parts = decryptedData.split(":"); + if (parts.length != 2) { + return false; + } + + String decryptedClientId = parts[0]; + long timestamp = Long.parseLong(parts[1]); + if (!decryptedClientId.equals(clientId)) { + return false; + } + long currentTime = System.currentTimeMillis(); + return currentTime - timestamp <= EXPIRATION_TIME; + + } catch (Exception e) { + return false; + } + } + + private static String encrypt(String data, String key) throws Exception { + Cipher cipher = Cipher.getInstance(ALGORITHM); + SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + + byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(encryptedBytes); + } + + private static String decrypt(String token, String key) throws Exception { + Cipher cipher = Cipher.getInstance(ALGORITHM); + SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + + byte[] decryptedBytes = cipher.doFinal(Base64.getUrlDecoder().decode(token)); + return new String(decryptedBytes, StandardCharsets.UTF_8); + } +} diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/MessageUtil.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/MessageUtil.java index 6b864686d..d1d895722 100644 --- a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/MessageUtil.java +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/MessageUtil.java @@ -32,6 +32,7 @@ import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.util.CharsetUtil; import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; import org.apache.rocketmq.mqtt.common.model.Message; import java.nio.ByteBuffer; @@ -137,6 +138,21 @@ public static Message toMessage(MqttPublishMessage mqttMessage) { return message; } + public static Message toMessage(CoapRequestMessage coapMessage) { + Message message = new Message(); + message.setFirstTopic(TopicUtils.decode(coapMessage.getTopic()).getFirstTopic()); + message.setOriginTopic(coapMessage.getTopic()); + message.setRetained(coapMessage.isReatin()); + message.putUserProperty(Message.extPropertyQoS, String.valueOf(coapMessage.getQosLevel().value())); + int readableBytes = coapMessage.getPayload().length; + byte[] body = new byte[readableBytes]; + System.arraycopy(coapMessage.getPayload(), 0, body, 0, readableBytes); + message.setPayload(body); + + // todo: add other properties, topicAlias, expiry, etc + return message; + } + public static MqttPublishMessage removeRetainedFlag(MqttPublishMessage mqttPublishMessage) { MqttFixedHeader tmpFixHeader = mqttPublishMessage.fixedHeader(); mqttPublishMessage = new MqttPublishMessage(new MqttFixedHeader(tmpFixHeader.messageType(), tmpFixHeader.isDup(), tmpFixHeader.qosLevel(), false, tmpFixHeader.remainingLength()), diff --git a/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/PasswordHashUtil.java b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/PasswordHashUtil.java new file mode 100644 index 000000000..f001f9811 --- /dev/null +++ b/mqtt-common/src/main/java/org/apache/rocketmq/mqtt/common/util/PasswordHashUtil.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.common.util; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; + +public class PasswordHashUtil { + + // Generate random salt. + public static String generateSalt(int length) { + byte[] salt = new byte[length]; + new SecureRandom().nextBytes(salt); + return Base64.getEncoder().encodeToString(salt); + } + + // Hash with salt, algorithm must be String of "MD5"/"SHA"/"SHA-256"/"SHA-512". + public static String hashWithSalt(String password, String salt, String algorithm, String saltPosition) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance(algorithm); + switch (saltPosition) { + case "prefix": + md.update(salt.getBytes(StandardCharsets.UTF_8)); + md.update(password.getBytes(StandardCharsets.UTF_8)); + break; + case "suffix": + md.update(password.getBytes(StandardCharsets.UTF_8)); + md.update(salt.getBytes(StandardCharsets.UTF_8)); + break; + case "disable": + md.update(password.getBytes(StandardCharsets.UTF_8)); + default: + throw new IllegalArgumentException("Invalidsalt position: " + saltPosition); + } + byte[] hashedPassword = md.digest(); + return Base64.getEncoder().encodeToString(hashedPassword); + } + + + public static boolean validatePassword(String password, String storedHash, String salt, String algorithm, String saltPosition) throws NoSuchAlgorithmException { + if (algorithm.equals("PLAIN")) { + return password.equals(storedHash); + } + return hashWithSalt(password, salt, algorithm, saltPosition).equals(storedHash); + } + + +} diff --git a/mqtt-common/src/test/java/org/apache/rocketmq/mqtt/common/test/util/TestCoapTokenUtil.java b/mqtt-common/src/test/java/org/apache/rocketmq/mqtt/common/test/util/TestCoapTokenUtil.java new file mode 100644 index 000000000..3e7c8df57 --- /dev/null +++ b/mqtt-common/src/test/java/org/apache/rocketmq/mqtt/common/test/util/TestCoapTokenUtil.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.common.test.util; + +import org.apache.rocketmq.mqtt.common.util.CoapTokenUtil; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class TestCoapTokenUtil { + + @Test + public void test() throws Exception { + String clientId = "client123"; + String token = CoapTokenUtil.generateToken(clientId); + assertNotNull(token); + assertTrue(CoapTokenUtil.isValid(clientId, token)); + } + +} diff --git a/mqtt-common/src/test/java/org/apache/rocketmq/mqtt/common/test/util/TestPasswordHashUtil.java b/mqtt-common/src/test/java/org/apache/rocketmq/mqtt/common/test/util/TestPasswordHashUtil.java new file mode 100644 index 000000000..44e7f5928 --- /dev/null +++ b/mqtt-common/src/test/java/org/apache/rocketmq/mqtt/common/test/util/TestPasswordHashUtil.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.common.test.util; + +import org.apache.rocketmq.mqtt.common.util.PasswordHashUtil; +import org.junit.Test; + +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class TestPasswordHashUtil { + + @Test + public void testGenerateSalt() { + int saltLength = 16; + String salt = PasswordHashUtil.generateSalt(saltLength); + assertEquals(saltLength, Base64.getDecoder().decode(salt).length); + } + + @Test + public void testHashWithSalt() throws NoSuchAlgorithmException { + String password = "password"; + String salt = PasswordHashUtil.generateSalt(16); + String algorithm = "SHA-256"; + String saltPosition = "suffix"; + + String hashPassword = PasswordHashUtil.hashWithSalt(password, salt, algorithm, saltPosition); + assertNotNull(hashPassword); + assertNotEquals(password, hashPassword); + + String hashPassword2 = PasswordHashUtil.hashWithSalt(password, salt, algorithm, saltPosition); + assertEquals(hashPassword, hashPassword2); + } + + @Test + public void testValidatePasswordPlain() throws Exception { + String password = "password"; + assertTrue(PasswordHashUtil.validatePassword(password, password, null, "PLAIN", null)); + } + + @Test + public void testValidatePasswordHash() throws Exception { + String password = "password"; + String salt = PasswordHashUtil.generateSalt(16); + String hashPassword = PasswordHashUtil.hashWithSalt(password, salt, "SHA-256", "suffix"); + assertTrue(PasswordHashUtil.validatePassword(password, hashPassword, salt, "SHA-256", "suffix")); + } +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/DatagramChannelManager.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/DatagramChannelManager.java new file mode 100644 index 000000000..c4b02fe17 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/channel/DatagramChannelManager.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.channel; + +import io.netty.channel.socket.DatagramChannel; +import org.apache.rocketmq.mqtt.common.model.CoapMessage; +import org.apache.rocketmq.mqtt.common.model.CoapMessageType; +import org.apache.rocketmq.mqtt.cs.session.CoapSession; +import org.apache.rocketmq.mqtt.cs.session.infly.CoapResponseCache; +import org.apache.rocketmq.mqtt.cs.session.infly.CoapRetryManager; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +@Component +public class DatagramChannelManager { + + @Resource + private CoapResponseCache coapResponseCache; + + @Resource + private CoapRetryManager coapRetryManager; + + private DatagramChannel channel; + + public void setChannel(DatagramChannel channel) { + this.channel = channel; + } + + public DatagramChannel getChannel() { + return channel; + } + + public void write(CoapMessage message) { + channel.writeAndFlush(message); + } + + // Write to channel and add to response cache. + public void writeResponse(CoapMessage message) { + channel.writeAndFlush(message); + coapResponseCache.put(message); + } + + // Write to channel and add to retry manager. + public void pushMessage(CoapSession session, CoapMessage message) { + channel.writeAndFlush(message); + if (message.getType() == CoapMessageType.CON) { + coapRetryManager.addRetryMessage(session, message); + } + } +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/config/ConnectConf.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/config/ConnectConf.java index 904b66d55..8921b4870 100644 --- a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/config/ConnectConf.java +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/config/ConnectConf.java @@ -49,8 +49,13 @@ public class ConnectConf { private int mqttWsPort = 8888; private int quicPort = 14567; + + private int coapPort = 5683; + private boolean enableQuic = false; + private boolean enableCoapConnect = false; + private boolean enableTlsSever = false; private boolean needClientAuth = false; private String sslCaCertFile; @@ -80,6 +85,8 @@ public class ConnectConf { private int serverReceiveMaximum = 32767; private int maxTransferCountOnMessageInDisk = 8; + private long coapSessionTimeout = 90000; //90000ms,90s + public ConnectConf() throws IOException { ClassPathResource classPathResource = new ClassPathResource(CONF_FILE_NAME); InputStream in = classPathResource.getInputStream(); @@ -126,6 +133,14 @@ public int getMqttWsPort() { return mqttWsPort; } + public int getCoapPort() { + return coapPort; + } + + public void setCoapPort(int coapPort) { + this.coapPort = coapPort; + } + public boolean isEnableTlsSever() { return enableTlsSever; } @@ -353,4 +368,20 @@ public boolean isEnableQuic() { public void setEnableQuic(boolean enableQuic) { this.enableQuic = enableQuic; } + + public long getCoapSessionTimeout() { + return coapSessionTimeout; + } + + public void setCoapSessionTimeout(long coapSessionTimeout) { + this.coapSessionTimeout = coapSessionTimeout; + } + + public boolean isEnableCoapConnect() { + return enableCoapConnect; + } + + public void setEnableCoapConnect(boolean enableCoapConnect) { + this.enableCoapConnect = enableCoapConnect; + } } diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/CoapPacketHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/CoapPacketHandler.java new file mode 100644 index 000000000..d5c8bd5c0 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/CoapPacketHandler.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.protocol; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.CoapMessage; + +public interface CoapPacketHandler { + + /** + * preHandler before upstream processor, for preprocessing + * + * @param ctx + * @param coapMessage + * @return + */ + boolean preHandler(ChannelHandlerContext ctx, T coapMessage); + + /** + * doHandler after upstream processor + * + * @param ctx + * @param coapMessage + * @param upstreamHookResult + */ + void doHandler(ChannelHandlerContext ctx, T coapMessage, HookResult upstreamHookResult); + +} \ No newline at end of file diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/CoapDecoder.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/CoapDecoder.java new file mode 100644 index 000000000..803e90794 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/CoapDecoder.java @@ -0,0 +1,326 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.protocol.coap; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.DatagramPacket; +import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.handler.codec.mqtt.MqttQoS; +import org.apache.rocketmq.mqtt.common.model.CoapMessage; +import org.apache.rocketmq.mqtt.common.model.CoapMessageCode; +import org.apache.rocketmq.mqtt.common.model.CoapMessageOption; +import org.apache.rocketmq.mqtt.common.model.CoapMessageOptionNumber; +import org.apache.rocketmq.mqtt.common.model.CoapMessageType; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.common.model.CoapRequestType; +import org.apache.rocketmq.mqtt.common.model.Constants; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; + +import javax.annotation.Resource; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + + +public class CoapDecoder extends MessageToMessageDecoder { + + @Resource + private DatagramChannelManager datagramChannelManager; + + private CoapMessageType coapType; + private int coapTokenLength; + private CoapMessageCode coapCode; + private int coapMessageId; + private byte[] coapToken; + private byte[] coapPayload; + InetSocketAddress remoteAddress; + private boolean isObserve; + + private String errorContent; + private CoapMessageCode errorCode; + + @Override + public void decode(ChannelHandlerContext ctx, DatagramPacket packet, List out) { + // Get data and remote address + ByteBuf in = packet.content(); + remoteAddress = packet.sender(); + + // The length of Coap message is at least 4 bytes. + if (in.readableBytes() < 4) { + errorCode = CoapMessageCode.BAD_REQUEST; + errorContent = "Format-Error: The length of header must be at least 4 bytes!"; + sendErrorResponse(); + in.skipBytes(in.readableBytes()); // Skip unread bytes + return; + } + + // Handle first byte, including version, type, and token length. + int firstByte = in.readUnsignedByte(); + int version = (firstByte >> 6) & 0x03; + if (version != Constants.COAP_VERSION) { + errorCode = CoapMessageCode.BAD_REQUEST; + errorContent = "Format-Error: Version must be 1!"; + sendErrorResponse(); + in.skipBytes(in.readableBytes()); // Skip unread bytes + return; + } + coapType = CoapMessageType.valueOf((firstByte >> 4) & 0x03); + coapTokenLength = firstByte & 0x0F; + if (coapTokenLength > Constants.COAP_MAX_TOKEN_LENGTH) { + errorCode = CoapMessageCode.BAD_REQUEST; + errorContent = "Format-Error: The length of token is too long!"; + sendErrorResponse(); + in.skipBytes(in.readableBytes()); // Skip unread bytes + return; + } + try { + coapCode = CoapMessageCode.valueOf(in.readUnsignedByte()); + } catch (IllegalArgumentException e) { + errorCode = CoapMessageCode.BAD_REQUEST; + errorContent = "Format-Error: The code is not defined!"; + sendErrorResponse(); + in.skipBytes(in.readableBytes()); // Skip unread bytes + return; + } + if (!CoapMessageCode.isRequestCode(coapCode) && !CoapMessageCode.isEmptyCode(coapCode)) { + errorCode = CoapMessageCode.BAD_REQUEST; + errorContent = "Format-Error: The code must be a request code!"; + sendErrorResponse(); + in.skipBytes(in.readableBytes()); // Skip unread bytes + return; + } + + // Handle messageID. + coapMessageId = in.readUnsignedShort(); + + // Handle token. + if (in.readableBytes() < coapTokenLength) { + errorCode = CoapMessageCode.BAD_REQUEST; // Return 4.00 Response + errorContent = "Format-Error: The length of remaining readable bytes is less than tokenLength!"; + sendErrorResponse(); + in.skipBytes(in.readableBytes()); // Skip unread bytes + return; + } + coapToken = new byte[coapTokenLength]; + in.readBytes(coapToken); + + // Construct request message. + CoapRequestMessage coapMessage = new CoapRequestMessage(version, coapType, coapTokenLength, coapCode, coapMessageId, coapToken, remoteAddress); + + // Handle ACK, which is an empty message. + if (coapType == CoapMessageType.ACK) { + coapMessage.setRequestType(CoapRequestType.ACK); + ctx.fireChannelRead(coapMessage); + return; + } + + // Handle options. There may be multiple options, each containing option delta, option length and option value. The end of options is marked by payload marker. + int nextByte; + int optionNumber = 0; + List uriPaths = new ArrayList<>(); + while (in.readableBytes() > 0) { + nextByte = in.readUnsignedByte(); + // Terminate when meeting payload marker. + if (nextByte == Constants.COAP_PAYLOAD_MARKER) { + break; + } + // Handle optionDelta and optionLength. + int optionDelta = nextByte >> 4; + int optionLength = nextByte & 0x0F; + // Add up extended delta if delta is larger than 12. + if (optionDelta == 13) { + optionDelta += in.readUnsignedByte(); + } else if (optionDelta == 14) { + optionDelta += 255 + in.readUnsignedShort(); + } else if (optionDelta == 15) { + errorCode = CoapMessageCode.BAD_REQUEST; // Return 4.00 Response + errorContent = "Format-Error: OptionDelta can not be 15!"; + sendErrorResponse(); + in.skipBytes(in.readableBytes()); + return; + } + optionNumber += optionDelta; // current optionNumber = last optionNumber + optionDelta + if (!CoapMessageOptionNumber.isValid(optionNumber)) { + errorCode = CoapMessageCode.BAD_OPTION; // Return 4.02 Response + errorContent = "Format-Error: Option number is not defined!"; + sendErrorResponse(); + in.skipBytes(in.readableBytes()); + return; + } + // Add up extended length if length is larger than 12. + if (optionLength == 13) { + optionLength += in.readUnsignedByte(); + } else if (optionLength == 14) { + optionLength += 255 + in.readUnsignedShort(); + } else if (optionLength == 15) { + errorCode = CoapMessageCode.BAD_REQUEST; // Return 4.00 Response + errorContent = "Format-Error: OptionLength can not be 15!"; + sendErrorResponse(); + in.skipBytes(in.readableBytes()); + return; + } + if (in.readableBytes() < optionLength) { + errorCode = CoapMessageCode.BAD_REQUEST; // Return 4.00 Response + errorContent = "Format-Error: The number of readable bytes is less than optionLength"; + sendErrorResponse(); + in.skipBytes(in.readableBytes()); + return; + } + + // Handle option value. Need further process if optionNumber is URI_PATH, URI_QUERY or OBSERVE. + byte[] optionValue = new byte[optionLength]; + in.readBytes(optionValue); + if (optionNumber == CoapMessageOptionNumber.URI_PATH.value()) { + uriPaths.add(new String(optionValue, StandardCharsets.UTF_8)); + } + if (optionNumber == CoapMessageOptionNumber.URI_QUERY.value()) { + String query = new String(optionValue, StandardCharsets.UTF_8); + String[] parts = query.split(Constants.COAP_QUERY_DELIMITER, 2); + if (parts.length != 2) { + errorCode = CoapMessageCode.BAD_REQUEST; // Return 4.00 Response + errorContent = "Format-Error: The Format of Observe is not correct!"; + sendErrorResponse(); + in.skipBytes(in.readableBytes()); // Skip unread bytes + return; + } + switch (parts[0]) { + case Constants.COAP_QUERY_CLIENT_ID: + coapMessage.setClientId(parts[1]); + break; + case Constants.COAP_QUERY_QOS: + coapMessage.setQosLevel(MqttQoS.valueOf(Integer.parseInt(parts[1]))); + break; + case Constants.COAP_QUERY_RETAIN: + coapMessage.setReatin(Boolean.parseBoolean(parts[1])); + break; + case Constants.COAP_QUERY_EXPIRY: + coapMessage.setExpiry(Integer.parseInt(parts[1])); + break; + case Constants.COAP_QUERY_USER_NAME: + coapMessage.setUserName(parts[1]); + case Constants.COAP_QUERY_PASSWORD: + coapMessage.setPassword(parts[1]); + case Constants.COAP_AUTH_TOKEN: + coapMessage.setAuthToken(parts[1]); + default: + break; + } + } + if (optionNumber == CoapMessageOptionNumber.OBSERVE.value()) { + if (optionValue.length == 0) { + isObserve = true; + } else { + errorCode = CoapMessageCode.BAD_REQUEST; // Return 4.00 Response + errorContent = "Format-Error: The Format of Observe is not correct!"; + sendErrorResponse(); + in.skipBytes(in.readableBytes()); // Skip unread bytes + return; + } + } + coapMessage.addOption(new CoapMessageOption(optionNumber, optionValue)); + } + + // Handle Uri-Path. + if (uriPaths.isEmpty()) { + errorCode = CoapMessageCode.BAD_REQUEST; // Return 4.00 Response + errorContent = "Format-Error: The Format is not correct!"; + sendErrorResponse(); + in.skipBytes(in.readableBytes()); // Skip unread bytes + return; + } + // If in format /ps/full-topic-path, it will be a subscription or publish request. Eg, /ps/topic1/r1 means subscribing/publishing to /topic/r1. + // If in format /mqtt/connection, it will be a connection, heartbeat or disconnect request. + if (uriPaths.get(0).equals(Constants.COAP_PS_PREFIX)) { + switch (coapCode) { + case GET: + if (isObserve) { + coapMessage.setRequestType(CoapRequestType.SUBSCRIBE); + } else { + errorCode = CoapMessageCode.BAD_REQUEST; + errorContent = "Format-Error: The Format is not correct!"; + sendErrorResponse(); + in.skipBytes(in.readableBytes()); // Skip unread bytes + return; + } + break; + case POST: + coapMessage.setRequestType(CoapRequestType.PUBLISH); + break; + default: + errorCode = CoapMessageCode.BAD_REQUEST; + errorContent = "Format-Error: The Format is not correct!"; + sendErrorResponse(); + in.skipBytes(in.readableBytes()); // Skip unread bytes + return; + } + + // Construct full topic path. Eg, /ps/topic1/r1 be converted to /topic/r1. + coapMessage.setTopic(uriPaths.stream().skip(1).collect(Collectors.joining(Constants.MQTT_TOPIC_DELIMITER))); + } else if (uriPaths.size() == 2 && uriPaths.get(0).equals(Constants.COAP_CONNECTION_PREFIX_1) && uriPaths.get(1).equals(Constants.COAP_CONNECTION_PREFIX_2)) { + switch (coapCode) { + case POST: + coapMessage.setRequestType(CoapRequestType.CONNECT); + break; + case DELETE: + coapMessage.setRequestType(CoapRequestType.DISCONNECT); + break; + case PUT: + coapMessage.setRequestType(CoapRequestType.HEARTBEAT); + break; + default: + errorCode = CoapMessageCode.BAD_REQUEST; + errorContent = "Format-Error: The Format is not correct!"; + sendErrorResponse(); + in.skipBytes(in.readableBytes()); // Skip unread bytes + return; + } + } else { + errorCode = CoapMessageCode.BAD_REQUEST; + errorContent = "Format-Error: The Format is not correct!"; + sendErrorResponse(); + in.skipBytes(in.readableBytes()); // Skip unread bytes + return; + } + + // Handle payload + if (in.readableBytes() > 0) { + coapPayload = new byte[in.readableBytes()]; + in.readBytes(coapPayload); + coapMessage.setPayload(coapPayload); + } + + out.add(coapMessage); + } + + public void sendErrorResponse() { + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + coapType == CoapMessageType.CON ? CoapMessageType.ACK : CoapMessageType.NON, + coapToken == null ? 0 : coapTokenLength, + errorCode, + coapMessageId, + coapToken, + errorContent.getBytes(StandardCharsets.UTF_8), + remoteAddress + ); + datagramChannelManager.writeResponse(response); + } + +} \ No newline at end of file diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/CoapEncoder.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/CoapEncoder.java new file mode 100644 index 000000000..f182616b4 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/CoapEncoder.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.protocol.coap; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.DatagramPacket; +import io.netty.handler.codec.MessageToMessageEncoder; +import org.apache.rocketmq.mqtt.common.model.CoapMessage; +import org.apache.rocketmq.mqtt.common.model.CoapMessageOption; + +import java.util.List; + +public class CoapEncoder extends MessageToMessageEncoder { + + @Override + public void encode(ChannelHandlerContext ctx, CoapMessage msg, List out) throws Exception { + // Get buffer to write bytes to send. + ByteBuf buffer = Unpooled.buffer(); + + // Handle Version | Type | TokenLength into first byte. + byte firstByte = (byte)((msg.getVersion() << 6) | (msg.getType().value() << 4) | (msg.getTokenLength() & 0x0F)); + buffer.writeByte(firstByte); + + // Handle Code, MessageID, Token. + buffer.writeByte(msg.getCode().value()); + buffer.writeShort(msg.getMessageId()); + buffer.writeBytes(msg.getToken()); + + // Handle Options. + if (!msg.getOptions().isEmpty()) { + int prevOptionNumber = 0; + for (CoapMessageOption option : msg.getOptions()) { + int optionDelta = option.getOptionNumber().value() - prevOptionNumber; + prevOptionNumber = option.getOptionNumber().value(); + int optionLength = option.getOptionValue() == null ? 0 : option.getOptionValue().length; + + if (optionDelta < 13) { + buffer.writeByte((byte)((optionDelta << 4) | (optionLength & 0x0F))); + } else if (optionDelta < 269) { + buffer.writeByte((byte)((13 << 4) | (optionLength & 0x0F))); + buffer.writeByte(optionDelta - 13); + } else { + buffer.writeByte((byte)((14 << 4) | (optionLength & 0x0F))); + buffer.writeShort(optionDelta - 269); + } + + if (optionLength > 12 && optionLength < 269) { + buffer.writeByte(optionLength - 13); + } else if (optionLength >= 269) { + buffer.writeShort(optionLength - 269); + } + + if (optionLength > 0) { + buffer.writeBytes(option.getOptionValue()); + } + } + } + + // Handle Payload if not empty. + if (msg.getPayload() != null && msg.getPayload().length > 0) { + buffer.writeByte((byte)0xFF); + buffer.writeBytes(msg.getPayload()); + } + + // Send Response. + DatagramPacket responsePacket = new DatagramPacket(buffer, msg.getRemoteAddress()); + out.add(responsePacket); + + } + +} \ No newline at end of file diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/CoapPacketDispatcher.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/CoapPacketDispatcher.java new file mode 100644 index 000000000..abff88a0c --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/CoapPacketDispatcher.java @@ -0,0 +1,197 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.protocol.coap; + +import io.netty.channel.ChannelException; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import org.apache.rocketmq.mqtt.common.model.CoapMessage; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.cs.protocol.coap.handler.CoapPublishHandler; +import org.apache.rocketmq.mqtt.cs.protocol.coap.handler.CoapSubscribeHandler; +import org.apache.rocketmq.mqtt.cs.protocol.coap.handler.CoapConnectHandler; +import org.apache.rocketmq.mqtt.cs.protocol.coap.handler.CoapHeartbeatHandler; +import org.apache.rocketmq.mqtt.cs.protocol.coap.handler.CoapDisconnectHandler; +import org.apache.rocketmq.mqtt.cs.protocol.coap.handler.CoapAckHandler; +import org.apache.rocketmq.mqtt.cs.session.infly.CoapResponseCache; +import org.apache.rocketmq.mqtt.ds.upstream.coap.processor.CoapPublishProcessor; +import org.apache.rocketmq.mqtt.ds.upstream.coap.processor.CoapSubscribeProcessor; +import org.apache.rocketmq.mqtt.ds.upstream.coap.processor.CoapConnectProcessor; +import org.apache.rocketmq.mqtt.ds.upstream.coap.processor.CoapHeartbeatProcessor; +import org.apache.rocketmq.mqtt.ds.upstream.coap.processor.CoapDisconnectProcessor; +import org.apache.rocketmq.mqtt.ds.upstream.coap.processor.CoapAckProcessor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.concurrent.CompletableFuture; + +@Component +public class CoapPacketDispatcher extends SimpleChannelInboundHandler { + private static Logger logger = LoggerFactory.getLogger(CoapPacketDispatcher.class); + + @Resource + private CoapPublishHandler coapPublishHandler; + + @Resource + private CoapSubscribeHandler coapSubscribeHandler; + + @Resource + private CoapConnectHandler coapConnectHandler; + + @Resource + private CoapHeartbeatHandler coapHeartbeatHandler; + + @Resource + private CoapDisconnectHandler coapDisconnectHandler; + + @Resource + private CoapAckHandler coapAckHandler; + + @Resource + private CoapPublishProcessor coapPublishProcessor; + + @Resource + private CoapSubscribeProcessor coapSubscribeProcessor; + + @Resource + private CoapConnectProcessor coapConnectProcessor; + + @Resource + private CoapHeartbeatProcessor coapHeartbeatProcessor; + + @Resource + private CoapDisconnectProcessor coapDisconnectProcessor; + + @Resource + private CoapAckProcessor coapAckProcessor; + + @Resource + private CoapResponseCache coapResponseCache; + + @Override + protected void channelRead0(ChannelHandlerContext ctx, CoapRequestMessage msg) { + // If it is a retransmitted message, send the old response and do nothing. + CoapMessage oldResponse = coapResponseCache.get(msg.getMessageId()); + if (oldResponse != null) { + ctx.writeAndFlush(oldResponse); + return; + } + + // preHandler=>process=>postHandler + boolean preResult = preHandler(ctx, msg); + if (!preResult) { + return; + } + CompletableFuture processResult; + try { + processResult = processCoapMessage(msg); + if (processResult == null) { + _channelRead0(ctx, msg, null); + return; + } + } catch (Throwable t) { + logger.error("", t); + throw new ChannelException(t.getMessage()); + } + processResult.whenComplete((hookResult, throwable) -> { + if (throwable != null) { + logger.error("", throwable); + ctx.fireExceptionCaught(new ChannelException(throwable.getMessage())); + return; + } + if (hookResult == null) { + ctx.fireExceptionCaught(new ChannelException("Coap UpstreamHook Result Unknown")); + return; + } + try { + _channelRead0(ctx, msg, hookResult); + } catch (Throwable t) { + logger.error("", t); + ctx.fireExceptionCaught(new ChannelException(t.getMessage())); + } + }); + } + + private void _channelRead0(ChannelHandlerContext ctx, CoapRequestMessage msg, HookResult processResult) { + switch (msg.getRequestType()) { + case PUBLISH: + coapPublishHandler.doHandler(ctx, msg, processResult); + break; + case SUBSCRIBE: + coapSubscribeHandler.doHandler(ctx, msg, processResult); + break; + case CONNECT: + coapConnectHandler.doHandler(ctx, msg, processResult); + break; + case HEARTBEAT: + coapHeartbeatHandler.doHandler(ctx, msg, processResult); + break; + case DISCONNECT: + coapDisconnectHandler.doHandler(ctx, msg, processResult); + break; + case ACK: + coapAckHandler.doHandler(ctx, msg, processResult); + break; + default: + } + } + + private boolean preHandler(ChannelHandlerContext ctx, CoapRequestMessage msg) { + switch (msg.getRequestType()) { + case PUBLISH: + return coapPublishHandler.preHandler(ctx, msg); + case SUBSCRIBE: + return coapSubscribeHandler.preHandler(ctx, msg); + case CONNECT: + return coapConnectHandler.preHandler(ctx, msg); + case HEARTBEAT: + return coapHeartbeatHandler.preHandler(ctx, msg); + case DISCONNECT: + return coapDisconnectHandler.preHandler(ctx, msg); + case ACK: + return coapAckHandler.preHandler(ctx, msg); + default: + return false; + } + } + + public CompletableFuture processCoapMessage(CoapRequestMessage msg) { + switch (msg.getRequestType()) { + case PUBLISH: + return coapPublishProcessor.process(msg); + case SUBSCRIBE: + return coapSubscribeProcessor.process(msg); + case CONNECT: + return coapConnectProcessor.process(msg); + case HEARTBEAT: + return coapHeartbeatProcessor.process(msg); + case DISCONNECT: + return coapDisconnectProcessor.process(msg); + case ACK: + return coapAckProcessor.process(msg); + default: + } + CompletableFuture hookResult = new CompletableFuture<>(); + hookResult.complete(new HookResult(HookResult.FAIL, "InvalidCoapMsgCode", null)); + return hookResult; + } + +} \ No newline at end of file diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/CoapRPCHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/CoapRPCHandler.java new file mode 100644 index 000000000..9e46fae9e --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/CoapRPCHandler.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.protocol.coap; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.socket.DatagramPacket; +import io.netty.util.ReferenceCountUtil; +import org.apache.rocketmq.mqtt.common.facade.MetaPersistManager; +import org.apache.rocketmq.mqtt.ds.notify.NotifyManager; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Set; +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; + +@Component +public class CoapRPCHandler extends SimpleChannelInboundHandler { + + @Resource + private MetaPersistManager metaPersistManager; + + @Resource + private NotifyManager notifyManager; + + @Override + protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception { + // Get sender socket address. + InetSocketAddress address = packet.sender(); + // Get all machines. + Set connectorNodes = metaPersistManager.getConnectNodeSet(); + if (connectorNodes == null || connectorNodes.isEmpty()) { + throw new RemotingException("No Connect Nodes"); + } + // Calculate machine index with ip-port to forward the packet. + int hash = address.toString().hashCode(); + int nodeNum = Math.abs(hash % connectorNodes.size()); + List nodeList = new ArrayList<>(connectorNodes); + Collections.sort(nodeList); + String forwardNode = nodeList.get(nodeNum); + // Forward the packet if not for localhost, otherwise fire to next channel. + if (InetAddress.getLocalHost().getHostAddress().equals(forwardNode)) { + ctx.fireChannelRead(packet); + } else { + boolean isForward = notifyManager.doCoapForward(forwardNode, packet); + if (!isForward) { + ReferenceCountUtil.release(packet); + } + } + } +} \ No newline at end of file diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapAckHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapAckHandler.java new file mode 100644 index 000000000..d3d5f5ed1 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapAckHandler.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.protocol.coap.handler; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.cs.protocol.CoapPacketHandler; +import org.apache.rocketmq.mqtt.cs.session.infly.CoapRetryManager; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +@Component +public class CoapAckHandler implements CoapPacketHandler { + + @Resource + private CoapRetryManager coapRetryManager; + + @Override + public boolean preHandler(ChannelHandlerContext ctx, CoapRequestMessage coapMessage) { + return true; + } + + @Override + public void doHandler(ChannelHandlerContext ctx, CoapRequestMessage coapMessage, HookResult upstreamHookResult) { + if (!upstreamHookResult.isSuccess()) { + return; + } + // Ack the relative message in retry manager. + int messageId = coapMessage.getMessageId(); + if (coapRetryManager.contains(messageId)) { + coapRetryManager.ackMessage(messageId); + } + } +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapConnectHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapConnectHandler.java new file mode 100644 index 000000000..6d4eff546 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapConnectHandler.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.protocol.coap.handler; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.Constants; +import org.apache.rocketmq.mqtt.common.model.CoapMessage; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.common.model.CoapMessageType; +import org.apache.rocketmq.mqtt.common.model.CoapMessageCode; +import org.apache.rocketmq.mqtt.common.util.CoapTokenUtil; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; +import org.apache.rocketmq.mqtt.cs.protocol.CoapPacketHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.nio.charset.StandardCharsets; + +@Component +public class CoapConnectHandler implements CoapPacketHandler { + private static Logger logger = LoggerFactory.getLogger(CoapConnectHandler.class); + + @Resource + private DatagramChannelManager datagramChannelManager; + + @Override + public boolean preHandler(ChannelHandlerContext ctx, CoapRequestMessage coapMessage) { + if (coapMessage.getClientId() == null || coapMessage.getUserName() == null || coapMessage.getPassword() == null) { + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + coapMessage.getType() == CoapMessageType.CON ? CoapMessageType.ACK : CoapMessageType.NON, + coapMessage.getTokenLength(), + CoapMessageCode.BAD_REQUEST, + coapMessage.getMessageId(), + coapMessage.getToken(), + "Not complete info for connection mode.".getBytes(StandardCharsets.UTF_8), + coapMessage.getRemoteAddress() + ); + datagramChannelManager.writeResponse(response); + return false; + } + return true; + } + + @Override + public void doHandler(ChannelHandlerContext ctx, CoapRequestMessage coapMessage, HookResult upstreamHookResult) { + // Response fail ack if upstream hook fail. + if (!upstreamHookResult.isSuccess()) { + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + CoapMessageType.ACK, + coapMessage.getTokenLength(), + CoapMessageCode.UNAUTHORIZED, + coapMessage.getMessageId(), + coapMessage.getToken(), + upstreamHookResult.getRemark().getBytes(StandardCharsets.UTF_8), + coapMessage.getRemoteAddress() + ); + datagramChannelManager.writeResponse(response); + return; + } + try { + // Create new token. + String authToken = CoapTokenUtil.generateToken(coapMessage.getClientId()); + // Response success ack and return authToken. + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + CoapMessageType.ACK, + coapMessage.getTokenLength(), + CoapMessageCode.CREATED, + coapMessage.getMessageId(), + coapMessage.getToken(), + authToken.getBytes(StandardCharsets.UTF_8), + coapMessage.getRemoteAddress() + ); + datagramChannelManager.writeResponse(response); + } catch (Exception e) { + logger.error("", e); + } + + } +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapDisconnectHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapDisconnectHandler.java new file mode 100644 index 000000000..cdb6c711f --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapDisconnectHandler.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.protocol.coap.handler; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.Constants; +import org.apache.rocketmq.mqtt.common.model.CoapMessage; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.common.model.CoapMessageType; +import org.apache.rocketmq.mqtt.common.model.CoapMessageCode; +import org.apache.rocketmq.mqtt.common.util.CoapTokenUtil; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; +import org.apache.rocketmq.mqtt.cs.protocol.CoapPacketHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.nio.charset.StandardCharsets; + +@Component +public class CoapDisconnectHandler implements CoapPacketHandler { + private static Logger logger = LoggerFactory.getLogger(CoapDisconnectHandler.class); + + @Resource + private DatagramChannelManager datagramChannelManager; + + @Override + public boolean preHandler(ChannelHandlerContext ctx, CoapRequestMessage coapMessage) { + if (coapMessage.getClientId() == null || coapMessage.getAuthToken() == null) { + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + coapMessage.getType() == CoapMessageType.CON ? CoapMessageType.ACK : CoapMessageType.NON, + coapMessage.getTokenLength(), + CoapMessageCode.BAD_REQUEST, + coapMessage.getMessageId(), + coapMessage.getToken(), + "Not complete info for connection mode.".getBytes(StandardCharsets.UTF_8), + coapMessage.getRemoteAddress() + ); + datagramChannelManager.writeResponse(response); + return false; + } + return true; + } + + @Override + public void doHandler(ChannelHandlerContext ctx, CoapRequestMessage coapMessage, HookResult upstreamHookResult) { + // Response fail ack if upstream hook fail. + if (!upstreamHookResult.isSuccess()) { + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + CoapMessageType.ACK, + coapMessage.getTokenLength(), + CoapMessageCode.INTERNAL_SERVER_ERROR, + coapMessage.getMessageId(), + coapMessage.getToken(), + upstreamHookResult.getRemark().getBytes(StandardCharsets.UTF_8), + coapMessage.getRemoteAddress() + ); + datagramChannelManager.writeResponse(response); + return; + } + // Response unauthorized ack if authToken is not valid. + if (!CoapTokenUtil.isValid(coapMessage.getClientId(), coapMessage.getAuthToken())) { + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + CoapMessageType.ACK, + coapMessage.getTokenLength(), + CoapMessageCode.UNAUTHORIZED, + coapMessage.getMessageId(), + coapMessage.getToken(), + "AuthToken is not valid.".getBytes(StandardCharsets.UTF_8), + coapMessage.getRemoteAddress() + ); + datagramChannelManager.writeResponse(response); + return; + } + // Remove clientId-token from token manager. +// coapTokenManager.removeToken(coapMessage.getClientId()); + // Response ack success + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + CoapMessageType.ACK, + coapMessage.getTokenLength(), + CoapMessageCode.DELETED, + coapMessage.getMessageId(), + coapMessage.getToken(), + null, + coapMessage.getRemoteAddress() + ); + datagramChannelManager.writeResponse(response); + } +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapHeartbeatHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapHeartbeatHandler.java new file mode 100644 index 000000000..b89321cf2 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapHeartbeatHandler.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.protocol.coap.handler; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.Constants; +import org.apache.rocketmq.mqtt.common.model.CoapMessage; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.common.model.CoapMessageType; +import org.apache.rocketmq.mqtt.common.model.CoapMessageCode; +import org.apache.rocketmq.mqtt.common.util.CoapTokenUtil; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; +import org.apache.rocketmq.mqtt.cs.protocol.CoapPacketHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.nio.charset.StandardCharsets; + +@Component +public class CoapHeartbeatHandler implements CoapPacketHandler { + private static Logger logger = LoggerFactory.getLogger(CoapHeartbeatHandler.class); + + @Resource + private DatagramChannelManager datagramChannelManager; + + @Override + public boolean preHandler(ChannelHandlerContext ctx, CoapRequestMessage coapMessage) { + if (coapMessage.getClientId() == null || coapMessage.getAuthToken() == null) { + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + coapMessage.getType() == CoapMessageType.CON ? CoapMessageType.ACK : CoapMessageType.NON, + coapMessage.getTokenLength(), + CoapMessageCode.BAD_REQUEST, + coapMessage.getMessageId(), + coapMessage.getToken(), + "Not complete info for connection mode.".getBytes(StandardCharsets.UTF_8), + coapMessage.getRemoteAddress() + ); + datagramChannelManager.writeResponse(response); + return false; + } + return true; + } + + @Override + public void doHandler(ChannelHandlerContext ctx, CoapRequestMessage coapMessage, HookResult upstreamHookResult) { + // Response fail ack if upstream hook fail. + if (!upstreamHookResult.isSuccess()) { + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + CoapMessageType.ACK, + coapMessage.getTokenLength(), + CoapMessageCode.INTERNAL_SERVER_ERROR, + coapMessage.getMessageId(), + coapMessage.getToken(), + upstreamHookResult.getRemark().getBytes(StandardCharsets.UTF_8), + coapMessage.getRemoteAddress() + ); + datagramChannelManager.writeResponse(response); + return; + } + // Response unauthorized ack if authToken is not valid. + if (!CoapTokenUtil.isValid(coapMessage.getClientId(), coapMessage.getAuthToken())) { + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + CoapMessageType.ACK, + coapMessage.getTokenLength(), + CoapMessageCode.UNAUTHORIZED, + coapMessage.getMessageId(), + coapMessage.getToken(), + "AuthToken is not valid.".getBytes(StandardCharsets.UTF_8), + coapMessage.getRemoteAddress() + ); + datagramChannelManager.writeResponse(response); + return; + } + try { + // Refresh update time of token, and return new token + String newAuthToken = CoapTokenUtil.generateToken(coapMessage.getClientId()); + // Response ack success and new token + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + CoapMessageType.ACK, + coapMessage.getTokenLength(), + CoapMessageCode.CHANGED, + coapMessage.getMessageId(), + coapMessage.getToken(), + newAuthToken.getBytes(StandardCharsets.UTF_8), + coapMessage.getRemoteAddress() + ); + datagramChannelManager.writeResponse(response); + } catch (Exception e) { + logger.error("", e); + } + } +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapPublishHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapPublishHandler.java new file mode 100644 index 000000000..9930ae287 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapPublishHandler.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.protocol.coap.handler; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.Constants; +import org.apache.rocketmq.mqtt.common.model.CoapMessage; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.common.model.CoapMessageType; +import org.apache.rocketmq.mqtt.common.model.CoapMessageCode; +import org.apache.rocketmq.mqtt.common.util.CoapTokenUtil; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.protocol.CoapPacketHandler; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.nio.charset.StandardCharsets; + +@Component +public class CoapPublishHandler implements CoapPacketHandler { + + @Resource + private DatagramChannelManager datagramChannelManager; + + @Resource + private ConnectConf connectConf; + + @Override + public boolean preHandler(ChannelHandlerContext ctx, CoapRequestMessage coapMessage) { + if (connectConf.isEnableCoapConnect()) { + if (coapMessage.getClientId() == null || coapMessage.getAuthToken() == null) { + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + coapMessage.getType() == CoapMessageType.CON ? CoapMessageType.ACK : CoapMessageType.NON, + coapMessage.getTokenLength(), + CoapMessageCode.BAD_REQUEST, + coapMessage.getMessageId(), + coapMessage.getToken(), + "Not complete info for connection mode.".getBytes(StandardCharsets.UTF_8), + coapMessage.getRemoteAddress() + ); + datagramChannelManager.writeResponse(response); + return false; + } + if (!CoapTokenUtil.isValid(coapMessage.getClientId(), coapMessage.getAuthToken())) { + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + coapMessage.getType() == CoapMessageType.CON ? CoapMessageType.ACK : CoapMessageType.NON, + coapMessage.getTokenLength(), + CoapMessageCode.UNAUTHORIZED, + coapMessage.getMessageId(), + coapMessage.getToken(), + "Invalid authToken.".getBytes(StandardCharsets.UTF_8), + coapMessage.getRemoteAddress() + ); + datagramChannelManager.writeResponse(response); + return false; + } + } + return true; + } + + @Override + public void doHandler(ChannelHandlerContext ctx, CoapRequestMessage coapMessage, HookResult upstreamHookResult) { + // Send response to client with success/error content. + CoapMessage response; + if (upstreamHookResult.isSuccess()) { + response = new CoapMessage( + Constants.COAP_VERSION, + coapMessage.getType() == CoapMessageType.CON ? CoapMessageType.ACK : CoapMessageType.NON, + coapMessage.getTokenLength(), + CoapMessageCode.CREATED, + coapMessage.getMessageId(), + coapMessage.getToken(), + null, + coapMessage.getRemoteAddress() + ); + } else { + response = new CoapMessage( + Constants.COAP_VERSION, + coapMessage.getType() == CoapMessageType.CON ? CoapMessageType.ACK : CoapMessageType.NON, + coapMessage.getTokenLength(), + CoapMessageCode.INTERNAL_SERVER_ERROR, + coapMessage.getMessageId(), + coapMessage.getToken(), + upstreamHookResult.getRemark().getBytes(), + coapMessage.getRemoteAddress() + ); + } + datagramChannelManager.writeResponse(response); + } +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapSubscribeHandler.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapSubscribeHandler.java new file mode 100644 index 000000000..c030928c9 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/coap/handler/CoapSubscribeHandler.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.protocol.coap.handler; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.mqtt.common.facade.RetainedPersistManager; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.Constants; +import org.apache.rocketmq.mqtt.common.model.Message; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.common.model.CoapMessage; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.common.model.CoapMessageType; +import org.apache.rocketmq.mqtt.common.model.CoapMessageCode; +import org.apache.rocketmq.mqtt.common.util.CoapTokenUtil; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.protocol.CoapPacketHandler; +import org.apache.rocketmq.mqtt.cs.session.CoapSession; +import org.apache.rocketmq.mqtt.cs.session.loop.CoapSessionLoop; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +@Component +public class CoapSubscribeHandler implements CoapPacketHandler { + private static Logger logger = LoggerFactory.getLogger(CoapSubscribeHandler.class); + + @Resource + private CoapSessionLoop sessionLoop; + + @Resource + private RetainedPersistManager retainedPersistManager; + + @Resource + private DatagramChannelManager datagramChannelManager; + + @Resource + private ConnectConf connectConf; + + private ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1, new ThreadFactoryImpl("check_coap_subscribe_future")); + + @Override + public boolean preHandler(ChannelHandlerContext ctx, CoapRequestMessage coapMessage) { + if (connectConf.isEnableCoapConnect()) { + if (coapMessage.getClientId() == null || coapMessage.getAuthToken() == null) { + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + coapMessage.getType() == CoapMessageType.CON ? CoapMessageType.ACK : CoapMessageType.NON, + coapMessage.getTokenLength(), + CoapMessageCode.BAD_REQUEST, + coapMessage.getMessageId(), + coapMessage.getToken(), + "Not complete info for connection mode.".getBytes(StandardCharsets.UTF_8), + coapMessage.getRemoteAddress() + ); + datagramChannelManager.writeResponse(response); + return false; + } + if (!CoapTokenUtil.isValid(coapMessage.getClientId(), coapMessage.getAuthToken())) { + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + coapMessage.getType() == CoapMessageType.CON ? CoapMessageType.ACK : CoapMessageType.NON, + coapMessage.getTokenLength(), + CoapMessageCode.UNAUTHORIZED, + coapMessage.getMessageId(), + coapMessage.getToken(), + "Invalid authToken.".getBytes(StandardCharsets.UTF_8), + coapMessage.getRemoteAddress() + ); + datagramChannelManager.writeResponse(response); + return false; + } + } + return true; + } + + @Override + public void doHandler(ChannelHandlerContext ctx, CoapRequestMessage coapMessage, HookResult upstreamHookResult) { + // Send error response if upstream fail. + if (!upstreamHookResult.isSuccess()) { + doResponseFail(coapMessage, upstreamHookResult.getRemark()); + return; + } + + // Construct subscription. + Subscription subscription = new Subscription(); + subscription.setQos(coapMessage.getQosLevel().value()); + subscription.setTopicFilter(TopicUtils.normalizeTopic(coapMessage.getTopic())); + + // Get session from sessionLoop if it is already existed, otherwise create a new one. + InetSocketAddress address = coapMessage.getRemoteAddress(); + CoapSession session = sessionLoop.getSession(address); + // If session already exist, refresh subscribe time and send response. + if (session != null) { + session.refreshSubscribeTime(); + doResponseSuccess(coapMessage, session); + return; + } + // If it is a new session, create and add to sessionLoop. And send response and retained message later. + CoapSession newSession = new CoapSession(); + newSession.setAddress(address); + newSession.setToken(coapMessage.getToken()); + newSession.setSubscribeTime(System.currentTimeMillis()); + newSession.setSubscription(subscription); + CompletableFuture future = new CompletableFuture<>(); + // todo: setFuture + scheduler.schedule(() -> { + if (!future.isDone()) { + future.complete(null); + } + }, 1, TimeUnit.SECONDS); + try { + sessionLoop.addSession(newSession, future); + future.thenAccept(aVoid -> { + if (!ctx.channel().isActive()) { + return; + } + // todo: removeFuture + doResponseSuccess(coapMessage, newSession); + sendRetainMessage(newSession); + }); + } catch (Exception e) { + logger.error("Coap Subscribe:{}", coapMessage.getRemoteAddress(), e); + } + + } + + private void sendRetainMessage(CoapSession session) { + // Get retainedMessage from persist manager and send to client. + CompletableFuture retainedMessage = retainedPersistManager.getRetainedMessage(session.getSubscription().getTopicFilter()); + retainedMessage.whenComplete((message, throwable) -> { + if (message == null) { + return; + } + session.messageNumIncrement(); + CoapMessage sendMessage = new CoapMessage( + Constants.COAP_VERSION, + session.getSubscription().getQos() == 0 ? CoapMessageType.NON : CoapMessageType.CON, + session.getToken().length, + CoapMessageCode.CONTENT, + session.getMessageId() + session.getMessageNum(), + session.getToken(), + message.getPayload(), + session.getAddress() + ); + datagramChannelManager.pushMessage(session, sendMessage); + }); + } + + public void doResponseFail(CoapRequestMessage coapMessage, String errContent) { + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + CoapMessageType.ACK, + coapMessage.getTokenLength(), + CoapMessageCode.INTERNAL_SERVER_ERROR, + coapMessage.getMessageId() + 1, + coapMessage.getToken(), + errContent.getBytes(StandardCharsets.UTF_8), + coapMessage.getRemoteAddress() + ); + response.addObserveOption(1); + datagramChannelManager.writeResponse(response); + } + + public void doResponseSuccess(CoapRequestMessage coapMessage, CoapSession session) { + CoapMessage response = new CoapMessage( + Constants.COAP_VERSION, + coapMessage.getType() == CoapMessageType.CON ? CoapMessageType.ACK : CoapMessageType.NON, + coapMessage.getTokenLength(), + CoapMessageCode.CONTENT, + coapMessage.getMessageId(), + coapMessage.getToken(), + null, + coapMessage.getRemoteAddress() + ); + response.addObserveOption(session.getMessageNum()); + datagramChannelManager.writeResponse(response); + } + +} \ No newline at end of file diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/rpc/RpcPacketDispatcher.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/rpc/RpcPacketDispatcher.java index ccea43bcc..3a0998288 100644 --- a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/rpc/RpcPacketDispatcher.java +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/protocol/rpc/RpcPacketDispatcher.java @@ -19,11 +19,17 @@ import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.JSON; import io.netty.channel.ChannelHandlerContext; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.DatagramPacket; import org.apache.rocketmq.mqtt.common.model.MessageEvent; import org.apache.rocketmq.mqtt.common.model.RpcCode; import org.apache.rocketmq.mqtt.common.model.RpcHeader; import org.apache.rocketmq.mqtt.cs.channel.ChannelManager; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; import org.apache.rocketmq.mqtt.cs.session.notify.MessageNotifyAction; import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; import org.apache.rocketmq.remoting.protocol.RemotingCommand; @@ -34,6 +40,7 @@ import javax.annotation.Resource; import java.nio.charset.StandardCharsets; import java.util.List; +import java.net.InetSocketAddress; @Component @@ -46,6 +53,9 @@ public class RpcPacketDispatcher implements NettyRequestProcessor { @Resource private ChannelManager channelManager; + @Resource + private DatagramChannelManager datagramChannelManager; + @Override public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws Exception { RemotingCommand response = RemotingCommand.createResponseCommand(RpcCode.SUCCESS, null); @@ -56,6 +66,8 @@ public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand doNotify(request); } else if (RpcCode.CMD_CLOSE_CHANNEL == code) { closeChannel(request); + } else if (RpcCode.COM_NOTIFY_COAP_MESSAGE == code) { + doNotifyCoap(request); } } catch (Throwable t) { logger.error("", t); @@ -81,4 +93,23 @@ private void closeChannel(RemotingCommand request) { channelManager.closeConnect(channelId, request.getRemark()); } + private void doNotifyCoap(RemotingCommand request) { + String payload = new String(request.getBody(), StandardCharsets.UTF_8); + JSONObject jsonObject = JSON.parseObject(payload); + + byte[] data = jsonObject.getBytes("data"); + String senderAddress = jsonObject.getString("senderAddress"); + int senderPort = jsonObject.getIntValue("senderPort"); + String recipientAddress = jsonObject.getString("recipientAddress"); + int recipientPort = jsonObject.getIntValue("recipientPort"); + ByteBuf buffer = Unpooled.wrappedBuffer(data); + + InetSocketAddress sender = new InetSocketAddress(senderAddress, senderPort); + InetSocketAddress recipient = new InetSocketAddress(recipientAddress, recipientPort); + DatagramPacket packet = new DatagramPacket(buffer.retain(), recipient, sender); + + DatagramChannel channel = datagramChannelManager.getChannel(); + channel.pipeline().context("coap-handler").fireChannelRead(packet); // forward to coap-decoder + } + } diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/CoapSession.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/CoapSession.java new file mode 100644 index 000000000..8bd7acad7 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/CoapSession.java @@ -0,0 +1,320 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.mqtt.cs.session; + +import org.apache.rocketmq.mqtt.common.model.Message; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.QueueOffset; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetSocketAddress; +import java.util.Map; +import java.util.List; +import java.util.ArrayList; +import java.util.Set; +import java.util.LinkedHashSet; +import java.util.Iterator; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; + +public class CoapSession { + private static Logger logger = LoggerFactory.getLogger(CoapSession.class); + + private static final int MAX_MESSAGE_ID = 65535; + + private InetSocketAddress address; + private final int messageId = new Random().nextInt(MAX_MESSAGE_ID) + 1; + private byte[] token; + private int messageNum = 0; + private long subscribeTime; + private volatile int pullSize; + + private Subscription subscription; + private Map offsetMap = new ConcurrentHashMap<>(16); + Map> sendingMessages = new ConcurrentHashMap<>(16); + + public CoapSession() {} + + public void refreshSubscribeTime() { + this.subscribeTime = System.currentTimeMillis(); + } + + public QueueOffset getQueueOffset(Queue queue) { + if (queue == null) { + throw new RuntimeException("queue is null"); + } + return offsetMap.get(queue); + } + + public void freshQueue(Set queues) { + if (subscription == null) { + throw new RuntimeException("subscription is null"); + } + if (queues == null) { + logger.warn("queues is null when freshQueue,{},{}", address, subscription); + return; + } + + // Remove expired queues. + for (Queue memQueue: offsetMap.keySet()) { + if (!queues.contains(memQueue)) { + offsetMap.remove(memQueue); + } + } + + // Init queueOffset for new queue. + for (Queue nowQueue : queues) { + if (!offsetMap.containsKey(nowQueue)) { + QueueOffset queueOffset = new QueueOffset(); + offsetMap.put(nowQueue, queueOffset); + // todo: this.markPersistOffsetFlag(true); + } + } + + // Remove expired sendingMessage. + for (Queue memQueue : sendingMessages.keySet()) { + if (!queues.contains(memQueue)) { + sendingMessages.remove(memQueue); + } + } + + if (queues.isEmpty()) { + logger.warn("queues is empty when freshQueue,{},{}", address, subscription); + } + } + + public void addQueueOffset(Queue queue, QueueOffset offset) { + offsetMap.put(queue, offset); + } + + public void updateQueueOffset(Queue queue, Message message) { + if (!offsetMap.containsKey(queue)) { + logger.warn("failed update queue offset,not found queueOffset,{},{},{}", address, subscription, + queue); + return; + } + QueueOffset queueOffset = offsetMap.get(queue); + queueOffset.setOffset(message.getOffset() + 1); + } + + public boolean addSendingMessages(Queue queue, List messages) { + if (queue == null) { + throw new RuntimeException("queue is null"); + } + if (messages == null || messages.isEmpty()) { + return false; + } + if (subscription.isShare()) { + return true; + } + if (!sendingMessages.containsKey(queue)) { + sendingMessages.putIfAbsent(queue, new LinkedHashSet<>(8)); + } + if (!offsetMap.containsKey(queue)) { + logger.warn("not found queueOffset,{},{},{}", address, subscription, queue); + return false; + } + boolean add = false; + QueueOffset queueOffset = offsetMap.get(queue); + for (Message message : messages) { + if (message.getOffset() < queueOffset.getOffset() && queueOffset.getOffset() != Long.MAX_VALUE) { + continue; + } + synchronized (this) { + if (sendingMessages.get(queue).add(message.copy())) { + add = true; + } + } + } + return add; + } + + public boolean sendingMessageIsEmpty(Queue queue) { + if (queue == null) { + throw new RuntimeException("queue is null"); + } + LinkedHashSet messages = sendingMessages.get(queue); + if (messages == null) { + return true; + } + synchronized (this) { + return messages.isEmpty(); + } + } + + // Get all messages of the queue which are pending to be sent. + public List pendMessageList(Queue queue) { + if (queue == null) { + throw new RuntimeException("queue is null"); + } + List list = new ArrayList<>(); + LinkedHashSet messages = sendingMessages.get(queue); + if (messages == null) { + return null; + } + synchronized (this) { + if (!messages.isEmpty()) { + for (Message message : messages) { + if (message.getAck() == -1) { + list.add(message); + } + } + } + } + return list; + } + + // Set message ack, remove the message from sendingMessages, and update queueOffset of the relative queue. + public void ack(Queue queue, long offset) { + if (queue == null) { + throw new RuntimeException("queue is null"); + } + LinkedHashSet messages = sendingMessages.get(queue); + if (messages == null) { + return; + } + synchronized (this) { + if (messages.isEmpty()) { + return; + } + boolean flag = true; + Iterator iterator = messages.iterator(); + while (iterator.hasNext()) { + Message message = iterator.next(); + if (message.getOffset() == offset) { + message.setAck(1); + } + if (message.getAck() != 1) { + flag = false; + } + if (flag) { + updateQueueOffset(queue, message); +// this.markPersistOffsetFlag(true); + iterator.remove(); + } + } + } + } + + // Set message ack, remove the message from sendingMessages, and update queueOffset of the relative queue. + public void ack(Queue queue, Message messageSend) { + LinkedHashSet messages = sendingMessages.get(queue); + if (messages == null) { + return; + } + synchronized (this) { + if (messages.isEmpty()) { + return; + } + Iterator iterator = messages.iterator(); + while (iterator.hasNext()) { + Message message = iterator.next(); + if (message.equals(messageSend)) { + message.setAck(1); + } + if (message.getAck() == 1) { + updateQueueOffset(queue, message); + iterator.remove(); + } + } + } + } + + public Message nextSendMessageByOrder(Queue queue) { + if (queue == null) { + throw new RuntimeException("queue is null"); + } + LinkedHashSet messages = sendingMessages.get(queue); + if (messages == null) { + return null; + } + synchronized (this) { + return messages.isEmpty() ? null : messages.iterator().next(); + } + } + + public int getNextMessageID() { + return messageId + messageNum; + } + + public void messageNumIncrement() { + messageNum++; + } + + public InetSocketAddress getAddress() { + return address; + } + + public void setAddress(InetSocketAddress address) { + this.address = address; + } + + public int getMessageId() { + return messageId; + } + + public byte[] getToken() { + return token; + } + + public void setToken(byte[] token) { + this.token = token; + } + + public int getMessageNum() { + return messageNum; + } + + public void setMessageNum(int messageNum) { + this.messageNum = messageNum; + } + + public long getSubscribeTime() { + return subscribeTime; + } + + public void setSubscribeTime(long subscribeTime) { + this.subscribeTime = subscribeTime; + } + + public Subscription getSubscription() { + return subscription; + } + + public void setSubscription(Subscription subscription) { + this.subscription = subscription; + } + + public int getPullSize() { + return pullSize; + } + + public void setPullSize(int pullSize) { + this.pullSize = pullSize; + } + + public Map getOffsetMap() { + return offsetMap; + } + + public void setOffsetMap(Map offsetMap) { + this.offsetMap = offsetMap; + } +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/QueueFresh.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/QueueFresh.java index 2b360d735..2cbb5b2bd 100644 --- a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/QueueFresh.java +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/QueueFresh.java @@ -60,4 +60,21 @@ public Set freshQueue(Session session, Subscription subscription) { return queues; } + public Set freshQueue(CoapSession session) { + Set queues = new HashSet<>(); + Subscription subscription = session.getSubscription(); + Set brokers = lmqQueueStore.getReadableBrokers(subscription.toFirstTopic()); + if (brokers == null || brokers.isEmpty()) { + return queues; + } + for (String broker : brokers) { + Queue moreQueue = new Queue(); + moreQueue.setQueueName(subscription.toQueueName()); + moreQueue.setBrokerName(broker); + queues.add(moreQueue); + } + session.freshQueue(queues); + return queues; + } + } diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/CoapResponseCache.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/CoapResponseCache.java new file mode 100644 index 000000000..689e18146 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/CoapResponseCache.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.session.infly; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import org.apache.rocketmq.mqtt.common.model.CoapMessage; +import org.springframework.stereotype.Component; + +@Component +public class CoapResponseCache { + private static final int MAX_SIZE = 10000; + + private Cache responseCache = Caffeine.newBuilder().maximumSize(MAX_SIZE).build(); + + public void put(CoapMessage coapMessage) { + responseCache.put(coapMessage.getMessageId(), coapMessage); + } + + public CoapMessage get(int messageId) { + return responseCache.getIfPresent(messageId); + } + +} \ No newline at end of file diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/CoapRetryManager.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/CoapRetryManager.java new file mode 100644 index 000000000..1471e234f --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/CoapRetryManager.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.session.infly; + +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.mqtt.common.model.CoapMessage; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; +import org.apache.rocketmq.mqtt.cs.session.CoapSession; +import org.apache.rocketmq.mqtt.cs.session.loop.CoapSessionLoop; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +@Component +public class CoapRetryManager { + private static Logger logger = LoggerFactory.getLogger(CoapRetryManager.class); + + @Resource + private DatagramChannelManager datagramChannelManager; + + @Resource + private CoapSessionLoop coapSessionLoop; + + private ScheduledThreadPoolExecutor scheduler; + + private ConcurrentMap retryMessageMap = new ConcurrentHashMap<>(1024); + + private static final int SCHEDULE_INTERVAL = 1000; + private static final int MAX_RETRY_TIME = 3; + private static final long RETRY_INTERVAL = 3000; + + @PostConstruct + public void init() { + scheduler = new ScheduledThreadPoolExecutor(1, new ThreadFactoryImpl("coap_retry_message_thread_")); + scheduler.scheduleWithFixedDelay(this::doRetry, SCHEDULE_INTERVAL, SCHEDULE_INTERVAL, TimeUnit.MILLISECONDS); + } + + public void addRetryMessage(CoapSession session, CoapMessage message) { + retryMessageMap.put(message.getMessageId(), new RetryMessage(message.getMessageId(), message, session)); + } + + public RetryMessage removeRetryMessage(int messageId) { + return retryMessageMap.remove(messageId); + } + + public boolean contains(int messageId) { + return retryMessageMap.containsKey(messageId); + } + + public void ackMessage(int messageId) { + RetryMessage removedMessage = retryMessageMap.remove(messageId); + // Refresh subscription each time receiving an ACK. + if (removedMessage.session != null) { + removedMessage.session.refreshSubscribeTime(); + } + } + + private void doRetry() { + if (retryMessageMap.isEmpty()) { + return; + } + for (RetryMessage retryMessage : retryMessageMap.values()) { + if (System.currentTimeMillis() - retryMessage.lastSendTime < RETRY_INTERVAL) { + continue; + } + if (retryMessage.retryTime >= MAX_RETRY_TIME) { + RetryMessage removedMessage = removeRetryMessage(retryMessage.messageId); + CoapSession session = removedMessage.session; + // Remove session if exceed max retry time. + if (session != null) { + // Release session from all relative retry message. + for (RetryMessage message : retryMessageMap.values()) { + if (message.session == session) { + message.session = null; + } + } + // Remove from session loop. + coapSessionLoop.removeSession(session.getAddress()); + } + logger.info("coap retry message expired, messageId:{}", retryMessage.messageId); + continue; + } + // Update messageID if session has newer messageID. + CoapSession session = retryMessage.session; + if (session != null) { + int latestMessageNum = session.getMessageNum(); + int latestMessageID = session.getMessageId() + latestMessageNum; + if (latestMessageID > retryMessage.messageId) { + retryMessage.messageId = latestMessageID; + retryMessage.message.setMessageId(latestMessageID); + retryMessage.message.clearOptions(); + retryMessage.message.addObserveOption(latestMessageNum); + session.messageNumIncrement(); + } + } + // Send retry message and refresh retry info. + datagramChannelManager.write(retryMessage.message); + retryMessage.retryTime++; + retryMessage.lastSendTime = System.currentTimeMillis(); + } + } + + public class RetryMessage { + private int messageId; + private CoapMessage message; + private CoapSession session; + private int retryTime = 0; + private long lastSendTime = System.currentTimeMillis(); + + public RetryMessage(int messageId, CoapMessage message, CoapSession session) { + this.messageId = messageId; + this.message = message; + this.session = session; + } + } + +} \ No newline at end of file diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/PushAction.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/PushAction.java index 235658704..e376fe542 100644 --- a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/PushAction.java +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/infly/PushAction.java @@ -27,15 +27,21 @@ import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.mqtt.common.facade.LmqQueueStore; +import org.apache.rocketmq.mqtt.common.model.CoapMessage; +import org.apache.rocketmq.mqtt.common.model.Constants; +import org.apache.rocketmq.mqtt.common.model.CoapMessageCode; +import org.apache.rocketmq.mqtt.common.model.CoapMessageType; import org.apache.rocketmq.mqtt.common.model.Message; import org.apache.rocketmq.mqtt.common.model.Queue; import org.apache.rocketmq.mqtt.common.model.Subscription; import org.apache.rocketmq.mqtt.common.util.MessageUtil; import org.apache.rocketmq.mqtt.common.util.TopicUtils; import org.apache.rocketmq.mqtt.cs.channel.ChannelInfo; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; import org.apache.rocketmq.mqtt.cs.config.ConnectConf; import org.apache.rocketmq.mqtt.cs.protocol.mqtt.facotry.MqttMessageFactory; import org.apache.rocketmq.mqtt.cs.session.Session; +import org.apache.rocketmq.mqtt.cs.session.CoapSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -72,6 +78,9 @@ public class PushAction { @Resource private LmqQueueStore lmqQueueStore; + @Resource + private DatagramChannelManager datagramChannelManager; + public void messageArrive(Session session, Subscription subscription, Queue queue) { if (session == null) { return; @@ -95,6 +104,19 @@ public void messageArrive(Session session, Subscription subscription, Queue queu } } + public void coapMessageArrive(CoapSession session, Queue queue) { + if (session == null) { + return; + } + List list = session.pendMessageList(queue); + if (list != null && !list.isEmpty()) { + for (Message message : list) { + message.setAck(0); + coapPush(message, session, queue); + } + } + } + public void push(Message message, Subscription subscription, Session session, Queue queue) { String clientId = session.getClientId(); int mqttId = mqttMsgId.nextId(clientId); @@ -132,6 +154,30 @@ public void push(Message message, Subscription subscription, Session session, Qu } } + public void coapPush(Message message, CoapSession session, Queue queue) { + try { + if (message.getStoreTimestamp() > 0 && message.getStoreTimestamp() < session.getSubscribeTime()) { + logger.warn("coap old msg:{},{},{},{}", session.getAddress(), message.getMsgId(), + message.getStoreTimestamp(), session.getSubscribeTime()); + return; + } + } catch (Exception e) { + logger.error("", e); + } + + // Deal with message with empty payload + String msgPayLoad = new String(message.getPayload()); + if (msgPayLoad.equals(MessageUtil.EMPTYSTRING) && message.isEmpty()) { + message.setPayload("".getBytes()); + } + + // Send message to client, and remove from the sendingMessages of session. + session.messageNumIncrement(); + CoapMessage sendMessage = buildCoapMessage(message, session); + datagramChannelManager.pushMessage(session, sendMessage); + session.ack(queue, message); + } + public void _sendMessage(Session session, String clientId, Subscription subscription, Message message) { String payLoad = new String(message.getPayload()); @@ -332,4 +378,19 @@ public void _rollNext(Session session, InFlyCache.PendingDown pendingDown) { } } + private CoapMessage buildCoapMessage(Message message, CoapSession session) { + CoapMessage coapMessage = new CoapMessage( + Constants.COAP_VERSION, + session.getSubscription().getQos() == 0 ? CoapMessageType.NON : CoapMessageType.CON, + session.getToken().length, + CoapMessageCode.CONTENT, + session.getMessageId() + session.getMessageNum(), + session.getToken(), + message.getPayload(), + session.getAddress() + ); + coapMessage.addObserveOption(session.getMessageNum()); + return coapMessage; + } + } diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/CoapSessionLoop.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/CoapSessionLoop.java new file mode 100644 index 000000000..cb61945cc --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/CoapSessionLoop.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.session.loop; + +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.cs.session.CoapSession; + +import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; + +public interface CoapSessionLoop { + + /** + * Add one coap session. + * + * @param session + * @param future + */ + void addSession(CoapSession session, CompletableFuture future); + + /** + * Get one coap session by ip-port. + * + * @param address + * @return + */ + CoapSession getSession(InetSocketAddress address); + + /** + * Remove one coap session. + * + * @param address + * @return + */ + CoapSession removeSession(InetSocketAddress address); + + /** + * notify to pull message from queue + * + * @param session + * @param queue + */ + void notifyPullMessage(CoapSession session, Queue queue); +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/CoapSessionLoopImpl.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/CoapSessionLoopImpl.java new file mode 100644 index 000000000..3b1a063d0 --- /dev/null +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/loop/CoapSessionLoopImpl.java @@ -0,0 +1,361 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.session.loop; + +import com.alibaba.fastjson.JSONObject; +import io.netty.util.HashedWheelTimer; +import io.netty.util.Timeout; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.mqtt.common.facade.LmqQueueStore; +import org.apache.rocketmq.mqtt.common.model.PullResult; +import org.apache.rocketmq.mqtt.common.model.Queue; +import org.apache.rocketmq.mqtt.common.model.QueueOffset; +import org.apache.rocketmq.mqtt.common.model.Subscription; +import org.apache.rocketmq.mqtt.common.model.Constants; +import org.apache.rocketmq.mqtt.common.model.CoapMessage; +import org.apache.rocketmq.mqtt.common.model.CoapMessageType; +import org.apache.rocketmq.mqtt.common.model.CoapMessageCode; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.session.CoapSession; +import org.apache.rocketmq.mqtt.cs.session.QueueFresh; +import org.apache.rocketmq.mqtt.cs.session.infly.PushAction; +import org.apache.rocketmq.mqtt.cs.session.match.MatchAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +@Component +public class CoapSessionLoopImpl implements CoapSessionLoop { + private static Logger logger = LoggerFactory.getLogger(CoapSessionLoopImpl.class); + + @Resource + private PushAction pushAction; + + @Resource + private MatchAction matchAction; + + @Resource + private ConnectConf connectConf; + + @Resource + private LmqQueueStore lmqQueueStore; + + @Resource + private QueueFresh queueFresh; + + @Resource + private DatagramChannelManager datagramChannelManager; + + private ScheduledThreadPoolExecutor pullService; + private ScheduledThreadPoolExecutor scheduler; + private HashedWheelTimer hashedWheelTimer; + + private Map sessionMap = new ConcurrentHashMap<>(1024); + private Map pullEventMap = new ConcurrentHashMap<>(1024); + private Map pullStatus = new ConcurrentHashMap<>(1024); + + private AtomicLong rid = new AtomicLong(); + private long pullIntervalMillis = 10; + + @PostConstruct + public void init() { + pullService = new ScheduledThreadPoolExecutor(1, new ThreadFactoryImpl("coap_pull_message_thread_")); + scheduler = new ScheduledThreadPoolExecutor(2, new ThreadFactoryImpl("coap_loop_scheduler_")); + // Check new message arrive. + pullService.scheduleWithFixedDelay(() -> pullLoop(), pullIntervalMillis, pullIntervalMillis, TimeUnit.MILLISECONDS); + // Check session alive and remove expired session. + hashedWheelTimer = new HashedWheelTimer(1, TimeUnit.SECONDS); + hashedWheelTimer.start(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + for (InetSocketAddress address : sessionMap.keySet()) { + removeSession(address); + } + })); + } + + private void pullLoop() { + try { + for (Map.Entry entry : pullEventMap.entrySet()) { + PullEvent pullEvent = entry.getValue(); + CoapSession session = pullEvent.session; + if (Boolean.TRUE.equals(pullStatus.get(eventQueueKey(session, pullEvent.queue)))) { + continue; + } + doPull(pullEvent); + } + } catch (Exception e) { + logger.error("", e); + } + } + + @Override + public void addSession(CoapSession session, CompletableFuture future) { + // todo: addSubscriptionAndInit + InetSocketAddress address = session.getAddress(); + synchronized (this) { + // If this session is already exist, refresh the subscription time and do nothing. + if (sessionMap.containsKey(address)) { + return; + } + sessionMap.put(address, session); + } + // Init + AtomicInteger result = new AtomicInteger(0); + queueFresh.freshQueue(session); + Map offsetMap = session.getOffsetMap(); + result.addAndGet(offsetMap.size()); + for (Map.Entry entry : offsetMap.entrySet()) { + initOffset(session, entry.getKey(), entry.getValue(), future, result); + } + matchAction.addSubscription(session); + hashedWheelTimer.newTimeout(timeout -> checkSessionAlive(timeout, address), connectConf.getCoapSessionTimeout(), TimeUnit.MILLISECONDS); // Add to alive checker. + } + + @Override + public CoapSession getSession(InetSocketAddress address) { + return sessionMap.get(address); + } + + @Override + public CoapSession removeSession(InetSocketAddress address) { + CoapSession session = null; + try { + synchronized (this) { + session = sessionMap.remove(address); + } + // Send the last notification before removal. + if (session != null) { + session.messageNumIncrement(); + CoapMessage removeMessage = new CoapMessage( + Constants.COAP_VERSION, + session.getSubscription().getQos() == 0 ? CoapMessageType.NON : CoapMessageType.CON, + session.getToken().length, + CoapMessageCode.FORBIDDEN, + session.getMessageId() + session.getMessageNum(), + session.getToken(), + "Subscription is expired, please subscribe again.".getBytes(StandardCharsets.UTF_8), + session.getAddress() + ); + datagramChannelManager.pushMessage(null, removeMessage); + matchAction.removeSubscription(session); + // todo: persistOffset(session) + } + } catch (Exception e) { + logger.error("unloadSession fail:{}", address, e); + } + return session; + } + + @Override + public void notifyPullMessage(CoapSession session, Queue queue) { + if (session == null || queue == null) { + return; + } + logger.info("session loop impl doing notifyPullMessage queueFresh.freshQueue({}, {}})", session, session.getSubscription()); + queueFresh.freshQueue(session); + pullMessage(session, queue); + } + + private void pullMessage(CoapSession session, Queue queue) { + if (session == null || queue == null) { + return; + } + if (!session.sendingMessageIsEmpty(queue)) { + scheduler.schedule(() -> pullMessage(session, queue), pullIntervalMillis, TimeUnit.MILLISECONDS); + } else { + PullEvent pullEvent = new PullEvent(session, queue); + pullEventMap.put(eventQueueKey(session, queue), pullEvent); + } + } + + private void checkSessionAlive(Timeout timeout, InetSocketAddress address) { + CoapSession session = sessionMap.get(address); + if (session == null) { + return; + } + if (System.currentTimeMillis() - session.getSubscribeTime() > connectConf.getCoapSessionTimeout()) { + removeSession(address); + } else { + long delay = connectConf.getCoapSessionTimeout() - (System.currentTimeMillis() - session.getSubscribeTime()); + hashedWheelTimer.newTimeout(timeout.task(), delay, TimeUnit.MILLISECONDS); + } + } + + private void doPull(PullEvent pullEvent) { + CoapSession session = pullEvent.session; + Subscription subscription = session.getSubscription(); + Queue queue = pullEvent.queue; + QueueOffset queueOffset = session.getQueueOffset(queue); + if (session.getSubscription() == null || queueOffset == null) { + clearPullStatus(session, queue, pullEvent); + return; + } + + if (!queueOffset.isInitialized()) { + initOffset(session, queue, queueOffset, null, null); + scheduler.schedule(() -> pullMessage(session, queue), pullIntervalMillis, TimeUnit.MILLISECONDS); + return; + } + + pullStatus.put(eventQueueKey(session, queue), true); + int count = session.getPullSize() > 0 ? session.getPullSize() : connectConf.getPullBatchSize(); + CompletableFuture result = new CompletableFuture<>(); + result.whenComplete((pullResult, throwable) -> { + if (throwable != null) { + clearPullStatus(session, queue, pullEvent); + logger.error("{}", session.getAddress(), throwable); + scheduler.schedule(() -> pullMessage(session, queue), 1, TimeUnit.SECONDS); + return; + } + try { + if (PullResult.PULL_SUCCESS == pullResult.getCode()) { + if (pullResult.getMessageList() != null && + pullResult.getMessageList().size() >= Math.min(count, connectConf.getMaxTransferCountOnMessageInDisk())) { + scheduler.schedule(() -> pullMessage(session, queue), pullIntervalMillis, TimeUnit.MILLISECONDS); + } + boolean add = session.addSendingMessages(queue, pullResult.getMessageList()); + if (add) { + pushAction.coapMessageArrive(session, queue); + } + } else if (PullResult.PULL_OFFSET_MOVED == pullResult.getCode()) { + queueOffset.setOffset(pullResult.getNextQueueOffset().getOffset()); + pullMessage(session, queue); + } else { + logger.error("response:{},{}", session.getAddress(), JSONObject.toJSONString(pullResult)); + } + } finally { + clearPullStatus(session, queue, pullEvent); + } + }); + + CompletableFuture pullResult = lmqQueueStore.pullMessage(subscription.toFirstTopic(), queue, queueOffset, count); + pullResult.whenComplete((pullResult1, throwable) -> { + if (throwable != null) { + result.completeExceptionally(throwable); + } else { + result.complete(pullResult1); + } + }); + } + + private void clearPullStatus(CoapSession session, Queue queue, PullEvent pullEvent) { + pullEventMap.remove(eventQueueKey(session, queue), pullEvent); + pullStatus.remove(eventQueueKey(session, queue)); + } + + private void futureDone(CompletableFuture future, AtomicInteger result) { + if (future == null) { + return; + } + if (result == null) { + return; + } + if (result.decrementAndGet() <= 0) { + future.complete(null); + } + } + + private void initOffset(CoapSession session, Queue queue, QueueOffset queueOffset, CompletableFuture future, AtomicInteger result) { + if (queueOffset.isInitialized()) { + futureDone(future, result); + return; + } + if (queueOffset.isInitializing()) { + return; + } + queueOffset.setInitializing(); + CompletableFuture queryResult = lmqQueueStore.queryQueueMaxOffset(queue); + queryResult.whenComplete((maxOffset, throwable) -> { + if (throwable != null) { + logger.error("queryQueueMaxId onException {}", queue.getQueueName(), throwable); + QueueOffset thisQueueOffset = session.getQueueOffset(queue); + if (thisQueueOffset != null) { + if (!thisQueueOffset.isInitialized()) { + thisQueueOffset.setOffset(Long.MAX_VALUE); + } + thisQueueOffset.setInitialized(); + } + futureDone(future, result); + return; + } + QueueOffset thisQueueOffset = session.getQueueOffset(queue); + if (thisQueueOffset != null) { + if (!thisQueueOffset.isInitialized()) { + thisQueueOffset.setOffset(maxOffset); + } + thisQueueOffset.setInitialized(); + } + futureDone(future, result); + }); + } + + private String eventQueueKey(CoapSession session, Queue queue) { + StringBuilder sb = new StringBuilder(); + sb.append(session.getAddress()); + sb.append("-"); + sb.append(queue.getQueueId()); + sb.append("-"); + sb.append(queue.getQueueName()); + sb.append("-"); + sb.append(queue.getBrokerName()); + return sb.toString(); + } + + class PullEvent { + private CoapSession session; + private Queue queue; + private long id = rid.getAndIncrement(); + + public PullEvent(CoapSession session, Queue queue) { + this.session = session; + this.queue = queue; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + PullEvent pullEvent = (PullEvent) o; + + return id == pullEvent.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + } +} diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/match/MatchAction.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/match/MatchAction.java index 84bc4755c..5b0d97cce 100644 --- a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/match/MatchAction.java +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/match/MatchAction.java @@ -24,7 +24,9 @@ import org.apache.rocketmq.mqtt.common.model.Trie; import org.apache.rocketmq.mqtt.common.util.TopicUtils; import org.apache.rocketmq.mqtt.cs.session.Session; +import org.apache.rocketmq.mqtt.cs.session.CoapSession; import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoop; +import org.apache.rocketmq.mqtt.cs.session.loop.CoapSessionLoop; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -36,6 +38,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.net.InetSocketAddress; @Component @@ -45,9 +48,14 @@ public class MatchAction { @Resource private SessionLoop sessionLoop; + @Resource + private CoapSessionLoop coapSessionLoop; + private Trie trie = new Trie<>(); private ConcurrentMap> topicCache = new ConcurrentHashMap<>(16); + private ConcurrentMap> coapTopicCache = new ConcurrentHashMap<>(16); + public Set> matchClients(String topic, String namespace) { Set> result = new HashSet<>(); @@ -103,6 +111,31 @@ public Set> matchClients(String topic, String namesp return result; } + public Set matchCoapClients(String topic) { + Set result = new HashSet<>(); + MqttTopic mqttTopic = TopicUtils.decode(topic); + String secondTopic = TopicUtils.normalizeSecondTopic(mqttTopic.getSecondTopic()); + if (TopicUtils.isRetryTopic(topic) || TopicUtils.isP2P(secondTopic) || TopicUtils.isP2pTopic(topic)) { + return result; + } + Set addresses = new HashSet<>(); + synchronized (coapTopicCache) { + Set precises = coapTopicCache.get(topic); + if (precises != null && !precises.isEmpty()) { + addresses.addAll(precises); + } + } + // todo: add trie relative + for (InetSocketAddress address : addresses) { + CoapSession session = coapSessionLoop.getSession(address); + if (session == null) { + continue; + } + result.add(session); + } + return result; + } + public void addSubscription(Session session, Set subscriptions) { String channelId = session.getChannelId(); if (channelId == null || subscriptions == null || subscriptions.isEmpty()) { @@ -126,6 +159,16 @@ public void addSubscription(Session session, Set subscriptions) { } } + public void addSubscription(CoapSession session) { + Subscription subscription = session.getSubscription(); + String topicFilter = subscription.getTopicFilter(); + + synchronized (coapTopicCache) { + coapTopicCache.putIfAbsent(topicFilter, new HashSet<>()); + coapTopicCache.get(topicFilter).add(session.getAddress()); + } + } + public void removeSubscription(Session session, Set subscriptions) { String channelId = session.getChannelId(); if (channelId == null || subscriptions == null || subscriptions.isEmpty()) { @@ -158,4 +201,18 @@ public void removeSubscription(String channelId, Subscription subscription) { } } + public void removeSubscription(CoapSession session) { + String topicFilter = session.getSubscription().getTopicFilter(); + + synchronized (coapTopicCache) { + Set addressSet = coapTopicCache.get(topicFilter); + if (addressSet != null) { + addressSet.remove(session.getAddress()); + if (addressSet.isEmpty()) { + coapTopicCache.remove(topicFilter); + } + } + } + } + } diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/notify/MessageNotifyAction.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/notify/MessageNotifyAction.java index e1f0ec3c2..314ec607c 100644 --- a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/notify/MessageNotifyAction.java +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/session/notify/MessageNotifyAction.java @@ -24,8 +24,10 @@ import org.apache.rocketmq.mqtt.common.util.TopicUtils; import org.apache.rocketmq.mqtt.cs.session.QueueFresh; import org.apache.rocketmq.mqtt.cs.session.Session; +import org.apache.rocketmq.mqtt.cs.session.CoapSession; import org.apache.rocketmq.mqtt.cs.session.loop.QueueCache; import org.apache.rocketmq.mqtt.cs.session.loop.SessionLoop; +import org.apache.rocketmq.mqtt.cs.session.loop.CoapSessionLoop; import org.apache.rocketmq.mqtt.cs.session.match.MatchAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,6 +48,9 @@ public class MessageNotifyAction { @Resource private SessionLoop sessionLoop; + @Resource + private CoapSessionLoop coapSessionLoop; + @Resource private QueueCache queueCache; @@ -77,6 +82,24 @@ public void notify(List events) { } } } + for (MessageEvent event : events) { + Set coapResult = matchAction.matchCoapClients(TopicUtils.normalizeTopic(event.getPubTopic())); + if (coapResult == null || coapResult.isEmpty()) { + continue; + } + for (CoapSession coapSession : coapResult) { + Set set = queueFresh.freshQueue(coapSession); + if (set == null || set.isEmpty()) { + continue; + } + for (Queue queue : set) { + if (isTargetQueue(queue, event)) { + // todo: add queueCache + coapSessionLoop.notifyPullMessage(coapSession, queue); + } + } + } + } } private boolean isTargetQueue(Queue queue, MessageEvent event) { diff --git a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/starter/MqttServer.java b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/starter/MqttServer.java index f68cd57c1..4154e8334 100644 --- a/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/starter/MqttServer.java +++ b/mqtt-cs/src/main/java/org/apache/rocketmq/mqtt/cs/starter/MqttServer.java @@ -28,6 +28,7 @@ import io.netty.channel.ChannelPipeline; import io.netty.channel.WriteBufferWaterMark; import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; @@ -47,6 +48,7 @@ import java.util.concurrent.TimeUnit; import org.apache.rocketmq.mqtt.cs.channel.ConnectHandler; import org.apache.rocketmq.mqtt.cs.channel.AdaptiveTlsHandler; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; import org.apache.rocketmq.mqtt.cs.config.ConnectConf; import org.apache.rocketmq.mqtt.cs.protocol.ChannelPipelineLazyInit; import org.apache.rocketmq.mqtt.cs.protocol.MqttVersionHandler; @@ -55,6 +57,10 @@ import org.apache.rocketmq.mqtt.cs.protocol.ssl.SslFactory; import org.apache.rocketmq.mqtt.cs.protocol.ws.WebSocketServerHandler; import org.apache.rocketmq.mqtt.cs.protocol.ws.WebSocketEncoder; +import org.apache.rocketmq.mqtt.cs.protocol.coap.CoapRPCHandler; +import org.apache.rocketmq.mqtt.cs.protocol.coap.CoapDecoder; +import org.apache.rocketmq.mqtt.cs.protocol.coap.CoapEncoder; +import org.apache.rocketmq.mqtt.cs.protocol.coap.CoapPacketDispatcher; import org.apache.rocketmq.remoting.common.TlsMode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -72,6 +78,8 @@ public class MqttServer { private final ServerBootstrap wsServerBootstrap = new ServerBootstrap(); private final ServerBootstrap tlsServerBootstrap = new ServerBootstrap(); + private Bootstrap coapBootstrap = new Bootstrap(); + private final Bootstrap quicBootstrap = new Bootstrap(); @Resource @@ -86,6 +94,12 @@ public class MqttServer { @Resource private Mqtt5PacketDispatcher mqtt5PacketDispatcher; + @Resource + private CoapRPCHandler coapRPCHandler; + + @Resource + private CoapPacketDispatcher coapPacketDispatcher; + @Resource private WebSocketServerHandler webSocketServerHandler; @@ -95,6 +109,10 @@ public class MqttServer { @Resource private ChannelManager channelManager; + + @Resource + private DatagramChannelManager datagramChannelManager; + private NioEventLoopGroup acceptorEventLoopGroup; private NioEventLoopGroup workerEventLoopGroup; @@ -113,6 +131,8 @@ public void init() throws Exception { startWs(); + startCoap(); + // QUIC over DTLS if (connectConf.isEnableQuic()) { startQuic(); @@ -291,4 +311,29 @@ protected void initChannel(QuicStreamChannel ch) { LOGGER.info("MQTT server for QUIC over DTLS started, listening: {}", connectConf.getQuicPort()); } + private void startCoap() { + int port = connectConf.getCoapPort(); + coapBootstrap + .group(new NioEventLoopGroup(connectConf.getNettyWorkerThreadNum())) + .channel(NioDatagramChannel.class) + .option(ChannelOption.SO_BROADCAST, true) + .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) + .option(ChannelOption.WRITE_BUFFER_WATER_MARK,new WriteBufferWaterMark(connectConf.getLowWater(), connectConf.getHighWater())) + .localAddress(new InetSocketAddress(port)) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(DatagramChannel ch) throws Exception { + datagramChannelManager.setChannel(ch); + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast("coap-handler", coapRPCHandler); + pipeline.addLast("coap-encoder", new CoapEncoder()); + pipeline.addLast("coap-decoder", new CoapDecoder()); + pipeline.addLast("coap-dispatcher", coapPacketDispatcher); + } + }); + coapBootstrap.bind(); + LOGGER.info("start coap server , port:{}", port); + + } + } diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/channel/TestDatagramChannelManager.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/channel/TestDatagramChannelManager.java new file mode 100644 index 000000000..e1782b93c --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/channel/TestDatagramChannelManager.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.test.channel; + +import io.netty.channel.socket.DatagramChannel; +import org.apache.commons.lang.reflect.FieldUtils; +import org.apache.rocketmq.mqtt.common.model.CoapMessage; +import org.apache.rocketmq.mqtt.common.model.CoapMessageType; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; +import org.apache.rocketmq.mqtt.cs.session.CoapSession; +import org.apache.rocketmq.mqtt.cs.session.infly.CoapResponseCache; +import org.apache.rocketmq.mqtt.cs.session.infly.CoapRetryManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TestDatagramChannelManager { + + private DatagramChannelManager datagramChannelManager; + + @Mock + private CoapResponseCache coapResponseCache; + + @Mock + private CoapRetryManager coapRetryManager; + + @Mock + private DatagramChannel channel; + + @Mock + private CoapMessage message; + + @Mock + private CoapSession session; + + @Before + public void setUp() throws Exception{ + datagramChannelManager = new DatagramChannelManager(); + FieldUtils.writeDeclaredField(datagramChannelManager, "coapResponseCache", coapResponseCache, true); + FieldUtils.writeDeclaredField(datagramChannelManager, "coapRetryManager", coapRetryManager, true); + FieldUtils.writeDeclaredField(datagramChannelManager, "channel", channel, true); + } + + @Test + public void testWrite() { + datagramChannelManager.write(message); + verify(channel).writeAndFlush(message); + verifyNoMoreInteractions(coapResponseCache, coapRetryManager, channel); + } + + @Test + public void testWriteResponse() { + datagramChannelManager.writeResponse(message); + verify(channel).writeAndFlush(message); + verify(coapResponseCache).put(message); + verifyNoMoreInteractions(coapResponseCache, coapRetryManager, channel); + } + + @Test + public void testPushMessage() { + when(message.getType()).thenReturn(CoapMessageType.CON); + + datagramChannelManager.pushMessage(session, message); + verify(channel).writeAndFlush(message); + verify(coapRetryManager).addRetryMessage(session, message); + verifyNoMoreInteractions(coapResponseCache, coapRetryManager, channel); + } + + +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/TestCoapDecoder.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/TestCoapDecoder.java new file mode 100644 index 000000000..b7b903dfd --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/TestCoapDecoder.java @@ -0,0 +1,350 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.test.protocol.coap; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.DatagramPacket; +import io.netty.handler.codec.mqtt.MqttQoS; +import org.apache.commons.lang.reflect.FieldUtils; +import org.apache.rocketmq.mqtt.common.model.CoapMessageCode; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.common.model.CoapRequestType; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; +import org.apache.rocketmq.mqtt.cs.protocol.coap.CoapDecoder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +@RunWith(MockitoJUnitRunner.class) +public class TestCoapDecoder { + + private CoapDecoder coapDecoder; + private DatagramPacket packet; + private List out = new ArrayList<>(); + private InetSocketAddress localAddress = new InetSocketAddress("0.0.0.0", 5683); + private InetSocketAddress remoteAddress = new InetSocketAddress("195.56.3.1", 5683); + + @Mock + private ChannelHandlerContext ctx; + + @Mock + private DatagramChannelManager datagramChannelManager; + + @Before + public void setUp() throws Exception{ + coapDecoder = new CoapDecoder(); + FieldUtils.writeDeclaredField(coapDecoder, "datagramChannelManager", datagramChannelManager, true); + } + + @Test + public void testInvalidShortHeader() { + ByteBuf in = Unpooled.buffer(); + in.writeByte(0x40); // Version=1, Type=0, TokenLength=0 + packet = new DatagramPacket(in, localAddress, remoteAddress); + + coapDecoder.decode(ctx, packet, out); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(response.getCode(), CoapMessageCode.BAD_REQUEST); + return true; + })); + verifyNoMoreInteractions(ctx, datagramChannelManager); + } + + @Test + public void testInvalidCoapVersion() { + ByteBuf in = Unpooled.buffer(); + in.writeByte(0xC0); // Version=3, Type=0, TokenLength=0 + in.writeByte(0x01); // Code=GET + in.writeShort(0x0101); // Message ID=257 + packet = new DatagramPacket(in, localAddress, remoteAddress); + + coapDecoder.decode(ctx, packet, out); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(response.getCode(), CoapMessageCode.BAD_REQUEST); + return true; + })); + verifyNoMoreInteractions(ctx, datagramChannelManager); + } + + @Test + public void testInvalidLongToken() { + ByteBuf in = Unpooled.buffer(); + in.writeByte(0x49); // Version=1, Type=0, TokenLength=9 + in.writeByte(0x01); // Code=GET + in.writeShort(0x0101); // Message ID=257 + packet = new DatagramPacket(in, localAddress, remoteAddress); + + coapDecoder.decode(ctx, packet, out); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(response.getCode(), CoapMessageCode.BAD_REQUEST); + return true; + })); + verifyNoMoreInteractions(ctx, datagramChannelManager); + } + + @Test + public void testInvalidCoapCode() { + ByteBuf in = Unpooled.buffer(); + in.writeByte(0x40); // Version=1, Type=0, TokenLength=0 + in.writeByte(0x05); // Invalid Code=5 + in.writeShort(0x0101); // Message ID=257 + packet = new DatagramPacket(in, localAddress, remoteAddress); + + coapDecoder.decode(ctx, packet, out); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(response.getCode(), CoapMessageCode.BAD_REQUEST); + return true; + })); + verifyNoMoreInteractions(ctx, datagramChannelManager); + } + + @Test + public void testInvalidToken() { + ByteBuf in = Unpooled.buffer(); + in.writeByte(0x43); // Version=1, Type=0, TokenLength=3 + in.writeByte(0x01); // Code=GET + in.writeShort(0x0101); // Message ID=257 + in.writeByte(0x01); // Invalid token, shorter then tokenLength + packet = new DatagramPacket(in, localAddress, remoteAddress); + + coapDecoder.decode(ctx, packet, out); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(response.getCode(), CoapMessageCode.BAD_REQUEST); + return true; + })); + verifyNoMoreInteractions(ctx, datagramChannelManager); + } + + @Test + public void testValidAck() { + ByteBuf in = Unpooled.buffer(); + in.writeByte(0x60); // Version=1, Type=2, TokenLength=0 + in.writeByte(0x00); // Code=ACK + in.writeShort(0x0101); // Message ID=257 + packet = new DatagramPacket(in, localAddress, remoteAddress); + + coapDecoder.decode(ctx, packet, out); + + verify(ctx).fireChannelRead(argThat(msg -> { + assertEquals(((CoapRequestMessage) msg).getRequestType(), CoapRequestType.ACK); + return true; + })); + verifyNoMoreInteractions(ctx, datagramChannelManager); + } + + @Test + public void testInvalidOptionNumber() { + ByteBuf in = Unpooled.buffer(); + in.writeByte(0x40); // Version=1, Type=0, TokenLength=0 + in.writeByte(0x01); // Code=GET + in.writeShort(0x0101); // Message ID=257 + in.writeByte(0x20); // Invalid Option Number=2 + packet = new DatagramPacket(in, localAddress, remoteAddress); + + coapDecoder.decode(ctx, packet, out); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(response.getCode(), CoapMessageCode.BAD_OPTION); + return true; + })); + verifyNoMoreInteractions(ctx, datagramChannelManager); + } + + @Test + public void testValidSubscribe() { + ByteBuf in = Unpooled.buffer(); + in.writeByte(0x40); // Version=1, Type=0, TokenLength=0 + in.writeByte(0x01); // Code=GET + in.writeShort(0x0101); // Message ID=257 + in.writeByte(0x60); // Option: Observe, 0 + // Construct Option URI-PATH: ps/topic1/r1 + in.writeByte(0x52); // Option: URI-Path, option value length=2 + in.writeBytes("ps".getBytes()); + in.writeByte(0x06); // Option: URI-Path, option value length=6 + in.writeBytes("topic1".getBytes()); + in.writeByte(0x02); // Option: URI-Path, option value length=6 + in.writeBytes("r1".getBytes()); + packet = new DatagramPacket(in, localAddress, remoteAddress); + + coapDecoder.decode(ctx, packet, out); + + assertEquals(1, out.size()); + CoapRequestMessage outMessage = (CoapRequestMessage) out.get(0); + assertEquals(CoapRequestType.SUBSCRIBE, outMessage.getRequestType()); + assertEquals("topic1/r1", outMessage.getTopic()); + + verifyNoMoreInteractions(ctx, datagramChannelManager); + } + + @Test + public void testValidPublish() { + ByteBuf in = Unpooled.buffer(); + in.writeByte(0x40); // Version=1, Type=0, TokenLength=0 + in.writeByte(0x02); // Code=POST + in.writeShort(0x0101); // Message ID=257 + // Construct Option URI-PATH: ps/topic1/r1 + in.writeByte(0xB2); // Option: URI-Path, option value length=2 + in.writeBytes("ps".getBytes()); + in.writeByte(0x06); // Option: URI-Path, option value length=6 + in.writeBytes("topic1".getBytes()); + in.writeByte(0x02); // Option: URI-Path, option value length=6 + in.writeBytes("r1".getBytes()); + in.writeByte(0xFF); // Payload Marker + in.writeBytes("Hello!".getBytes()); // Payload + packet = new DatagramPacket(in, localAddress, remoteAddress); + + coapDecoder.decode(ctx, packet, out); + + assertEquals(1, out.size()); + CoapRequestMessage outMessage = (CoapRequestMessage) out.get(0); + assertEquals(CoapRequestType.PUBLISH, outMessage.getRequestType()); + assertEquals("topic1/r1", outMessage.getTopic()); + assertEquals("Hello!", new String(outMessage.getPayload(), StandardCharsets.UTF_8)); + + verifyNoMoreInteractions(ctx, datagramChannelManager); + } + + @Test + public void testValidConnect() { + ByteBuf in = Unpooled.buffer(); + in.writeByte(0x40); // Version=1, Type=0, TokenLength=0 + in.writeByte(0x02); // Code=POST + in.writeShort(0x0101); // Message ID=257 + // Construct Option URI-PATH: mqtt/connection + in.writeByte(0xB4); // Option: URI-Path, option value length=4 + in.writeBytes("mqtt".getBytes()); + in.writeByte(0x0A); // Option: URI-Path, option value length=10 + in.writeBytes("connection".getBytes()); + packet = new DatagramPacket(in, localAddress, remoteAddress); + + coapDecoder.decode(ctx, packet, out); + + assertEquals(1, out.size()); + CoapRequestMessage outMessage = (CoapRequestMessage) out.get(0); + assertEquals(CoapRequestType.CONNECT, outMessage.getRequestType()); + + verifyNoMoreInteractions(ctx, datagramChannelManager); + } + + @Test + public void testValidDisconnect() { + ByteBuf in = Unpooled.buffer(); + in.writeByte(0x40); // Version=1, Type=0, TokenLength=0 + in.writeByte(0x04); // Code=DELETE + in.writeShort(0x0101); // Message ID=257 + // Construct Option URI-PATH: mqtt/connection + in.writeByte(0xB4); // Option: URI-Path, option value length=4 + in.writeBytes("mqtt".getBytes()); + in.writeByte(0x0A); // Option: URI-Path, option value length=10 + in.writeBytes("connection".getBytes()); + packet = new DatagramPacket(in, localAddress, remoteAddress); + + coapDecoder.decode(ctx, packet, out); + + assertEquals(1, out.size()); + CoapRequestMessage outMessage = (CoapRequestMessage) out.get(0); + assertEquals(CoapRequestType.DISCONNECT, outMessage.getRequestType()); + + verifyNoMoreInteractions(ctx, datagramChannelManager); + } + + @Test + public void testValidHeartbeat() { + ByteBuf in = Unpooled.buffer(); + in.writeByte(0x40); // Version=1, Type=0, TokenLength=0 + in.writeByte(0x03); // Code=PUT + in.writeShort(0x0101); // Message ID=257 + // Construct Option URI-PATH: mqtt/connection + in.writeByte(0xB4); // Option: URI-Path, option value length=4 + in.writeBytes("mqtt".getBytes()); + in.writeByte(0x0A); // Option: URI-Path, option value length=10 + in.writeBytes("connection".getBytes()); + packet = new DatagramPacket(in, localAddress, remoteAddress); + + coapDecoder.decode(ctx, packet, out); + + assertEquals(1, out.size()); + CoapRequestMessage outMessage = (CoapRequestMessage) out.get(0); + assertEquals(CoapRequestType.HEARTBEAT, outMessage.getRequestType()); + + verifyNoMoreInteractions(ctx, datagramChannelManager); + } + + @Test + public void testValidQuery() { + ByteBuf in = Unpooled.buffer(); + in.writeByte(0x40); // Version=1, Type=0, TokenLength=0 + in.writeByte(0x02); // Code=POST + in.writeShort(0x0101); // Message ID=257 + // Construct Option URI-PATH: ps/topic1/r1 + in.writeByte(0xB2); // Option: URI-Path, option value length=2 + in.writeBytes("ps".getBytes()); + in.writeByte(0x06); // Option: URI-Path, option value length=6 + in.writeBytes("topic1".getBytes()); + in.writeByte(0x02); // Option: URI-Path, option value length=2 + in.writeBytes("r1".getBytes()); + // Construct Option URI-QUERY: clientid=123, qos=1, retain=true, username=admin, password=public + in.writeByte(0x4C); // Option: URI-Query, option value length=12 + in.writeBytes("clientid=123".getBytes()); + in.writeByte(0x05); + in.writeBytes("qos=1".getBytes()); + in.writeByte(0x0B); + in.writeBytes("retain=true".getBytes()); + in.writeByte(0x0B); + in.writeBytes("username=01".getBytes()); + in.writeByte(0x0C); + in.writeBytes("password=111".getBytes()); + in.writeByte(0xFF); // Payload Marker + in.writeBytes("Hello!".getBytes()); // Payload + packet = new DatagramPacket(in, localAddress, remoteAddress); + + coapDecoder.decode(ctx, packet, out); + + assertEquals(1, out.size()); + CoapRequestMessage outMessage = (CoapRequestMessage) out.get(0); + assertEquals(CoapRequestType.PUBLISH, outMessage.getRequestType()); + assertEquals("topic1/r1", outMessage.getTopic()); + assertEquals("123", outMessage.getClientId()); + assertEquals(MqttQoS.AT_LEAST_ONCE, outMessage.getQosLevel()); + assertTrue(outMessage.isReatin()); + assertEquals("01", outMessage.getUserName()); + assertEquals("111", outMessage.getPassword()); + assertEquals("Hello!", new String(outMessage.getPayload(), StandardCharsets.UTF_8)); + + verifyNoMoreInteractions(ctx, datagramChannelManager); + } +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/TestCoapEncoder.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/TestCoapEncoder.java new file mode 100644 index 000000000..bb862af9e --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/TestCoapEncoder.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.test.protocol.coap; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.DatagramPacket; +import org.apache.rocketmq.mqtt.common.model.CoapMessage; +import org.apache.rocketmq.mqtt.common.model.CoapMessageCode; +import org.apache.rocketmq.mqtt.common.model.CoapMessageType; +import org.apache.rocketmq.mqtt.common.model.Constants; +import org.apache.rocketmq.mqtt.cs.protocol.coap.CoapEncoder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +@RunWith(MockitoJUnitRunner.class) +public class TestCoapEncoder { + + private CoapEncoder coapEncoder; + private CoapMessage msg; + private List out = new ArrayList<>(); + + @Mock + private ChannelHandlerContext ctx; + + @Before + public void setUp() throws Exception{ + coapEncoder = new CoapEncoder(); + } + + @Test + public void testEncodeAck() throws Exception { + msg = new CoapMessage( + Constants.COAP_VERSION, + CoapMessageType.ACK, + 2, + CoapMessageCode.CREATED, + 1234, + new byte[]{1,2}, + null, + new InetSocketAddress("127.0.0.1", 5683) + ); + + coapEncoder.encode(ctx, msg, out); + + assertEquals(1, out.size()); + ByteBuf buffer = ((DatagramPacket) out.get(0)).content(); + assertEquals(0x62, buffer.readByte()); // version=1, type=2, tokenLength=2 + assertEquals(0x41, buffer.readByte()); // code=65 + assertEquals(1234, buffer.readShort()); // messageID + assertEquals(0x0102, buffer.readShort()); // token + + verifyNoMoreInteractions(ctx); + } + + @Test + public void testEncodeNotify() throws Exception { + msg = new CoapMessage( + Constants.COAP_VERSION, + CoapMessageType.CON, + 2, + CoapMessageCode.CONTENT, + 1234, + new byte[]{1,2}, + "Hello".getBytes(StandardCharsets.UTF_8), + new InetSocketAddress("127.0.0.1", 5683) + ); + + coapEncoder.encode(ctx, msg, out); + + assertEquals(1, out.size()); + ByteBuf buffer = ((DatagramPacket) out.get(0)).content(); + assertEquals(0x42, buffer.readByte()); // version=1, type=0, tokenLength=2 + assertEquals(0x45, buffer.readByte()); // code=69 + assertEquals(1234, buffer.readShort()); // messageID + assertEquals(0x0102, buffer.readShort()); // token + assertEquals(0xFF, buffer.readUnsignedByte()); // payload marker + assertEquals("Hello", buffer.readCharSequence(5, StandardCharsets.UTF_8).toString()); + + verifyNoMoreInteractions(ctx); + } +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/TestCoapPacketDispatcher.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/TestCoapPacketDispatcher.java new file mode 100644 index 000000000..dd99e2888 --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/TestCoapPacketDispatcher.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.test.protocol.coap; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.embedded.EmbeddedChannel; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.CoapMessage; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.common.model.CoapRequestType; +import org.apache.rocketmq.mqtt.cs.protocol.coap.CoapPacketDispatcher; +import org.apache.rocketmq.mqtt.cs.protocol.coap.handler.CoapPublishHandler; +import org.apache.rocketmq.mqtt.cs.protocol.coap.handler.CoapSubscribeHandler; +import org.apache.rocketmq.mqtt.cs.session.infly.CoapResponseCache; +import org.apache.rocketmq.mqtt.ds.upstream.coap.processor.CoapPublishProcessor; +import org.apache.rocketmq.mqtt.ds.upstream.coap.processor.CoapSubscribeProcessor; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.concurrent.CompletableFuture; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.eq; + +@RunWith(MockitoJUnitRunner.class) +public class TestCoapPacketDispatcher { + + @InjectMocks + private CoapPacketDispatcher coapPacketDispatcher; + + private EmbeddedChannel channel; + + @Mock + private CoapRequestMessage msg; + + @Mock + private CoapResponseCache coapResponseCache; + + @Mock + private CoapMessage oldResponse; + + @Mock + private CoapPublishHandler coapPublishHandler; + + @Mock + private CoapPublishProcessor coapPublishProcessor; + + @Mock + private CoapSubscribeHandler coapSubscribeHandler; + + @Mock + private CoapSubscribeProcessor coapSubscribeProcessor; + + @Before + public void setUp() throws IllegalAccessException { + channel = new EmbeddedChannel(coapPacketDispatcher); + } + + @Test + public void testRead0Retransmit() { + when(msg.getMessageId()).thenReturn(1); + when(coapResponseCache.get(anyInt())).thenReturn(oldResponse); + // Pass msg into channel and invoke channelRead0() + channel.writeInbound(msg); + // Assert that the response is written to the channel + verify(coapResponseCache).get(1); + Object out = channel.readOutbound(); + assertEquals(oldResponse, out); + assertNull(channel.readOutbound()); + } + + @Test + public void testRead0Publish() { + when(msg.getRequestType()).thenReturn(CoapRequestType.PUBLISH); + when(coapPublishHandler.preHandler(any(ChannelHandlerContext.class), any(CoapRequestMessage.class))).thenReturn(true); + CompletableFuture processResult = new CompletableFuture<>(); + processResult.complete(new HookResult(HookResult.SUCCESS, null, null)); + when(coapPublishProcessor.process(any(CoapRequestMessage.class))).thenReturn(processResult); + + channel.writeInbound(msg); + + verify(coapPublishHandler).preHandler(any(ChannelHandlerContext.class), eq(msg)); + verify(coapPublishProcessor).process(msg); + verify(coapPublishHandler).doHandler(any(ChannelHandlerContext.class), eq(msg), any(HookResult.class)); + assertNull(channel.readOutbound()); + } + + @Test + public void testRead0Subscribe() { + when(msg.getRequestType()).thenReturn(CoapRequestType.SUBSCRIBE); + when(coapSubscribeHandler.preHandler(any(ChannelHandlerContext.class), any(CoapRequestMessage.class))).thenReturn(true); + CompletableFuture processResult = new CompletableFuture<>(); + processResult.complete(new HookResult(HookResult.SUCCESS, null, null)); + when(coapSubscribeProcessor.process(any(CoapRequestMessage.class))).thenReturn(processResult); + + channel.writeInbound(msg); + + verify(coapSubscribeHandler).preHandler(any(ChannelHandlerContext.class), eq(msg)); + verify(coapSubscribeProcessor).process(msg); + verify(coapSubscribeHandler).doHandler(any(ChannelHandlerContext.class), eq(msg), any(HookResult.class)); + assertNull(channel.readOutbound()); + } +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/TestCoapRPCHandler.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/TestCoapRPCHandler.java new file mode 100644 index 000000000..9a96b43e2 --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/TestCoapRPCHandler.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.test.protocol.coap; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.DatagramPacket; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.mqtt.common.facade.MetaPersistManager; +import org.apache.rocketmq.mqtt.cs.protocol.coap.CoapRPCHandler; +import org.apache.rocketmq.mqtt.ds.notify.NotifyManager; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; + +@RunWith(MockitoJUnitRunner.class) +public class TestCoapRPCHandler { + + private CoapRPCHandler coapRPCHandler; + + @Mock + private MetaPersistManager metaPersistManager; + + @Mock + private NotifyManager notifyManager; + + @Mock + private ChannelHandlerContext ctx; + + @Mock + private DatagramPacket packet; + + @Before + public void setUp() throws IllegalAccessException { + coapRPCHandler = new CoapRPCHandler(); + FieldUtils.writeDeclaredField(coapRPCHandler, "metaPersistManager", metaPersistManager, true); + FieldUtils.writeDeclaredField(coapRPCHandler, "notifyManager", notifyManager, true); + + when(packet.sender()).thenReturn(new InetSocketAddress("125.45.12.1", 5683)); + } + + @Test + public void testNoConnectNode() throws Exception { + when(metaPersistManager.getConnectNodeSet()).thenReturn(null); + + assertThrows(RemotingException.class, () -> coapRPCHandler.channelRead(ctx, packet)); + verify(metaPersistManager).getConnectNodeSet(); + verifyNoMoreInteractions(metaPersistManager, notifyManager, ctx); + } + + @Test + public void testForLocalhost() throws Exception { + String localAdress = InetAddress.getLocalHost().getHostAddress(); + Set nodes = new HashSet<>(); + nodes.add(localAdress); + when(metaPersistManager.getConnectNodeSet()).thenReturn(nodes); + + coapRPCHandler.channelRead(ctx, packet); + + verify(metaPersistManager).getConnectNodeSet(); + verify(ctx).fireChannelRead(packet); + verifyNoMoreInteractions(metaPersistManager, notifyManager, ctx); + } + + @Test + public void testForwardSuccess() throws Exception { + String forwardAddress = "10.11.123.1"; + Set nodes = new HashSet<>(); + nodes.add(forwardAddress); + when(metaPersistManager.getConnectNodeSet()).thenReturn(nodes); + when(notifyManager.doCoapForward(anyString(), any(DatagramPacket.class))).thenReturn(true); + + coapRPCHandler.channelRead(ctx, packet); + + verify(metaPersistManager).getConnectNodeSet(); + verify(notifyManager).doCoapForward(forwardAddress, packet); + verifyNoMoreInteractions(metaPersistManager, notifyManager, ctx); + } +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapAckHandler.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapAckHandler.java new file mode 100644 index 000000000..48a706a69 --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapAckHandler.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.test.protocol.coap.handler; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.CoapMessageCode; +import org.apache.rocketmq.mqtt.common.model.CoapMessageType; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.cs.protocol.coap.handler.CoapAckHandler; +import org.apache.rocketmq.mqtt.cs.session.infly.CoapRetryManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.InetSocketAddress; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +@RunWith(MockitoJUnitRunner.class) +public class TestCoapAckHandler { + + private CoapAckHandler coapAckHandler; + + @Mock + private CoapRetryManager coapRetryManager; + + @Mock + private ChannelHandlerContext ctx; + + private CoapRequestMessage coapMessage; + + @Before + public void setUp() throws IllegalAccessException { + coapAckHandler = new CoapAckHandler(); + FieldUtils.writeDeclaredField(coapAckHandler, "coapRetryManager", coapRetryManager, true); + coapMessage = new CoapRequestMessage( + 1, + CoapMessageType.ACK, + 0, + CoapMessageCode.EMPTY, + 1111, + null, + null, + new InetSocketAddress("127.0.0.1", 9675) + ); + } + + @Test + public void testPreHandler() { + boolean result = coapAckHandler.preHandler(ctx, coapMessage); + assertTrue(result); + } + + @Test + public void testAckSuccess() { + HookResult successHookResult = new HookResult(HookResult.SUCCESS, null, null); + when(coapRetryManager.contains(anyInt())).thenReturn(true); + + coapAckHandler.doHandler(ctx, coapMessage, successHookResult); + + verify(coapRetryManager).contains(anyInt()); + verify(coapRetryManager).ackMessage(anyInt()); + verifyNoMoreInteractions(ctx, coapRetryManager); + } +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapConnectHandler.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapConnectHandler.java new file mode 100644 index 000000000..b97a1f5ce --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapConnectHandler.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.test.protocol.coap.handler; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.commons.lang.reflect.FieldUtils; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.Constants; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.common.model.CoapMessageCode; +import org.apache.rocketmq.mqtt.common.model.CoapMessageType; +import org.apache.rocketmq.mqtt.common.model.CoapRequestType; +import org.apache.rocketmq.mqtt.common.util.CoapTokenUtil; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; +import org.apache.rocketmq.mqtt.cs.protocol.coap.handler.CoapConnectHandler; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +@RunWith(MockitoJUnitRunner.class) +public class TestCoapConnectHandler { + + private CoapConnectHandler coapConnectHandler; + private CoapRequestMessage coapMessage; + + @Mock + private DatagramChannelManager datagramChannelManager; + + @Mock + private ChannelHandlerContext ctx; + + @Before + public void setUp() throws Exception { + coapConnectHandler = new CoapConnectHandler(); + FieldUtils.writeDeclaredField(coapConnectHandler, "datagramChannelManager", datagramChannelManager, true); + coapMessage = new CoapRequestMessage( + Constants.COAP_VERSION, + CoapMessageType.CON, + 0, + CoapMessageCode.POST, + 1111, + null, + null, + new InetSocketAddress("127.0.0.1", 9675) + ); + coapMessage.setRequestType(CoapRequestType.CONNECT); + coapMessage.setClientId("123"); + coapMessage.setUserName("admin"); + coapMessage.setPassword("public"); + } + + @Test + public void testPreHandler() { + assertTrue(coapConnectHandler.preHandler(ctx, coapMessage)); + verifyNoMoreInteractions(datagramChannelManager, ctx); + + coapMessage.setClientId(null); + assertFalse(coapConnectHandler.preHandler(ctx, coapMessage)); + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(CoapMessageCode.BAD_REQUEST, response.getCode()); + return true; + })); + verifyNoMoreInteractions(datagramChannelManager, ctx); + } + + @Test + public void testConnectFail() { + HookResult failHookResult = new HookResult(HookResult.FAIL, "Error", null); + + coapConnectHandler.doHandler(ctx, coapMessage, failHookResult); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(CoapMessageCode.UNAUTHORIZED, response.getCode()); + assertEquals(failHookResult.getRemark(), new String(response.getPayload(), StandardCharsets.UTF_8)); + return true; + })); + verifyNoMoreInteractions(datagramChannelManager, ctx); + } + + @Test + public void testConnectSuccess() { + HookResult successHookResult = new HookResult(HookResult.SUCCESS, null, null); + + coapConnectHandler.doHandler(ctx, coapMessage, successHookResult); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(CoapMessageCode.CREATED, response.getCode()); + try { + assertTrue(CoapTokenUtil.isValid(coapMessage.getClientId(), new String(response.getPayload(), StandardCharsets.UTF_8))); + } catch (Exception e) { + throw new RuntimeException(e); + } + return true; + })); + verifyNoMoreInteractions(datagramChannelManager, ctx); + } +} \ No newline at end of file diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapDisconnectHandler.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapDisconnectHandler.java new file mode 100644 index 000000000..4eb088191 --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapDisconnectHandler.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.test.protocol.coap.handler; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.commons.lang.reflect.FieldUtils; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.Constants; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.common.model.CoapMessageCode; +import org.apache.rocketmq.mqtt.common.model.CoapMessageType; +import org.apache.rocketmq.mqtt.common.model.CoapRequestType; +import org.apache.rocketmq.mqtt.common.util.CoapTokenUtil; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; +import org.apache.rocketmq.mqtt.cs.protocol.coap.handler.CoapDisconnectHandler; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + + +@RunWith(MockitoJUnitRunner.class) +public class TestCoapDisconnectHandler { + + private CoapDisconnectHandler coapDisconnectHandler; + private CoapRequestMessage coapMessage; + + @Mock + private DatagramChannelManager datagramChannelManager; + + @Mock + private ChannelHandlerContext ctx; + + @Before + public void setUp() throws Exception { + coapDisconnectHandler = new CoapDisconnectHandler(); + FieldUtils.writeDeclaredField(coapDisconnectHandler, "datagramChannelManager", datagramChannelManager, true); + coapMessage = new CoapRequestMessage( + Constants.COAP_VERSION, + CoapMessageType.CON, + 0, + CoapMessageCode.DELETED, + 1111, + null, + null, + new InetSocketAddress("127.0.0.1", 9675) + ); + coapMessage.setRequestType(CoapRequestType.DISCONNECT); + coapMessage.setClientId("123"); + coapMessage.setAuthToken(CoapTokenUtil.generateToken("123")); + } + + @Test + public void testPreHandler() { + assertTrue(coapDisconnectHandler.preHandler(ctx, coapMessage)); + verifyNoMoreInteractions(datagramChannelManager, ctx); + + coapMessage.setClientId(null); + assertFalse(coapDisconnectHandler.preHandler(ctx, coapMessage)); + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(CoapMessageCode.BAD_REQUEST, response.getCode()); + return true; + })); + verifyNoMoreInteractions(datagramChannelManager, ctx); + } + + @Test + public void testDisconnectFail() { + HookResult failHookResult = new HookResult(HookResult.FAIL, "Error", null); + + coapDisconnectHandler.doHandler(ctx, coapMessage, failHookResult); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(CoapMessageCode.INTERNAL_SERVER_ERROR, response.getCode()); + assertEquals(failHookResult.getRemark(), new String(response.getPayload(), StandardCharsets.UTF_8)); + return true; + })); + verifyNoMoreInteractions(datagramChannelManager, ctx); + } + + @Test + public void testDisconnectUnauthorized() { + HookResult successHookResult = new HookResult(HookResult.SUCCESS, null, null); + coapMessage.setAuthToken("wrongToken"); + + coapDisconnectHandler.doHandler(ctx, coapMessage, successHookResult); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(CoapMessageCode.UNAUTHORIZED, response.getCode()); + assertEquals("AuthToken is not valid.", new String(response.getPayload(), StandardCharsets.UTF_8)); + return true; + })); + verifyNoMoreInteractions(datagramChannelManager, ctx); + } + + @Test + public void testDisconnectSuccess() { + HookResult successHookResult = new HookResult(HookResult.SUCCESS, null, null); + + coapDisconnectHandler.doHandler(ctx, coapMessage, successHookResult); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(CoapMessageCode.DELETED, response.getCode()); + return true; + })); + verifyNoMoreInteractions(datagramChannelManager, ctx); + } +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapHeartbeatHandler.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapHeartbeatHandler.java new file mode 100644 index 000000000..9321d814a --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapHeartbeatHandler.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.test.protocol.coap.handler; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.commons.lang.reflect.FieldUtils; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.Constants; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.common.model.CoapMessageCode; +import org.apache.rocketmq.mqtt.common.model.CoapMessageType; +import org.apache.rocketmq.mqtt.common.model.CoapRequestType; +import org.apache.rocketmq.mqtt.common.util.CoapTokenUtil; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; +import org.apache.rocketmq.mqtt.cs.protocol.coap.handler.CoapHeartbeatHandler; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +@RunWith(MockitoJUnitRunner.class) +public class TestCoapHeartbeatHandler { + + private CoapHeartbeatHandler coapHeartbeatHandler; + private CoapRequestMessage coapMessage; + + @Mock + private DatagramChannelManager datagramChannelManager; + + @Mock + private ChannelHandlerContext ctx; + + @Before + public void setUp() throws Exception { + coapHeartbeatHandler = new CoapHeartbeatHandler(); + FieldUtils.writeDeclaredField(coapHeartbeatHandler, "datagramChannelManager", datagramChannelManager, true); + coapMessage = new CoapRequestMessage( + Constants.COAP_VERSION, + CoapMessageType.CON, + 0, + CoapMessageCode.PUT, + 1111, + null, + null, + new InetSocketAddress("127.0.0.1", 9675) + ); + coapMessage.setRequestType(CoapRequestType.HEARTBEAT); + coapMessage.setClientId("123"); + coapMessage.setAuthToken(CoapTokenUtil.generateToken("123")); + } + + @Test + public void testPreHandler() { + assertTrue(coapHeartbeatHandler.preHandler(ctx, coapMessage)); + verifyNoMoreInteractions(datagramChannelManager, ctx); + + coapMessage.setClientId(null); + assertFalse(coapHeartbeatHandler.preHandler(ctx, coapMessage)); + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(CoapMessageCode.BAD_REQUEST, response.getCode()); + return true; + })); + verifyNoMoreInteractions(datagramChannelManager, ctx); + } + + @Test + public void testHeartbeatFail() { + HookResult failHookResult = new HookResult(HookResult.FAIL, "Error", null); + + coapHeartbeatHandler.doHandler(ctx, coapMessage, failHookResult); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(CoapMessageCode.INTERNAL_SERVER_ERROR, response.getCode()); + assertEquals(failHookResult.getRemark(), new String(response.getPayload(), StandardCharsets.UTF_8)); + return true; + })); + verifyNoMoreInteractions(datagramChannelManager, ctx); + } + + @Test + public void testHeartbeatUnauthorized() { + HookResult successHookResult = new HookResult(HookResult.SUCCESS, null, null); + coapMessage.setAuthToken("wrongToken"); + + coapHeartbeatHandler.doHandler(ctx, coapMessage, successHookResult); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(CoapMessageCode.UNAUTHORIZED, response.getCode()); + assertEquals("AuthToken is not valid.", new String(response.getPayload(), StandardCharsets.UTF_8)); + return true; + })); + verifyNoMoreInteractions(datagramChannelManager, ctx); + } + + @Test + public void testHeartbeatSuccess() { + HookResult successHookResult = new HookResult(HookResult.SUCCESS, null, null); + + coapHeartbeatHandler.doHandler(ctx, coapMessage, successHookResult); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(CoapMessageCode.CHANGED, response.getCode()); + return true; + })); + verifyNoMoreInteractions(datagramChannelManager, ctx); + } +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapPublishHandler.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapPublishHandler.java new file mode 100644 index 000000000..9420ee9c9 --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapPublishHandler.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.test.protocol.coap.handler; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.CoapMessageCode; +import org.apache.rocketmq.mqtt.common.model.CoapMessageType; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.protocol.coap.handler.CoapPublishHandler; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +@RunWith(MockitoJUnitRunner.class) +public class TestCoapPublishHandler { + + private CoapPublishHandler coapPublishHandler; + private CoapRequestMessage coapMessage; + + @Mock + private DatagramChannelManager datagramChannelManager; + + @Mock + private ChannelHandlerContext ctx; + + @Mock + private ConnectConf connectConf; + + @Before + public void setUp() throws IllegalAccessException { + coapPublishHandler = new CoapPublishHandler(); + FieldUtils.writeDeclaredField(coapPublishHandler, "datagramChannelManager", datagramChannelManager, true); + FieldUtils.writeDeclaredField(coapPublishHandler, "connectConf", connectConf, true); + coapMessage = new CoapRequestMessage( + 1, + CoapMessageType.CON, + 0, + CoapMessageCode.POST, + 1111, + null, + "TestData".getBytes(StandardCharsets.UTF_8), + new InetSocketAddress("127.0.0.1", 9675) + ); + } + + @Test + public void testPreHandler() { + when(connectConf.isEnableCoapConnect()).thenReturn(false); + boolean result = coapPublishHandler.preHandler(ctx, coapMessage); + assertTrue(result); + } + + @Test + public void testDoHandlerUpstreamFail() { + HookResult failHookResult = new HookResult(HookResult.FAIL, "Error", null); + + coapPublishHandler.doHandler(ctx, coapMessage, failHookResult); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(response.getCode(), CoapMessageCode.INTERNAL_SERVER_ERROR); + return true; + })); + verifyNoMoreInteractions(ctx, datagramChannelManager, connectConf); + } + + @Test + public void testDoHandlerSuccess() { + HookResult successHookResult = new HookResult(HookResult.SUCCESS, null, null); + + coapPublishHandler.doHandler(ctx, coapMessage, successHookResult); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(response.getCode(), CoapMessageCode.CREATED); + return true; + })); + verifyNoMoreInteractions(ctx, datagramChannelManager, connectConf); + } +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapSubscribeHandler.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapSubscribeHandler.java new file mode 100644 index 000000000..95d1e3d85 --- /dev/null +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/coap/handler/TestCoapSubscribeHandler.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.cs.test.protocol.coap.handler; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.mqtt.MqttQoS; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.mqtt.common.facade.RetainedPersistManager; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.CoapMessageCode; +import org.apache.rocketmq.mqtt.common.model.CoapMessageType; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.common.model.Constants; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; +import org.apache.rocketmq.mqtt.cs.config.ConnectConf; +import org.apache.rocketmq.mqtt.cs.protocol.coap.handler.CoapSubscribeHandler; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; + +import org.apache.rocketmq.mqtt.cs.session.CoapSession; +import org.apache.rocketmq.mqtt.cs.session.loop.CoapSessionLoop; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TestCoapSubscribeHandler { + + private CoapSubscribeHandler coapSubscribeHandler; + private CoapRequestMessage coapMessage; + + @Mock + private CoapSessionLoop sessionLoop; + + @Mock + private RetainedPersistManager retainedPersistManager; + + @Mock + private DatagramChannelManager datagramChannelManager; + + @Mock + private ChannelHandlerContext ctx; + + @Mock + private CoapSession session; + + @Mock + private ConnectConf connectConf; + + @Before + public void setUp() throws IllegalAccessException { + coapSubscribeHandler = new CoapSubscribeHandler(); + FieldUtils.writeDeclaredField(coapSubscribeHandler, "sessionLoop", sessionLoop, true); + FieldUtils.writeDeclaredField(coapSubscribeHandler, "retainedPersistManager", retainedPersistManager, true); + FieldUtils.writeDeclaredField(coapSubscribeHandler, "datagramChannelManager", datagramChannelManager, true); + FieldUtils.writeDeclaredField(coapSubscribeHandler, "connectConf", connectConf, true); + coapMessage = new CoapRequestMessage( + Constants.COAP_VERSION, + CoapMessageType.CON, + 0, + CoapMessageCode.GET, + 1111, + null, + "TestData".getBytes(StandardCharsets.UTF_8), + new InetSocketAddress("127.0.0.1", 9675) + ); + coapMessage.setTopic("topic1/r1"); + coapMessage.setQosLevel(MqttQoS.AT_LEAST_ONCE); + } + + @Test + public void testPreHandler() { + when(connectConf.isEnableCoapConnect()).thenReturn(false); + boolean result = coapSubscribeHandler.preHandler(ctx, coapMessage); + assertTrue(result); + } + + @Test + public void testDoHandlerUpstreamFail() { + HookResult failHookResult = new HookResult(HookResult.FAIL, "Error", null); + + coapSubscribeHandler.doHandler(ctx, coapMessage, failHookResult); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(response.getCode(), CoapMessageCode.INTERNAL_SERVER_ERROR); + return true; + })); + verifyNoMoreInteractions(ctx, sessionLoop, retainedPersistManager, datagramChannelManager, connectConf); + } + + @Test + public void testDoHanlderOldSession() { + HookResult successHookResult = new HookResult(HookResult.SUCCESS, null, null); + when(sessionLoop.getSession(any(InetSocketAddress.class))).thenReturn(session); + + coapSubscribeHandler.doHandler(ctx, coapMessage, successHookResult); + + verify(sessionLoop).getSession(any(InetSocketAddress.class)); + verify(session).refreshSubscribeTime(); + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(response.getCode(), CoapMessageCode.CONTENT); + return true; + })); + verifyNoMoreInteractions(ctx, sessionLoop, retainedPersistManager, datagramChannelManager, connectConf); + } + + @Test + public void testDoHanlderNewSession() { + HookResult successHookResult = new HookResult(HookResult.SUCCESS, null, null); + when(sessionLoop.getSession(any(InetSocketAddress.class))).thenReturn(null); + + coapSubscribeHandler.doHandler(ctx, coapMessage, successHookResult); + + verify(sessionLoop).getSession(any(InetSocketAddress.class)); + verify(sessionLoop).addSession(any(CoapSession.class), any()); + verifyNoMoreInteractions(ctx, sessionLoop, retainedPersistManager, datagramChannelManager, connectConf); + } + + @Test + public void testDoResponseFail() { + coapSubscribeHandler.doResponseFail(coapMessage, "Error"); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(response.getCode(), CoapMessageCode.INTERNAL_SERVER_ERROR); + return true; + })); + verifyNoMoreInteractions(ctx, sessionLoop, retainedPersistManager, datagramChannelManager, connectConf); + } + + @Test + public void testDoResponseSuccess() { + coapSubscribeHandler.doResponseSuccess(coapMessage, session); + + verify(datagramChannelManager).writeResponse(argThat(response -> { + assertEquals(response.getCode(), CoapMessageCode.CONTENT); + return true; + })); + verifyNoMoreInteractions(ctx, sessionLoop, retainedPersistManager, datagramChannelManager); + } +} diff --git a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/rpc/TestRpcPacketDispatcher.java b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/rpc/TestRpcPacketDispatcher.java index 548be0221..50c074420 100644 --- a/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/rpc/TestRpcPacketDispatcher.java +++ b/mqtt-cs/src/test/java/org/apache/rocketmq/mqtt/cs/test/protocol/rpc/TestRpcPacketDispatcher.java @@ -18,10 +18,15 @@ package org.apache.rocketmq.mqtt.cs.test.protocol.rpc; import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.DatagramPacket; import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.rocketmq.mqtt.common.model.RpcCode; import org.apache.rocketmq.mqtt.cs.channel.ChannelManager; +import org.apache.rocketmq.mqtt.cs.channel.DatagramChannelManager; import org.apache.rocketmq.mqtt.cs.protocol.rpc.RpcPacketDispatcher; import org.apache.rocketmq.mqtt.cs.session.notify.MessageNotifyAction; import org.apache.rocketmq.remoting.protocol.RemotingCommand; @@ -32,17 +37,20 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import java.nio.charset.StandardCharsets; import java.util.HashSet; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class TestRpcPacketDispatcher { private RemotingCommand notifyCommand = RemotingCommand.createResponseCommand(RpcCode.CMD_NOTIFY_MQTT_MESSAGE, null); private RemotingCommand closeCommand = RemotingCommand.createResponseCommand(RpcCode.CMD_CLOSE_CHANNEL, null); + private RemotingCommand notifyCoapCommand = RemotingCommand.createResponseCommand(RpcCode.COM_NOTIFY_COAP_MESSAGE, null); private RpcPacketDispatcher packetDispatcher; @@ -52,14 +60,27 @@ public class TestRpcPacketDispatcher { @Mock private ChannelManager channelManager; + @Mock + private DatagramChannelManager datagramChannelManager; + @Mock private ChannelHandlerContext ctx; + @Mock + private DatagramChannel datagramChannel; + + @Mock + private ChannelPipeline pipeline; + + @Mock + private ChannelHandlerContext coapContext; + @Before public void setUp() throws Exception { packetDispatcher = new RpcPacketDispatcher(); FieldUtils.writeDeclaredField(packetDispatcher, "messageNotifyAction", messageNotifyAction, true); FieldUtils.writeDeclaredField(packetDispatcher, "channelManager", channelManager, true); + FieldUtils.writeDeclaredField(packetDispatcher, "datagramChannelManager", datagramChannelManager, true); } @Test @@ -92,4 +113,27 @@ public void testProcessRequestFail() throws Exception { public void testRejectRequest() { Assert.assertFalse(packetDispatcher.rejectRequest()); } + + @Test + public void testProcessRequestNotifyCoap() throws Exception { + when(datagramChannelManager.getChannel()).thenReturn(datagramChannel); + when(datagramChannel.pipeline()).thenReturn(pipeline); + when(pipeline.context("coap-handler")).thenReturn(coapContext); + + JSONObject jsonObject = new JSONObject(); + jsonObject.put("data", new byte[]{1, 2, 3}); + jsonObject.put("senderAddress", "127.0.0.1"); + jsonObject.put("senderPort", 1234); + jsonObject.put("recipientAddress", "192.168.1.1"); + jsonObject.put("recipientPort", 5678); + + notifyCoapCommand.setBody(jsonObject.toJSONString().getBytes(StandardCharsets.UTF_8)); + packetDispatcher.processRequest(ctx, notifyCoapCommand); + + verify(datagramChannelManager).getChannel(); + verify(datagramChannel).pipeline(); + verify(pipeline).context("coap-handler"); + verify(coapContext).fireChannelRead(any(DatagramPacket.class)); + verifyNoMoreInteractions(messageNotifyAction, channelManager, ctx); + } } diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/auth/CoapAuthManager.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/auth/CoapAuthManager.java new file mode 100644 index 000000000..d5c4b96c3 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/auth/CoapAuthManager.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.ds.auth; + +import io.netty.handler.codec.mqtt.MqttConnectReturnCode; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.Remark; +import org.apache.rocketmq.mqtt.common.util.PasswordHashUtil; +import org.apache.rocketmq.mqtt.ds.config.ServiceConf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.CompletableFuture; + +@Component +public class CoapAuthManager { + public static Logger logger = LoggerFactory.getLogger(CoapAuthManager.class); + + private static String salt; + private static final String ALGORITHM = "SHA-256"; + private static final String SALT_POSITION = "suffix"; + private static String hashedPassword; + + @Resource + private ServiceConf serviceConf; + + @PostConstruct + public void init() throws NoSuchAlgorithmException { + salt = PasswordHashUtil.generateSalt(16); + hashedPassword = PasswordHashUtil.hashWithSalt(serviceConf.getSecretKey(), salt, ALGORITHM, SALT_POSITION); + } + + public CompletableFuture doAuth(String username, String password) { + try { + if (serviceConf.getUsername().equals(username) && PasswordHashUtil.validatePassword(password, hashedPassword, salt, ALGORITHM, SALT_POSITION)) { + return HookResult.newHookResult(HookResult.SUCCESS, null, null); + } + } catch (NoSuchAlgorithmException e) { + logger.error("", e); + } + return HookResult.newHookResult(HookResult.FAIL, MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USERNAME_OR_PASSWORD.byteValue(), Remark.AUTH_FAILED, null); + } + +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/notify/NotifyManager.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/notify/NotifyManager.java index d94b0f6b8..d9ce68279 100644 --- a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/notify/NotifyManager.java +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/notify/NotifyManager.java @@ -18,6 +18,9 @@ package org.apache.rocketmq.mqtt.ds.notify; import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import io.netty.buffer.ByteBuf; +import io.netty.channel.socket.DatagramPacket; import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; @@ -59,6 +62,7 @@ import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.net.InetSocketAddress; @Component @@ -270,6 +274,40 @@ protected boolean doNotify(String node, Set messageEvents) { } } + public boolean doCoapForward(String node, DatagramPacket packet) { + Set connectorNodes = metaPersistManager.getConnectNodeSet(); + if (connectorNodes == null || connectorNodes.isEmpty()) { + return false; + } + if (!connectorNodes.contains(node)) { + return true; + } + try { + // Serialization, change datagram packet to bytes. + ByteBuf buffer = packet.content(); + byte[] data = new byte[buffer.readableBytes()]; + buffer.getBytes(buffer.readerIndex(), data); + InetSocketAddress sender = packet.sender(); + InetSocketAddress recipient = packet.recipient(); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("data", data); + jsonObject.put("senderAddress", sender.getAddress().getHostAddress()); + jsonObject.put("senderPort", sender.getPort()); + jsonObject.put("recipientAddress", recipient.getAddress().getHostAddress()); + jsonObject.put("recipientPort", recipient.getPort()); + // Create RPC command. + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RpcCode.COM_NOTIFY_COAP_MESSAGE, + null); + remotingCommand.setBody(JSON.toJSONBytes(jsonObject)); + // RPC invoke. + RemotingCommand response = remotingClient.invokeSync(node + ":" + serviceConf.getCsRpcPort(), remotingCommand, 1000); + return response.getCode() == RpcCode.SUCCESS; + } catch (Exception e) { + logger.error("fail coap forward {}", node, e); + return false; + } + } + private RemotingCommand createMsgEventCommand(Set messageEvents) { RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RpcCode.CMD_NOTIFY_MQTT_MESSAGE, null); diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/CoapUpstreamProcessor.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/CoapUpstreamProcessor.java new file mode 100644 index 000000000..8c023b619 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/CoapUpstreamProcessor.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.ds.upstream.coap; + +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.remoting.exception.RemotingException; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public interface CoapUpstreamProcessor { + /** + * process mqtt upstream packet + * @param msg + * @return + */ + CompletableFuture process(CoapRequestMessage msg) throws RemotingException, com.alipay.sofa.jraft.error.RemotingException, ExecutionException, InterruptedException; +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapAckProcessor.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapAckProcessor.java new file mode 100644 index 000000000..7f74d02a7 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapAckProcessor.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.ds.upstream.coap.processor; + +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.ds.upstream.coap.CoapUpstreamProcessor; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; + +@Component +public class CoapAckProcessor implements CoapUpstreamProcessor { + + @Override + public CompletableFuture process(CoapRequestMessage coapMessage) { + return HookResult.newHookResult(HookResult.SUCCESS, null, null); + } +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapConnectProcessor.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapConnectProcessor.java new file mode 100644 index 000000000..63963e429 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapConnectProcessor.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.ds.upstream.coap.processor; + +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.ds.auth.CoapAuthManager; +import org.apache.rocketmq.mqtt.ds.upstream.coap.CoapUpstreamProcessor; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.concurrent.CompletableFuture; + +@Component +public class CoapConnectProcessor implements CoapUpstreamProcessor { + + @Resource + private CoapAuthManager coapAuthManager; + + @Override + public CompletableFuture process(CoapRequestMessage msg) { + return coapAuthManager.doAuth(msg.getUserName(), msg.getPassword()); + } +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapDisconnectProcessor.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapDisconnectProcessor.java new file mode 100644 index 000000000..00fc1fc0c --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapDisconnectProcessor.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.ds.upstream.coap.processor; + +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.ds.upstream.coap.CoapUpstreamProcessor; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; + +@Component +public class CoapDisconnectProcessor implements CoapUpstreamProcessor { + @Override + public CompletableFuture process(CoapRequestMessage msg) { + // RPC broadcast (clear token) + return null; + } +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapHeartbeatProcessor.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapHeartbeatProcessor.java new file mode 100644 index 000000000..17056de80 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapHeartbeatProcessor.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.ds.upstream.coap.processor; + +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.ds.upstream.coap.CoapUpstreamProcessor; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; + +@Component +public class CoapHeartbeatProcessor implements CoapUpstreamProcessor { + @Override + public CompletableFuture process(CoapRequestMessage msg) { + // todo: RPC broadcast (refresh token alive time) + return null; + } +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapPublishProcessor.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapPublishProcessor.java new file mode 100644 index 000000000..6b3e93f83 --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapPublishProcessor.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.ds.upstream.coap.processor; + +import com.alibaba.fastjson.JSON; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.mqtt.common.facade.LmqQueueStore; +import org.apache.rocketmq.mqtt.common.facade.RetainedPersistManager; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.common.model.Message; +import org.apache.rocketmq.mqtt.common.model.MqttTopic; +import org.apache.rocketmq.mqtt.common.model.StoreResult; +import org.apache.rocketmq.mqtt.common.util.MessageUtil; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; +import org.apache.rocketmq.mqtt.ds.meta.FirstTopicManager; +import org.apache.rocketmq.mqtt.ds.meta.WildcardManager; +import org.apache.rocketmq.mqtt.ds.upstream.coap.CoapUpstreamProcessor; +import org.apache.rocketmq.mqtt.ds.upstream.mqtt.processor.PublishProcessor; +import org.apache.rocketmq.mqtt.exporter.collector.MqttMetricsCollector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.nio.charset.StandardCharsets; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +@Component +public class CoapPublishProcessor implements CoapUpstreamProcessor { + private static Logger logger = LoggerFactory.getLogger(PublishProcessor.class); + + @Resource + private LmqQueueStore lmqQueueStore; + + @Resource + private WildcardManager wildcardManager; + + @Resource + private FirstTopicManager firstTopicManager; + + @Resource + RetainedPersistManager retainedPersistManager; + + @Override + public CompletableFuture process(CoapRequestMessage msg) { + CompletableFuture r = put(msg); + return r.thenCompose(storeResult -> HookResult.newHookResult(HookResult.SUCCESS, null, + JSON.toJSONBytes(storeResult))); + } + + public CompletableFuture put(CoapRequestMessage coapMessage) { + // todo: process topic alias + boolean isEmpty = false; + + // Deal empty payload. + if (coapMessage.getPayload() == null || coapMessage.getPayload().length == 0) { + coapMessage.setPayload(MessageUtil.EMPTYSTRING.getBytes(StandardCharsets.UTF_8)); + isEmpty = true; + } + + String originTopic = coapMessage.getTopic(); + String pubTopic = TopicUtils.normalizeTopic(originTopic); + MqttTopic mqttTopic = TopicUtils.decode(pubTopic); + firstTopicManager.checkFirstTopicIfCreated(mqttTopic.getFirstTopic()); // Check if the firstTopic is existed + Set queueNames = wildcardManager.matchQueueSetByMsgTopic(pubTopic, null); // Find queues by topic + + String msgId = MessageClientIDSetter.createUniqID(); + long bornTime = System.currentTimeMillis(); + + if (coapMessage.isReatin()) { + CoapRequestMessage retainedCoapMessage = coapMessage.copy(); + // Change the retained flag of message that will send MQ is 0. + retainedCoapMessage.setReatin(false); + // Store retained message. + Message metaMessage = MessageUtil.toMessage(retainedCoapMessage); + metaMessage.setMsgId(msgId); + metaMessage.setBornTimestamp(bornTime); + metaMessage.setEmpty(isEmpty); + CompletableFuture storeRetainedFuture = retainedPersistManager.storeRetainedMessage(TopicUtils.normalizeTopic(metaMessage.getOriginTopic()), metaMessage); + storeRetainedFuture.whenComplete((res, throwable) -> { + if (throwable != null) { + logger.error("Store topic:{} retained message error.{}", metaMessage.getOriginTopic(), throwable); + } + }); + } + + Message message = MessageUtil.toMessage(coapMessage); + message.setMsgId(msgId); + message.setBornTimestamp(bornTime); + message.setEmpty(isEmpty); + + collectWriteBytesAndTps(message.getFirstTopic(), message.getPayload().length); + + return lmqQueueStore.putMessage(queueNames, message); + + } + + private void collectWriteBytesAndTps(String topic, int length) { + try { + MqttMetricsCollector.collectReadWriteMatchActionBytes(length, topic, "put"); + MqttMetricsCollector.collectPutRequestTps(1, topic); + } catch (Throwable e) { + logger.error("Collect prometheus error", e); + } + } +} diff --git a/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapSubscribeProcessor.java b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapSubscribeProcessor.java new file mode 100644 index 000000000..88604ad5c --- /dev/null +++ b/mqtt-ds/src/main/java/org/apache/rocketmq/mqtt/ds/upstream/coap/processor/CoapSubscribeProcessor.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.ds.upstream.coap.processor; + +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.common.model.CoapRequestMessage; +import org.apache.rocketmq.mqtt.common.model.MqttTopic; +import org.apache.rocketmq.mqtt.common.util.TopicUtils; +import org.apache.rocketmq.mqtt.ds.meta.FirstTopicManager; +import org.apache.rocketmq.mqtt.ds.upstream.coap.CoapUpstreamProcessor; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.concurrent.CompletableFuture; + +@Component +public class CoapSubscribeProcessor implements CoapUpstreamProcessor { + + @Resource + private FirstTopicManager firstTopicManager; + + @Override + public CompletableFuture process(CoapRequestMessage coapMessage) { + String pubTopic = TopicUtils.normalizeTopic(coapMessage.getTopic()); + MqttTopic mqttTopic = TopicUtils.decode(pubTopic); + firstTopicManager.checkFirstTopicIfCreated(mqttTopic.getFirstTopic()); + return HookResult.newHookResult(HookResult.SUCCESS, null, null); + } +} diff --git a/mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/auth/TestCoapAuthManager.java b/mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/auth/TestCoapAuthManager.java new file mode 100644 index 000000000..c2899568b --- /dev/null +++ b/mqtt-ds/src/test/java/org/apache/rocketmq/mqtt/ds/test/auth/TestCoapAuthManager.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.mqtt.ds.test.auth; + +import org.apache.commons.lang.reflect.FieldUtils; +import org.apache.rocketmq.mqtt.common.hook.HookResult; +import org.apache.rocketmq.mqtt.ds.auth.CoapAuthManager; +import org.apache.rocketmq.mqtt.ds.config.ServiceConf; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; +import static org.junit.Assert.assertFalse; + +@RunWith(MockitoJUnitRunner.class) +public class TestCoapAuthManager { + private static final String username = "user111"; + private static final String secretKey = "password111"; + + private CoapAuthManager coapAuthManager; + + @Mock + private ServiceConf serviceConf; + + @Before + public void setUp() throws Exception { + coapAuthManager = new CoapAuthManager(); + FieldUtils.writeDeclaredField(coapAuthManager, "serviceConf", serviceConf, true); + + when(serviceConf.getUsername()).thenReturn(username); + when(serviceConf.getSecretKey()).thenReturn(secretKey); + + coapAuthManager.init(); + } + + @Test + public void doAuthWrongUsername() throws ExecutionException, InterruptedException { + String wrongUsername = "user222"; + CompletableFuture future = coapAuthManager.doAuth(wrongUsername, secretKey); + assertFalse(future.get().isSuccess()); + } + + @Test + public void doAuthWrongPassword() throws ExecutionException, InterruptedException { + String wrongPassword = "password222"; + CompletableFuture future = coapAuthManager.doAuth(username, wrongPassword); + assertFalse(future.get().isSuccess()); + } + + @Test + public void doAuthSuccess() throws ExecutionException, InterruptedException { + CompletableFuture future = coapAuthManager.doAuth(username, secretKey); + assertTrue(future.get().isSuccess()); + } + +}