Motivation: The MQTT decoder should raise an exception trying to build a CONNECT packet where password field is set but not the username one (as by MQTT 3.1/3.1.1 spec). Modification: Throw exception if password field is set but not the username Result: Fixes [#7205].
398 lines
15 KiB
Java
398 lines
15 KiB
Java
/*
|
|
* Copyright 2014 The Netty Project
|
|
*
|
|
* The Netty Project 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 io.netty.handler.codec.mqtt;
|
|
|
|
import io.netty.buffer.ByteBuf;
|
|
import io.netty.buffer.ByteBufAllocator;
|
|
import io.netty.channel.ChannelHandler;
|
|
import io.netty.channel.ChannelHandlerContext;
|
|
import io.netty.handler.codec.DecoderException;
|
|
import io.netty.handler.codec.MessageToMessageEncoder;
|
|
import io.netty.util.CharsetUtil;
|
|
import io.netty.util.internal.EmptyArrays;
|
|
|
|
import java.util.List;
|
|
|
|
import static io.netty.handler.codec.mqtt.MqttCodecUtil.*;
|
|
|
|
/**
|
|
* Encodes Mqtt messages into bytes following the protocol specification v3.1
|
|
* as described here <a href="http://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html">MQTTV3.1</a>
|
|
*/
|
|
@ChannelHandler.Sharable
|
|
public final class MqttEncoder extends MessageToMessageEncoder<MqttMessage> {
|
|
|
|
public static final MqttEncoder INSTANCE = new MqttEncoder();
|
|
|
|
private MqttEncoder() { }
|
|
|
|
@Override
|
|
protected void encode(ChannelHandlerContext ctx, MqttMessage msg, List<Object> out) throws Exception {
|
|
out.add(doEncode(ctx.alloc(), msg));
|
|
}
|
|
|
|
/**
|
|
* This is the main encoding method.
|
|
* It's only visible for testing.
|
|
*
|
|
* @param byteBufAllocator Allocates ByteBuf
|
|
* @param message MQTT message to encode
|
|
* @return ByteBuf with encoded bytes
|
|
*/
|
|
static ByteBuf doEncode(ByteBufAllocator byteBufAllocator, MqttMessage message) {
|
|
|
|
switch (message.fixedHeader().messageType()) {
|
|
case CONNECT:
|
|
return encodeConnectMessage(byteBufAllocator, (MqttConnectMessage) message);
|
|
|
|
case CONNACK:
|
|
return encodeConnAckMessage(byteBufAllocator, (MqttConnAckMessage) message);
|
|
|
|
case PUBLISH:
|
|
return encodePublishMessage(byteBufAllocator, (MqttPublishMessage) message);
|
|
|
|
case SUBSCRIBE:
|
|
return encodeSubscribeMessage(byteBufAllocator, (MqttSubscribeMessage) message);
|
|
|
|
case UNSUBSCRIBE:
|
|
return encodeUnsubscribeMessage(byteBufAllocator, (MqttUnsubscribeMessage) message);
|
|
|
|
case SUBACK:
|
|
return encodeSubAckMessage(byteBufAllocator, (MqttSubAckMessage) message);
|
|
|
|
case UNSUBACK:
|
|
case PUBACK:
|
|
case PUBREC:
|
|
case PUBREL:
|
|
case PUBCOMP:
|
|
return encodeMessageWithOnlySingleByteFixedHeaderAndMessageId(byteBufAllocator, message);
|
|
|
|
case PINGREQ:
|
|
case PINGRESP:
|
|
case DISCONNECT:
|
|
return encodeMessageWithOnlySingleByteFixedHeader(byteBufAllocator, message);
|
|
|
|
default:
|
|
throw new IllegalArgumentException(
|
|
"Unknown message type: " + message.fixedHeader().messageType().value());
|
|
}
|
|
}
|
|
|
|
private static ByteBuf encodeConnectMessage(
|
|
ByteBufAllocator byteBufAllocator,
|
|
MqttConnectMessage message) {
|
|
int payloadBufferSize = 0;
|
|
|
|
MqttFixedHeader mqttFixedHeader = message.fixedHeader();
|
|
MqttConnectVariableHeader variableHeader = message.variableHeader();
|
|
MqttConnectPayload payload = message.payload();
|
|
MqttVersion mqttVersion = MqttVersion.fromProtocolNameAndLevel(variableHeader.name(),
|
|
(byte) variableHeader.version());
|
|
|
|
// as MQTT 3.1 & 3.1.1 spec, If the User Name Flag is set to 0, the Password Flag MUST be set to 0
|
|
if (!variableHeader.hasUserName() && variableHeader.hasPassword()) {
|
|
throw new DecoderException("Without a username, the password MUST be not set");
|
|
}
|
|
|
|
// Client id
|
|
String clientIdentifier = payload.clientIdentifier();
|
|
if (!isValidClientId(mqttVersion, clientIdentifier)) {
|
|
throw new MqttIdentifierRejectedException("invalid clientIdentifier: " + clientIdentifier);
|
|
}
|
|
byte[] clientIdentifierBytes = encodeStringUtf8(clientIdentifier);
|
|
payloadBufferSize += 2 + clientIdentifierBytes.length;
|
|
|
|
// Will topic and message
|
|
String willTopic = payload.willTopic();
|
|
byte[] willTopicBytes = willTopic != null ? encodeStringUtf8(willTopic) : EmptyArrays.EMPTY_BYTES;
|
|
byte[] willMessage = payload.willMessageInBytes();
|
|
byte[] willMessageBytes = willMessage != null ? willMessage : EmptyArrays.EMPTY_BYTES;
|
|
if (variableHeader.isWillFlag()) {
|
|
payloadBufferSize += 2 + willTopicBytes.length;
|
|
payloadBufferSize += 2 + willMessageBytes.length;
|
|
}
|
|
|
|
String userName = payload.userName();
|
|
byte[] userNameBytes = userName != null ? encodeStringUtf8(userName) : EmptyArrays.EMPTY_BYTES;
|
|
if (variableHeader.hasUserName()) {
|
|
payloadBufferSize += 2 + userNameBytes.length;
|
|
}
|
|
|
|
byte[] password = payload.passwordInBytes();
|
|
byte[] passwordBytes = password != null ? password : EmptyArrays.EMPTY_BYTES;
|
|
if (variableHeader.hasPassword()) {
|
|
payloadBufferSize += 2 + passwordBytes.length;
|
|
}
|
|
|
|
// Fixed header
|
|
byte[] protocolNameBytes = mqttVersion.protocolNameBytes();
|
|
int variableHeaderBufferSize = 2 + protocolNameBytes.length + 4;
|
|
int variablePartSize = variableHeaderBufferSize + payloadBufferSize;
|
|
int fixedHeaderBufferSize = 1 + getVariableLengthInt(variablePartSize);
|
|
ByteBuf buf = byteBufAllocator.buffer(fixedHeaderBufferSize + variablePartSize);
|
|
buf.writeByte(getFixedHeaderByte1(mqttFixedHeader));
|
|
writeVariableLengthInt(buf, variablePartSize);
|
|
|
|
buf.writeShort(protocolNameBytes.length);
|
|
buf.writeBytes(protocolNameBytes);
|
|
|
|
buf.writeByte(variableHeader.version());
|
|
buf.writeByte(getConnVariableHeaderFlag(variableHeader));
|
|
buf.writeShort(variableHeader.keepAliveTimeSeconds());
|
|
|
|
// Payload
|
|
buf.writeShort(clientIdentifierBytes.length);
|
|
buf.writeBytes(clientIdentifierBytes, 0, clientIdentifierBytes.length);
|
|
if (variableHeader.isWillFlag()) {
|
|
buf.writeShort(willTopicBytes.length);
|
|
buf.writeBytes(willTopicBytes, 0, willTopicBytes.length);
|
|
buf.writeShort(willMessageBytes.length);
|
|
buf.writeBytes(willMessageBytes, 0, willMessageBytes.length);
|
|
}
|
|
if (variableHeader.hasUserName()) {
|
|
buf.writeShort(userNameBytes.length);
|
|
buf.writeBytes(userNameBytes, 0, userNameBytes.length);
|
|
}
|
|
if (variableHeader.hasPassword()) {
|
|
buf.writeShort(passwordBytes.length);
|
|
buf.writeBytes(passwordBytes, 0, passwordBytes.length);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
private static int getConnVariableHeaderFlag(MqttConnectVariableHeader variableHeader) {
|
|
int flagByte = 0;
|
|
if (variableHeader.hasUserName()) {
|
|
flagByte |= 0x80;
|
|
}
|
|
if (variableHeader.hasPassword()) {
|
|
flagByte |= 0x40;
|
|
}
|
|
if (variableHeader.isWillRetain()) {
|
|
flagByte |= 0x20;
|
|
}
|
|
flagByte |= (variableHeader.willQos() & 0x03) << 3;
|
|
if (variableHeader.isWillFlag()) {
|
|
flagByte |= 0x04;
|
|
}
|
|
if (variableHeader.isCleanSession()) {
|
|
flagByte |= 0x02;
|
|
}
|
|
return flagByte;
|
|
}
|
|
|
|
private static ByteBuf encodeConnAckMessage(
|
|
ByteBufAllocator byteBufAllocator,
|
|
MqttConnAckMessage message) {
|
|
ByteBuf buf = byteBufAllocator.buffer(4);
|
|
buf.writeByte(getFixedHeaderByte1(message.fixedHeader()));
|
|
buf.writeByte(2);
|
|
buf.writeByte(message.variableHeader().isSessionPresent() ? 0x01 : 0x00);
|
|
buf.writeByte(message.variableHeader().connectReturnCode().byteValue());
|
|
|
|
return buf;
|
|
}
|
|
|
|
private static ByteBuf encodeSubscribeMessage(
|
|
ByteBufAllocator byteBufAllocator,
|
|
MqttSubscribeMessage message) {
|
|
int variableHeaderBufferSize = 2;
|
|
int payloadBufferSize = 0;
|
|
|
|
MqttFixedHeader mqttFixedHeader = message.fixedHeader();
|
|
MqttMessageIdVariableHeader variableHeader = message.variableHeader();
|
|
MqttSubscribePayload payload = message.payload();
|
|
|
|
for (MqttTopicSubscription topic : payload.topicSubscriptions()) {
|
|
String topicName = topic.topicName();
|
|
byte[] topicNameBytes = encodeStringUtf8(topicName);
|
|
payloadBufferSize += 2 + topicNameBytes.length;
|
|
payloadBufferSize += 1;
|
|
}
|
|
|
|
int variablePartSize = variableHeaderBufferSize + payloadBufferSize;
|
|
int fixedHeaderBufferSize = 1 + getVariableLengthInt(variablePartSize);
|
|
|
|
ByteBuf buf = byteBufAllocator.buffer(fixedHeaderBufferSize + variablePartSize);
|
|
buf.writeByte(getFixedHeaderByte1(mqttFixedHeader));
|
|
writeVariableLengthInt(buf, variablePartSize);
|
|
|
|
// Variable Header
|
|
int messageId = variableHeader.messageId();
|
|
buf.writeShort(messageId);
|
|
|
|
// Payload
|
|
for (MqttTopicSubscription topic : payload.topicSubscriptions()) {
|
|
String topicName = topic.topicName();
|
|
byte[] topicNameBytes = encodeStringUtf8(topicName);
|
|
buf.writeShort(topicNameBytes.length);
|
|
buf.writeBytes(topicNameBytes, 0, topicNameBytes.length);
|
|
buf.writeByte(topic.qualityOfService().value());
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
|
|
private static ByteBuf encodeUnsubscribeMessage(
|
|
ByteBufAllocator byteBufAllocator,
|
|
MqttUnsubscribeMessage message) {
|
|
int variableHeaderBufferSize = 2;
|
|
int payloadBufferSize = 0;
|
|
|
|
MqttFixedHeader mqttFixedHeader = message.fixedHeader();
|
|
MqttMessageIdVariableHeader variableHeader = message.variableHeader();
|
|
MqttUnsubscribePayload payload = message.payload();
|
|
|
|
for (String topicName : payload.topics()) {
|
|
byte[] topicNameBytes = encodeStringUtf8(topicName);
|
|
payloadBufferSize += 2 + topicNameBytes.length;
|
|
}
|
|
|
|
int variablePartSize = variableHeaderBufferSize + payloadBufferSize;
|
|
int fixedHeaderBufferSize = 1 + getVariableLengthInt(variablePartSize);
|
|
|
|
ByteBuf buf = byteBufAllocator.buffer(fixedHeaderBufferSize + variablePartSize);
|
|
buf.writeByte(getFixedHeaderByte1(mqttFixedHeader));
|
|
writeVariableLengthInt(buf, variablePartSize);
|
|
|
|
// Variable Header
|
|
int messageId = variableHeader.messageId();
|
|
buf.writeShort(messageId);
|
|
|
|
// Payload
|
|
for (String topicName : payload.topics()) {
|
|
byte[] topicNameBytes = encodeStringUtf8(topicName);
|
|
buf.writeShort(topicNameBytes.length);
|
|
buf.writeBytes(topicNameBytes, 0, topicNameBytes.length);
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
|
|
private static ByteBuf encodeSubAckMessage(
|
|
ByteBufAllocator byteBufAllocator,
|
|
MqttSubAckMessage message) {
|
|
int variableHeaderBufferSize = 2;
|
|
int payloadBufferSize = message.payload().grantedQoSLevels().size();
|
|
int variablePartSize = variableHeaderBufferSize + payloadBufferSize;
|
|
int fixedHeaderBufferSize = 1 + getVariableLengthInt(variablePartSize);
|
|
ByteBuf buf = byteBufAllocator.buffer(fixedHeaderBufferSize + variablePartSize);
|
|
buf.writeByte(getFixedHeaderByte1(message.fixedHeader()));
|
|
writeVariableLengthInt(buf, variablePartSize);
|
|
buf.writeShort(message.variableHeader().messageId());
|
|
for (int qos : message.payload().grantedQoSLevels()) {
|
|
buf.writeByte(qos);
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
|
|
private static ByteBuf encodePublishMessage(
|
|
ByteBufAllocator byteBufAllocator,
|
|
MqttPublishMessage message) {
|
|
MqttFixedHeader mqttFixedHeader = message.fixedHeader();
|
|
MqttPublishVariableHeader variableHeader = message.variableHeader();
|
|
ByteBuf payload = message.payload().duplicate();
|
|
|
|
String topicName = variableHeader.topicName();
|
|
byte[] topicNameBytes = encodeStringUtf8(topicName);
|
|
|
|
int variableHeaderBufferSize = 2 + topicNameBytes.length +
|
|
(mqttFixedHeader.qosLevel().value() > 0 ? 2 : 0);
|
|
int payloadBufferSize = payload.readableBytes();
|
|
int variablePartSize = variableHeaderBufferSize + payloadBufferSize;
|
|
int fixedHeaderBufferSize = 1 + getVariableLengthInt(variablePartSize);
|
|
|
|
ByteBuf buf = byteBufAllocator.buffer(fixedHeaderBufferSize + variablePartSize);
|
|
buf.writeByte(getFixedHeaderByte1(mqttFixedHeader));
|
|
writeVariableLengthInt(buf, variablePartSize);
|
|
buf.writeShort(topicNameBytes.length);
|
|
buf.writeBytes(topicNameBytes);
|
|
if (mqttFixedHeader.qosLevel().value() > 0) {
|
|
buf.writeShort(variableHeader.messageId());
|
|
}
|
|
buf.writeBytes(payload);
|
|
|
|
return buf;
|
|
}
|
|
|
|
private static ByteBuf encodeMessageWithOnlySingleByteFixedHeaderAndMessageId(
|
|
ByteBufAllocator byteBufAllocator,
|
|
MqttMessage message) {
|
|
MqttFixedHeader mqttFixedHeader = message.fixedHeader();
|
|
MqttMessageIdVariableHeader variableHeader = (MqttMessageIdVariableHeader) message.variableHeader();
|
|
int msgId = variableHeader.messageId();
|
|
|
|
int variableHeaderBufferSize = 2; // variable part only has a message id
|
|
int fixedHeaderBufferSize = 1 + getVariableLengthInt(variableHeaderBufferSize);
|
|
ByteBuf buf = byteBufAllocator.buffer(fixedHeaderBufferSize + variableHeaderBufferSize);
|
|
buf.writeByte(getFixedHeaderByte1(mqttFixedHeader));
|
|
writeVariableLengthInt(buf, variableHeaderBufferSize);
|
|
buf.writeShort(msgId);
|
|
|
|
return buf;
|
|
}
|
|
|
|
private static ByteBuf encodeMessageWithOnlySingleByteFixedHeader(
|
|
ByteBufAllocator byteBufAllocator,
|
|
MqttMessage message) {
|
|
MqttFixedHeader mqttFixedHeader = message.fixedHeader();
|
|
ByteBuf buf = byteBufAllocator.buffer(2);
|
|
buf.writeByte(getFixedHeaderByte1(mqttFixedHeader));
|
|
buf.writeByte(0);
|
|
|
|
return buf;
|
|
}
|
|
|
|
private static int getFixedHeaderByte1(MqttFixedHeader header) {
|
|
int ret = 0;
|
|
ret |= header.messageType().value() << 4;
|
|
if (header.isDup()) {
|
|
ret |= 0x08;
|
|
}
|
|
ret |= header.qosLevel().value() << 1;
|
|
if (header.isRetain()) {
|
|
ret |= 0x01;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
private static void writeVariableLengthInt(ByteBuf buf, int num) {
|
|
do {
|
|
int digit = num % 128;
|
|
num /= 128;
|
|
if (num > 0) {
|
|
digit |= 0x80;
|
|
}
|
|
buf.writeByte(digit);
|
|
} while (num > 0);
|
|
}
|
|
|
|
private static int getVariableLengthInt(int num) {
|
|
int count = 0;
|
|
do {
|
|
num /= 128;
|
|
count++;
|
|
} while (num > 0);
|
|
return count;
|
|
}
|
|
|
|
private static byte[] encodeStringUtf8(String s) {
|
|
return s.getBytes(CharsetUtil.UTF_8);
|
|
}
|
|
}
|