From cf3c982e1d18e884fdf68c526ef266d70748ff59 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Tue, 3 Mar 2026 13:21:45 -0800 Subject: [PATCH 1/2] Packet validation support --- lib/browser/mqtt_internal/validate.spec.ts | 1444 ++++++++++++++++++++ lib/browser/mqtt_internal/validate.ts | 803 +++++++++++ 2 files changed, 2247 insertions(+) create mode 100644 lib/browser/mqtt_internal/validate.spec.ts create mode 100644 lib/browser/mqtt_internal/validate.ts diff --git a/lib/browser/mqtt_internal/validate.spec.ts b/lib/browser/mqtt_internal/validate.spec.ts new file mode 100644 index 00000000..3e457eb0 --- /dev/null +++ b/lib/browser/mqtt_internal/validate.spec.ts @@ -0,0 +1,1444 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +import * as model from "./model"; +import * as mqtt5_common from "../../common/mqtt5"; +import * as mqtt5_packet from "../../common/mqtt5_packet"; +import * as validate from "./validate"; + +function doBinaryUserPropertyNameTooLongTest(packet: model.IPacketBinary) { + let settings = createStandardNegotiatedSettings(); + + // @ts-ignore + packet.userProperties[0].name = new Uint8Array(65536); + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +} + +function doBinaryUserPropertyValueTooLongTest(packet: model.IPacketBinary) { + let settings = createStandardNegotiatedSettings(); + + // @ts-ignore + packet.userProperties[1].value = new Uint8Array(65536); + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +} + +function doBinaryZeroPacketIdTest(packet: model.IPacketBinary) { + let settings = createStandardNegotiatedSettings(); + + // @ts-ignore + packet.packetId = 0; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("not a valid packetId"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a valid packetId"); +} + +function doBinaryUndefinedPacketIdTest(packet: model.IPacketBinary) { + let settings = createStandardNegotiatedSettings(); + + // @ts-ignore + delete packet.packetId; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("must be defined"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("must be defined"); +} + +function doBadUserPropertiesTypeTest(packet: mqtt5_packet.IPacket) { + // @ts-ignore + packet.userProperties = true; + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("is not an array"); +} + +function doBadUserPropertiesUndefinedNameTest(packet: mqtt5_packet.IPacket) { + // @ts-ignore + delete packet.userProperties[0].name; + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid string"); +} + +function doBadUserPropertiesBadNameTypeTest(packet: mqtt5_packet.IPacket) { + // @ts-ignore + packet.userProperties[0].name = false; + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid string"); +} + +function doBadUserPropertiesUndefinedValueTest(packet: mqtt5_packet.IPacket) { + // @ts-ignore + delete packet.userProperties[1].value; + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid string"); +} + +function doBadUserPropertiesBadValueTypeTest(packet: mqtt5_packet.IPacket) { + // @ts-ignore + packet.userProperties[1].value = 21; + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid string"); +} + +// Publish Validation + +function createExternalPublishPacketMaximal() : mqtt5_packet.PublishPacket { + return { + type: mqtt5_packet.PacketType.Publish, + topicName: "my/topic", + qos: mqtt5_packet.QoS.AtLeastOnce, + payload: new Uint8Array(0), + retain: true, + payloadFormat: mqtt5_packet.PayloadFormatIndicator.Utf8, + messageExpiryIntervalSeconds: 123, + topicAlias: 123, + responseTopic: "response/Topic", + correlationData: new Uint8Array(0), + contentType: "rest-json", + userProperties: [ + {name: "name", value: "value"}, + {name: "hello", value: "world"} + ] + }; +} + +// user-submitted publishes + +test('External publish packet validation - isValid', async () => { + validate.validateInitialOutboundPacket(createExternalPublishPacketMaximal(), model.ProtocolMode.Mqtt311); + validate.validateInitialOutboundPacket(createExternalPublishPacketMaximal(), model.ProtocolMode.Mqtt5); +}); + +test('External publish packet validation - undefined topic', async () => { + let packet = createExternalPublishPacketMaximal(); + // @ts-ignore + delete packet.topicName; + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid string"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid string"); +}); + +test('External publish packet validation - bad topic type', async () => { + let packet = createExternalPublishPacketMaximal(); + // @ts-ignore + packet.topicName = 6; + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid string"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid string"); +}); + +test('External publish packet validation - bad topic value', async () => { + let packet = createExternalPublishPacketMaximal(); + packet.topicName = "#/#"; + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid topic"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid topic"); +}); + +test('External publish packet validation - undefined qos', async () => { + let packet = createExternalPublishPacketMaximal(); + // @ts-ignore + delete packet.qos; + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid u8"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u8"); +}); + +test('External publish packet validation - bad qos type', async () => { + let packet = createExternalPublishPacketMaximal(); + // @ts-ignore + packet.qos = "hi"; + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid u8"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u8"); +}); + +test('External publish packet validation - bad qos value', async () => { + let packet = createExternalPublishPacketMaximal(); + // @ts-ignore + packet.qos = 3; + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid QualityOfService"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid QualityOfService"); +}); + +test('External publish packet validation - bad payload type', async () => { + let packet = createExternalPublishPacketMaximal(); + // @ts-ignore + packet.payload = [3, "derp"]; + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("Invalid payload value"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("Invalid payload value"); +}); + +test('External publish packet validation - bad payload format type', async () => { + let packet = createExternalPublishPacketMaximal(); + // @ts-ignore + packet.payloadFormat = "hi"; + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u8"); +}); + +test('External publish packet validation - bad payload format value', async () => { + let packet = createExternalPublishPacketMaximal(); + // @ts-ignore + packet.payloadFormat = 2; + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid PayloadFormatIndicator"); +}); + +test('External publish packet validation - bad message expiry interval type', async () => { + let packet = createExternalPublishPacketMaximal(); + // @ts-ignore + packet.messageExpiryIntervalSeconds = "hi"; + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u32"); +}); + +test('External publish packet validation - bad message expiry interval value', async () => { + let packet = createExternalPublishPacketMaximal(); + packet.messageExpiryIntervalSeconds = -5; + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u32"); +}); + +test('External publish packet validation - bad topic alias type', async () => { + let packet = createExternalPublishPacketMaximal(); + // @ts-ignore + packet.topicAlias = "hi"; + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u16"); +}); + +test('External publish packet validation - bad topic alias value', async () => { + let packet = createExternalPublishPacketMaximal(); + packet.topicAlias = 0; + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("cannot be 0"); +}); + + +test('External publish packet validation - bad response topic type', async () => { + let packet = createExternalPublishPacketMaximal(); + // @ts-ignore + packet.responseTopic = 3; + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid string"); +}); + +test('External publish packet validation - bad response topic value', async () => { + let packet = createExternalPublishPacketMaximal(); + packet.responseTopic = "#/+"; + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid topic"); +}); + +test('External publish packet validation - bad correlation data type', async () => { + let packet = createExternalPublishPacketMaximal(); + // @ts-ignore + packet.correlationData = [3, "derp"]; + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not valid binary data"); +}); + +test('External publish packet validation - bad content type type', async () => { + let packet = createExternalPublishPacketMaximal(); + // @ts-ignore + packet.contentType = false; + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid string"); +}); + +test('External publish packet validation - bad user properties type', async () => { + doBadUserPropertiesTypeTest(createExternalPublishPacketMaximal()); +}); + +test('External publish packet validation - user properties name bad type', async () => { + doBadUserPropertiesBadNameTypeTest(createExternalPublishPacketMaximal()); +}); + +test('External publish packet validation - user properties name undefined type', async () => { + doBadUserPropertiesUndefinedNameTest(createExternalPublishPacketMaximal()); +}); + +test('External publish packet validation - bad user properties value type', async () => { + doBadUserPropertiesBadValueTypeTest(createExternalPublishPacketMaximal()); +}); + +test('External publish packet validation - user properties value undefined type', async () => { + doBadUserPropertiesUndefinedValueTest(createExternalPublishPacketMaximal()); +}); + +// binary publish + +function createBinaryPublishPacketMaximal() : model.PublishPacketBinary { + let packet = createExternalPublishPacketMaximal(); + let binaryPacket = model.convertInternalPacketToBinary(packet) as model.PublishPacketBinary; + + binaryPacket.duplicate = 0; + binaryPacket.packetId = 7; + + return binaryPacket; +} + +function createStandardNegotiatedSettings() : mqtt5_common.NegotiatedSettings { + return { + maximumQos: mqtt5_packet.QoS.AtLeastOnce, + sessionExpiryInterval: 1200, + receiveMaximumFromServer: 100, + maximumPacketSizeToServer: 128 * 1024, + topicAliasMaximumToServer: 200, + topicAliasMaximumToClient: 20, + serverKeepAlive: 3600, + retainAvailable: true, + wildcardSubscriptionsAvailable: true, + subscriptionIdentifiersAvailable: true, + sharedSubscriptionsAvailable: true, + rejoinedSession: false, + clientId: "Spongebob" + }; +} + +test('Binary publish packet validation - success', async () => { + let packet = createBinaryPublishPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); +}); + +test('Binary publish packet validation - packet too long', async () => { + let packet = createBinaryPublishPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + packet.payload = new Uint8Array(128 * 1024 + 1); + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("exceeds established maximum packet size"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("exceeds established maximum packet size"); +}); + +test('Binary publish packet validation - qos 0 packet id', async () => { + let packet = createBinaryPublishPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + packet.qos = 0; + packet.packetId = 5; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("packetId must not be set"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("packetId must not be set"); +}); + +test('Binary publish packet validation - qos 1 no packet id', async () => { + let packet = createBinaryPublishPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + packet.qos = 1; + delete packet.packetId; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("must be defined"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("must be defined"); +}); + +test('Binary publish packet validation - qos 1 zero-valued packet id', async () => { + let packet = createBinaryPublishPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + packet.qos = 1; + packet.packetId = 0; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("not a valid packetId"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a valid packetId"); +}); + +test('Binary publish packet validation - qos 1 too-large packet id', async () => { + let packet = createBinaryPublishPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + packet.qos = 65536; + packet.packetId = 0; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("not a valid packetId"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a valid packetId"); +}); + +test('Binary publish packet validation - retain not available', async () => { + let packet = createBinaryPublishPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + packet.retain = 1; + settings.retainAvailable = false; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("does not support retained messages"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("does not support retained messages"); +}); + +test('Binary publish packet validation - qos exceeds maximum', async () => { + let packet = createBinaryPublishPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + packet.qos = 2; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("greater than the maximum QoS"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("greater than the maximum QoS"); +}); + +test('Binary publish packet validation - topic too long', async () => { + let packet = createBinaryPublishPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + packet.topicName = new Uint8Array(65536); + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("not a 16-bit length buffer"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +}); + +test('Binary publish packet validation - subscription identifiers set', async () => { + let packet = createBinaryPublishPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + packet.subscriptionIdentifiers = [1]; + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("may not be set on outbound publish packets"); +}); + +test('Binary publish packet validation - topic alias zero', async () => { + let packet = createBinaryPublishPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + packet.topicAlias = 0; + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("cannot be zero"); +}); + +test('Binary publish packet validation - topic alias too big', async () => { + let packet = createBinaryPublishPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + packet.topicAlias = 256; + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("greater than the maximum topic alias"); +}); + +test('Binary publish packet validation - response topic too long', async () => { + let packet = createBinaryPublishPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + packet.responseTopic = new Uint8Array(65536); + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +}); + +test('Binary publish packet validation - correlation data too long', async () => { + let packet = createBinaryPublishPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + packet.correlationData = new Uint8Array(65536); + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +}); + +test('Binary publish packet validation - content type too long', async () => { + let packet = createBinaryPublishPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + packet.contentType = new Uint8Array(65536); + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +}); + +test('Binary publish packet validation - user property name too long', async () => { + doBinaryUserPropertyNameTooLongTest(createBinaryPublishPacketMaximal()); +}); + +test('Binary publish packet validation - user property value too long', async () => { + doBinaryUserPropertyValueTooLongTest(createBinaryPublishPacketMaximal()); +}); + +// inbound publish + +function createInternalPublishPacketMaximal() : model.PublishPacketInternal { + return { + type: mqtt5_packet.PacketType.Publish, + topicName: "my/topic", + qos: mqtt5_packet.QoS.AtLeastOnce, + duplicate: false, + packetId: 5, + payload: new Uint8Array(0), + retain: true, + payloadFormat: mqtt5_packet.PayloadFormatIndicator.Utf8, + messageExpiryIntervalSeconds: 123, + topicAlias: 123, + responseTopic: "response/Topic", + correlationData: new Uint8Array(0), + contentType: "rest-json", + userProperties: [ + {name: "name", value: "value"}, + {name: "hello", value: "world"} + ] + }; +} + +test('Inbound publish packet validation - success', async () => { + let packet = createInternalPublishPacketMaximal(); + + validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt311); + validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt5); +}); + +test('Inbound publish packet validation - invalid qos', async () => { + let packet = createInternalPublishPacketMaximal(); + // @ts-ignore + packet.qos = 255; + + expect(() => { validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid QualityOfService"); + expect(() => { validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid QualityOfService"); +}); + +test('Inbound publish packet validation - qos 1 with zero packet id', async () => { + let packet = createInternalPublishPacketMaximal(); + // @ts-ignore + packet.packetId = 0; + + expect(() => { validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid packetId"); + expect(() => { validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid packetId"); +}); + +test('Inbound publish packet validation - unresolved topic alias', async () => { + let packet = createInternalPublishPacketMaximal(); + packet.topicName = ""; + + expect(() => { validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("topicName is empty"); + expect(() => { validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("topicName is empty"); +}); + +// Puback Validation + +// Binary pubacks +function createInternalPubackPacketMaximal() : model.PubackPacketInternal { + return { + type: mqtt5_packet.PacketType.Puback, + packetId: 5, + reasonCode: mqtt5_packet.PubackReasonCode.Success, + reasonString: "well formed", + userProperties: [ + {name: "name", value: "value"}, + {name: "hello", value: "world"} + ] + }; +} + +function createBinaryPubackPacketMaximal() : model.PubackPacketBinary { + let packet = createInternalPubackPacketMaximal(); + let binaryPacket = model.convertInternalPacketToBinary(packet) as model.PubackPacketBinary; + + return binaryPacket; +} + +test('Binary puback packet validation - success', async () => { + let packet = createBinaryPubackPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); +}); + +test('Binary puback packet validation - packet too long', async () => { + let packet = createBinaryPubackPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + settings.maximumPacketSizeToServer = 1; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("exceeds established maximum packet size"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("exceeds established maximum packet size"); +}); + +test('Binary puback packet validation - zero packet id', async () => { + doBinaryZeroPacketIdTest(createBinaryPubackPacketMaximal()); +}); + +test('Binary puback packet validation - undefined packet id', async () => { + doBinaryUndefinedPacketIdTest(createBinaryPubackPacketMaximal()); +}); + +test('Binary puback packet validation - reason string too long', async () => { + let packet = createBinaryPubackPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + packet.reasonString = new Uint8Array(65536); + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +}); + +test('Binary puback packet validation - user property name too long', async () => { + let packet = createBinaryPubackPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + // @ts-ignore + packet.userProperties[0].name = new Uint8Array(65536); + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +}); + +test('Binary puback packet validation - user property value too long', async () => { + let packet = createBinaryPubackPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + // @ts-ignore + packet.userProperties[1].value = new Uint8Array(65536); + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +}); + +// Inbound pubacks + +test('Inbound puback packet validation - success', async () => { + let packet = createInternalPubackPacketMaximal(); + + validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt311); + validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt5); +}); + +test('Inbound puback packet validation - bad packet id', async () => { + let packet = createInternalPubackPacketMaximal(); + packet.packetId = 0; + + expect(() => { validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid packetId"); + expect(() => { validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid packetId"); +}); + +test('Inbound puback packet validation - bad reason code', async () => { + let packet = createInternalPubackPacketMaximal(); + packet.reasonCode = 255; + + expect(() => { validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid MQTT5 PubackReasonCode"); +}); + +// Subscribe Validation + +function createExternalSubscribePacketMaximal() : mqtt5_packet.SubscribePacket { + return { + type: mqtt5_packet.PacketType.Subscribe, + subscriptions: [ + { + topicFilter: "hello/there", + qos: mqtt5_packet.QoS.ExactlyOnce, + }, + { + topicFilter: "device/a", + qos: mqtt5_packet.QoS.AtMostOnce, + noLocal: false, + retainAsPublished: true, + retainHandlingType: mqtt5_packet.RetainHandlingType.SendOnSubscribeIfNew + } + ], + subscriptionIdentifier: 37, + userProperties: [ + { name: "key", value: "uffdah" }, + { name: "hello", value: "world" } + ] + }; +} + +// User-submitted subscribes + +test('External subscribe packet validation - success', async () => { + let packet = createExternalSubscribePacketMaximal(); + + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); +}); + +test('External subscribe packet validation - undefined subscriptions', async () => { + let packet = createExternalSubscribePacketMaximal(); + // @ts-ignore + delete packet.subscriptions; + + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("must be an array"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("must be an array"); +}); + +test('External subscribe packet validation - subscriptions bad type', async () => { + let packet = createExternalSubscribePacketMaximal(); + // @ts-ignore + packet.subscriptions = "oops"; + + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("must be an array"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("must be an array"); +}); + +test('External subscribe packet validation - empty subscriptions', async () => { + let packet = createExternalSubscribePacketMaximal(); + packet.subscriptions = []; + + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("cannot be empty"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("cannot be empty"); +}); + +test('External subscribe packet validation - undefined topic filter', async () => { + let packet = createExternalSubscribePacketMaximal(); + // @ts-ignore + delete packet.subscriptions[0].topicFilter; + + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid string"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid string"); +}); + +test('External subscribe packet validation - topic filter bad type', async () => { + let packet = createExternalSubscribePacketMaximal(); + // @ts-ignore + packet.subscriptions[0].topicFilter = 0; + + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid string"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid string"); +}); + +test('External subscribe packet validation - topic filter invalid', async () => { + let packet = createExternalSubscribePacketMaximal(); + packet.subscriptions[0].topicFilter = "###"; + + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid topic filter"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid topic filter"); +}); + +test('External subscribe packet validation - undefined qos', async () => { + let packet = createExternalSubscribePacketMaximal(); + // @ts-ignore + delete packet.subscriptions[0].qos; + + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid u8"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u8"); +}); + +test('External subscribe packet validation - qos bad type', async () => { + let packet = createExternalSubscribePacketMaximal(); + // @ts-ignore + packet.subscriptions[0].qos = "qos"; + + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid u8"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u8"); +}); + +test('External subscribe packet validation - qos invalid', async () => { + let packet = createExternalSubscribePacketMaximal(); + // @ts-ignore + packet.subscriptions[0].qos = 4; + + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid QualityOfService"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid QualityOfService"); +}); + +test('External subscribe packet validation - retain handling type bad type', async () => { + let packet = createExternalSubscribePacketMaximal(); + // @ts-ignore + packet.subscriptions[1].retainHandlingType = "qos"; + + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u8"); +}); + +test('External subscribe packet validation - retain handling type invalid', async () => { + let packet = createExternalSubscribePacketMaximal(); + // @ts-ignore + packet.subscriptions[1].retainHandlingType = 7; + + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid RetainHandlingType"); +}); + +test('External subscribe packet validation - subscription identifier wrong type', async () => { + let packet = createExternalSubscribePacketMaximal(); + // @ts-ignore + packet.subscriptionIdentifier = "uffdah"; + + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("cannot be VLI-encoded"); +}); + +test('External subscribe packet validation - subscription identifier too big', async () => { + let packet = createExternalSubscribePacketMaximal(); + packet.subscriptionIdentifier = 256 * 256 * 256 * 128; + + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("cannot be VLI-encoded"); +}); + +test('External subscribe packet validation - bad user properties type', async () => { + doBadUserPropertiesTypeTest(createExternalSubscribePacketMaximal()); +}); + +test('External subscribe packet validation - user properties name bad type', async () => { + doBadUserPropertiesBadNameTypeTest(createExternalSubscribePacketMaximal()); +}); + +test('External subscribe packet validation - user properties name undefined type', async () => { + doBadUserPropertiesUndefinedNameTest(createExternalSubscribePacketMaximal()); +}); + +test('External subscribe packet validation - bad user properties value type', async () => { + doBadUserPropertiesBadValueTypeTest(createExternalSubscribePacketMaximal()); +}); + +test('External subscribe packet validation - user properties value undefined type', async () => { + doBadUserPropertiesUndefinedValueTest(createExternalSubscribePacketMaximal()); +}); + +// Binary subscribes + +function creatdBinarySubscribePacketMaximal() : model.SubscribePacketBinary { + let packet = createExternalSubscribePacketMaximal(); + let binarySubscribe = model.convertInternalPacketToBinary(packet) as model.SubscribePacketBinary; + + binarySubscribe.packetId = 3; + + return binarySubscribe; +} + +test('Binary subscribe packet validation - success', async () => { + let packet = creatdBinarySubscribePacketMaximal(); + let settings = createStandardNegotiatedSettings(); + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); +}); + +test('Binary subscribe packet validation - packet length too long', async () => { + let packet = creatdBinarySubscribePacketMaximal(); + let settings = createStandardNegotiatedSettings(); + settings.maximumPacketSizeToServer = 15; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("exceeds established maximum packet size"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("exceeds established maximum packet size"); +}); + +test('Binary subscribe packet validation - zero packet id', async () => { + doBinaryZeroPacketIdTest(creatdBinarySubscribePacketMaximal()); +}); + +test('Binary subscribe packet validation - undefined packet id', async () => { + doBinaryUndefinedPacketIdTest(creatdBinarySubscribePacketMaximal()); +}); + +test('Binary subscribe packet validation - shared subs unavailable', async () => { + let packet = creatdBinarySubscribePacketMaximal(); + let settings = createStandardNegotiatedSettings(); + + let encoder = new TextEncoder(); + let sharedTopicFilter = "$share/0/foo/bar"; + packet.subscriptions[0].topicFilterAsString = sharedTopicFilter; + packet.subscriptions[0].topicFilter = encoder.encode(sharedTopicFilter).buffer; + settings.sharedSubscriptionsAvailable = false; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("not supported by the server"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not supported by the server"); +}); + +test('Binary subscribe packet validation - no local and shared', async () => { + let packet = creatdBinarySubscribePacketMaximal(); + let settings = createStandardNegotiatedSettings(); + + let encoder = new TextEncoder(); + let sharedTopicFilter = "$share/0/foo/bar"; + packet.subscriptions[0].topicFilterAsString = sharedTopicFilter; + packet.subscriptions[0].topicFilter = encoder.encode(sharedTopicFilter).buffer; + packet.subscriptions[0].noLocal = 1; + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("may not be set on a shared subscriptions"); +}); + +test('Binary subscribe packet validation - wildcard subs unavailable', async () => { + let packet = creatdBinarySubscribePacketMaximal(); + let settings = createStandardNegotiatedSettings(); + + let encoder = new TextEncoder(); + let wildcardTopicFilter = "a/+/b/#"; + packet.subscriptions[0].topicFilterAsString = wildcardTopicFilter; + packet.subscriptions[0].topicFilter = encoder.encode(wildcardTopicFilter).buffer; + settings.wildcardSubscriptionsAvailable = false; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("not supported by the server"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not supported by the server"); +}); + +test('Binary subscribe packet validation - topic filter too long', async () => { + let packet = creatdBinarySubscribePacketMaximal(); + let settings = createStandardNegotiatedSettings(); + + let encoder = new TextEncoder(); + let newTopicFilter = "a".repeat(65536); + packet.subscriptions[0].topicFilterAsString = newTopicFilter; + packet.subscriptions[0].topicFilter = encoder.encode(newTopicFilter).buffer; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("not a 16-bit length buffer"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +}); + +test('Binary subscribe packet validation - user property name too long', async () => { + doBinaryUserPropertyNameTooLongTest(creatdBinarySubscribePacketMaximal()); +}); + +test('Binary subscribe packet validation - user property value too long', async () => { + doBinaryUserPropertyValueTooLongTest(creatdBinarySubscribePacketMaximal()); +}); + +// Suback Validation + +// Inbound subacks + +function createInternalSubackPacketMaximal() : model.SubackPacketInternal { + return { + type: mqtt5_packet.PacketType.Suback, + packetId: 3, + reasonCodes: [ + mqtt5_packet.SubackReasonCode.GrantedQoS0, + mqtt5_packet.SubackReasonCode.GrantedQoS1, + mqtt5_packet.SubackReasonCode.GrantedQoS2 + ], + userProperties: [ + {name: "name", value: "value"}, + {name: "hello", value: "world"} + ] + }; +} + +test('Inbound suback packet validation - success', async () => { + let packet = createInternalSubackPacketMaximal(); + + validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt311); + validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt5); +}); + +test('Inbound suback packet validation - zero packet id', async () => { + let packet = createInternalSubackPacketMaximal(); + packet.packetId = 0; + + expect(() => { validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid packetId"); + expect(() => { validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid packetId"); +}); + +test('Inbound suback packet validation - bad reason code', async () => { + let packet = createInternalSubackPacketMaximal(); + packet.reasonCodes[0] = 3; + + expect(() => { validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid MQTT311 SubackReasonCode"); + expect(() => { validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid MQTT5 SubackReasonCode"); +}); + +// Unsubscribe Validation + +function createExternalUnsubscribePacketMaximal() : mqtt5_packet.UnsubscribePacket { + return { + type: mqtt5_packet.PacketType.Unsubscribe, + topicFilters: [ + "hello/there", + "device/a" + ], + userProperties: [ + { name: "key", value: "uffdah" }, + { name: "hello", value: "world" } + ] + }; +} + +// User-submitted unsubscribes + +test('External unsubscribe packet validation - success', async () => { + let packet = createExternalUnsubscribePacketMaximal(); + + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); +}); + +test('External unsubscribe packet validation - undefined subscriptions', async () => { + let packet = createExternalUnsubscribePacketMaximal(); + // @ts-ignore + delete packet.topicFilters; + + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("cannot be empty"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("cannot be empty"); +}); + +test('External unsubscribe packet validation - empty subscriptions', async () => { + let packet = createExternalUnsubscribePacketMaximal(); + packet.topicFilters = []; + + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("cannot be empty"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("cannot be empty"); +}); + +test('External unsubscribe packet validation - undefined topic filter', async () => { + let packet = createExternalUnsubscribePacketMaximal(); + // @ts-ignore + delete packet.topicFilters[0]; + + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid string"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid string"); +}); + +test('External unsubscribe packet validation - null topic filter', async () => { + let packet = createExternalUnsubscribePacketMaximal(); + // @ts-ignore + packet.topicFilters[0] = null; + + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid string"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid string"); +}); + +test('External unsubscribe packet validation - topic filter bad type', async () => { + let packet = createExternalUnsubscribePacketMaximal(); + // @ts-ignore + packet.topicFilters[0] = 5; + + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid string"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid string"); +}); + +test('External unsubscribe packet validation - topic filter invalid', async () => { + let packet = createExternalUnsubscribePacketMaximal(); + // @ts-ignore + packet.topicFilters[0] = "#/a"; + + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid topic filter"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid topic filter"); +}); + +test('External unsubscribe packet validation - bad user properties type', async () => { + doBadUserPropertiesTypeTest(createExternalUnsubscribePacketMaximal()); +}); + +test('External unsubscribe packet validation - user properties name bad type', async () => { + doBadUserPropertiesBadNameTypeTest(createExternalUnsubscribePacketMaximal()); +}); + +test('External unsubscribe packet validation - user properties name undefined type', async () => { + doBadUserPropertiesUndefinedNameTest(createExternalUnsubscribePacketMaximal()); +}); + +test('External unsubscribe packet validation - bad user properties value type', async () => { + doBadUserPropertiesBadValueTypeTest(createExternalUnsubscribePacketMaximal()); +}); + +test('External unsubscribe packet validation - user properties value undefined type', async () => { + doBadUserPropertiesUndefinedValueTest(createExternalUnsubscribePacketMaximal()); +}); + +// Binary unsubscribes + +function creatdBinaryUnsubscribePacketMaximal() : model.UnsubscribePacketBinary { + let packet = createExternalUnsubscribePacketMaximal(); + let binaryUnsubscribe = model.convertInternalPacketToBinary(packet) as model.UnsubscribePacketBinary; + + binaryUnsubscribe.packetId = 3; + + return binaryUnsubscribe; +} + +test('Binary unsubscribe packet validation - success', async () => { + let packet = creatdBinaryUnsubscribePacketMaximal(); + let settings = createStandardNegotiatedSettings(); + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); +}); + +test('Binary unsubscribe packet validation - packet length too long', async () => { + let packet = creatdBinaryUnsubscribePacketMaximal(); + let settings = createStandardNegotiatedSettings(); + settings.maximumPacketSizeToServer = 15; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("exceeds established maximum packet size"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("exceeds established maximum packet size"); +}); + +test('Binary unsubscribe packet validation - zero packet id', async () => { + doBinaryZeroPacketIdTest(creatdBinaryUnsubscribePacketMaximal()); +}); + +test('Binary unsubscribe packet validation - undefined packet id', async () => { + doBinaryUndefinedPacketIdTest(creatdBinaryUnsubscribePacketMaximal()); +}); + +test('Binary unsubscribe packet validation - topic filter too long', async () => { + let packet = creatdBinaryUnsubscribePacketMaximal(); + let settings = createStandardNegotiatedSettings(); + + let encoder = new TextEncoder(); + let newTopicFilter = "a".repeat(65536); + packet.topicFiltersAsStrings[0] = newTopicFilter; + packet.topicFilters[0] = encoder.encode(newTopicFilter).buffer; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("not a 16-bit length buffer"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +}); + +test('Binary unsubscribe packet validation - user property name too long', async () => { + doBinaryUserPropertyNameTooLongTest(creatdBinaryUnsubscribePacketMaximal()); +}); + +test('Binary unsubscribe packet validation - user property value too long', async () => { + doBinaryUserPropertyValueTooLongTest(creatdBinaryUnsubscribePacketMaximal()); +}); + +// Unsuback Validation + +function createInternalUnsubackPacketMaximal() : model.UnsubackPacketInternal { + return { + type: mqtt5_packet.PacketType.Unsuback, + packetId: 3, + reasonCodes: [ + mqtt5_packet.UnsubackReasonCode.Success, + mqtt5_packet.UnsubackReasonCode.NoSubscriptionExisted, + ], + userProperties: [ + {name: "name", value: "value"}, + {name: "hello", value: "world"} + ] + }; +} + +// Inbound unsubacks + +test('Inbound unsuback packet validation - success', async () => { + let packet = createInternalUnsubackPacketMaximal(); + + validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt311); + validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt5); +}); + +test('Inbound unsuback packet validation - zero packet id', async () => { + let packet = createInternalUnsubackPacketMaximal(); + packet.packetId = 0; + + expect(() => { validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid packetId"); + expect(() => { validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid packetId"); +}); + +test('Inbound unsuback packet validation - bad reason code', async () => { + let packet = createInternalUnsubackPacketMaximal(); + packet.reasonCodes[0] = 3; + + validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid MQTT5 UnsubackReasonCode"); +}); + +// Connect Validation + +function createInternalConnectPacketMaximal() : model.ConnectPacketInternal { + return { + type: mqtt5_packet.PacketType.Connect, + cleanStart: true, + topicAliasMaximum: 10, + authenticationMethod: "GSSAPI", + authenticationData: new Uint8Array(10), + keepAliveIntervalSeconds: 3600, + clientId: "TerbTerberson", + username: "terb", + password: new Uint8Array(10), + sessionExpiryIntervalSeconds: 3600, + requestResponseInformation: true, + requestProblemInformation: true, + receiveMaximum: 10, + maximumPacketSizeBytes: 128 * 1024, + willDelayIntervalSeconds: 60, + will: { + topicName: "hello/there", + qos: mqtt5_packet.QoS.AtLeastOnce, + retain: false, + payload: new Uint8Array(10), + payloadFormat: mqtt5_packet.PayloadFormatIndicator.Bytes, + messageExpiryIntervalSeconds: 10, + responseTopic: "hello/there", + correlationData: new Uint8Array(10), + contentType: "rest/json", + }, + userProperties: [ + {name: "name", value: "value"}, + {name: "hello", value: "world"} + ] + }; +} + +function createBinaryConnectPacketMaximal() : model.ConnectPacketBinary { + let packet = createInternalConnectPacketMaximal(); + return model.convertInternalPacketToBinary(packet) as model.ConnectPacketBinary; +} + +// Binary connects + +test('Binary connect packet validation - success', async () => { + let packet = createBinaryConnectPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); +}); + +test('Binary connect packet validation - packet length too long', async () => { + let packet = createBinaryConnectPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + settings.maximumPacketSizeToServer = 15; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("exceeds established maximum packet size"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("exceeds established maximum packet size"); +}); + +test('Binary connect packet validation - client id too long', async () => { + let packet = createBinaryConnectPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + + let encoder = new TextEncoder(); + let newClientId = "a".repeat(65536); + packet.clientId = encoder.encode(newClientId).buffer; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("not a 16-bit length buffer"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +}); + +test('Binary connect packet validation - username too long', async () => { + let packet = createBinaryConnectPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + + let encoder = new TextEncoder(); + let newUsername = "o".repeat(65536); + packet.username = encoder.encode(newUsername).buffer; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("not a 16-bit length buffer"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +}); + +test('Binary connect packet validation - password too long', async () => { + let packet = createBinaryConnectPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + + let encoder = new TextEncoder(); + let newPassword = "o".repeat(65536); + packet.password = encoder.encode(newPassword).buffer; + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("not a 16-bit length buffer"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +}); + +test('Binary connect packet validation - authentication method too long', async () => { + let packet = createBinaryConnectPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + + let encoder = new TextEncoder(); + let newMethod = "a".repeat(65536); + packet.authenticationMethod = encoder.encode(newMethod).buffer; + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +}); + +test('Binary connect packet validation - authentication data too long', async () => { + let packet = createBinaryConnectPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + + packet.authenticationData = new Uint8Array(65537); + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +}); + +test('Binary connect packet validation - user property name too long', async () => { + doBinaryUserPropertyNameTooLongTest(createBinaryConnectPacketMaximal()); +}); + +test('Binary connect packet validation - user property value too long', async () => { + doBinaryUserPropertyValueTooLongTest(createBinaryConnectPacketMaximal()); +}); + +test('Binary connect packet validation - will too long', async () => { + let packet = createBinaryConnectPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + + // @ts-ignore + packet.will.payload = new Uint8Array(65537); + + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); }).toThrow("exceeds established maximum packet size"); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("exceeds established maximum packet size"); +}); + +// Disconnect Validation + +function createExternalDisconnectPacketMaximal() : mqtt5_packet.DisconnectPacket { + return { + type: mqtt5_packet.PacketType.Disconnect, + sessionExpiryIntervalSeconds: 3600, + reasonCode: mqtt5_packet.DisconnectReasonCode.DisconnectWithWillMessage, + reasonString: "Imtired", + serverReference: "somewhere.over.therainbow", + userProperties: [ + {name: "name", value: "value"}, + {name: "hello", value: "world"} + ] + }; +} + +// user-submitted disconnects + +test('External disconnect packet validation - isValid', async () => { + validate.validateInitialOutboundPacket(createExternalDisconnectPacketMaximal(), model.ProtocolMode.Mqtt311); + validate.validateInitialOutboundPacket(createExternalDisconnectPacketMaximal(), model.ProtocolMode.Mqtt5); +}); + +test('External disconnect packet validation - undefined reason code', async () => { + let packet = createExternalDisconnectPacketMaximal(); + // @ts-ignore + delete packet.reasonCode; + + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u8"); +}); + +test('External disconnect packet validation - reason code bad type', async () => { + let packet = createExternalDisconnectPacketMaximal(); + // @ts-ignore + packet.reasonCode = "Success"; + + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u8"); +}); + +test('External disconnect packet validation - reason code bad value', async () => { + let packet = createExternalDisconnectPacketMaximal(); + // @ts-ignore + packet.reasonCode = 127; + + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid MQTT5 DisconnectReasonCode"); +}); + +test('External disconnect packet validation - session expiry bad type', async () => { + let packet = createExternalDisconnectPacketMaximal(); + // @ts-ignore + packet.sessionExpiryIntervalSeconds = "Tomorrow"; + + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u32"); +}); + +test('External disconnect packet validation - session expiry too large', async () => { + let packet = createExternalDisconnectPacketMaximal(); + packet.sessionExpiryIntervalSeconds = 256 * 256 * 256 * 256; + + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u32"); +}); + +test('External disconnect packet validation - session expiry too small', async () => { + let packet = createExternalDisconnectPacketMaximal(); + packet.sessionExpiryIntervalSeconds = -1; + + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u32"); +}); + +test('External disconnect packet validation - reason string bad type', async () => { + let packet = createExternalDisconnectPacketMaximal(); + // @ts-ignore + packet.reasonString = {}; + + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid string"); +}); + +test('External disconnect packet validation - server reference bad type', async () => { + let packet = createExternalDisconnectPacketMaximal(); + // @ts-ignore + packet.serverReference = []; + + validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid string"); +}); + +test('External disconnect packet validation - bad user properties type', async () => { + doBadUserPropertiesTypeTest(createExternalDisconnectPacketMaximal()); +}); + +test('External disconnect packet validation - user properties name bad type', async () => { + doBadUserPropertiesBadNameTypeTest(createExternalDisconnectPacketMaximal()); +}); + +test('External disconnect packet validation - user properties name undefined type', async () => { + doBadUserPropertiesUndefinedNameTest(createExternalDisconnectPacketMaximal()); +}); + +test('External disconnect packet validation - bad user properties value type', async () => { + doBadUserPropertiesBadValueTypeTest(createExternalDisconnectPacketMaximal()); +}); + +test('External disconnect packet validation - user properties value undefined type', async () => { + doBadUserPropertiesUndefinedValueTest(createExternalDisconnectPacketMaximal()); +}); + +// binary disconnects + +function createBinaryDisconnectPacketMaximal() : model.DisconnectPacketBinary { + let packet = createExternalDisconnectPacketMaximal(); + let binaryPacket = model.convertInternalPacketToBinary(packet) as model.DisconnectPacketBinary; + + return binaryPacket; +} + +test('Binary disconnect packet validation - success', async () => { + let packet = createBinaryDisconnectPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); +}); + +test('Binary disconnect packet validation - packet too long', async () => { + let packet = createBinaryDisconnectPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + settings.maximumPacketSizeToServer = 20; + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("exceeds established maximum packet size"); +}); + +test('Binary disconnect packet validation - positive session expiry interval when previously established zero-length', async () => { + let packet = createBinaryDisconnectPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + settings.sessionExpiryInterval = 0; + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("cannot be positive"); +}); + +test('Binary disconnect packet validation - reason string too long', async () => { + let packet = createBinaryDisconnectPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + packet.reasonString = new Uint8Array(65536); + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +}); + +test('Binary disconnect packet validation - server reference too long', async () => { + let packet = createBinaryDisconnectPacketMaximal(); + let settings = createStandardNegotiatedSettings(); + packet.serverReference = new Uint8Array(65536); + + validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt311, settings); + expect(() => { validate.validateBinaryOutboundPacket(packet, model.ProtocolMode.Mqtt5, settings); }).toThrow("not a 16-bit length buffer"); +}); + +test('Binary disconnect packet validation - user property name too long', async () => { + doBinaryUserPropertyNameTooLongTest(createBinaryDisconnectPacketMaximal()); +}); + +test('Binary disconnect packet validation - user property value too long', async () => { + doBinaryUserPropertyValueTooLongTest(createBinaryDisconnectPacketMaximal()); +}); + +// inbound disconnects + +function createInternalDisconnectPacketMaximal() : model.DisconnectPacketInternal { + let packet = createExternalDisconnectPacketMaximal(); + delete packet.sessionExpiryIntervalSeconds; + packet.reasonCode = mqtt5_packet.DisconnectReasonCode.NormalDisconnection; + + return packet; +} + +test('Inbound disconnect packet validation - success', async () => { + let packet = createInternalDisconnectPacketMaximal(); + + validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt311); + validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt5); +}); + +test('Inbound disconnect packet validation - server-side session expiry', async () => { + let packet = createInternalDisconnectPacketMaximal(); + packet.sessionExpiryIntervalSeconds = 30; + + validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt311); + expect(() => { validate.validateInboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("must not define"); +}); \ No newline at end of file diff --git a/lib/browser/mqtt_internal/validate.ts b/lib/browser/mqtt_internal/validate.ts new file mode 100644 index 00000000..3582ae88 --- /dev/null +++ b/lib/browser/mqtt_internal/validate.ts @@ -0,0 +1,803 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +import {CrtError} from "../error"; +import * as encoder from "./encoder"; +import * as mqtt5_packet from '../../common/mqtt5_packet'; +import * as mqtt5_common from "../../common/mqtt5"; +import * as mqtt_shared from '../../common/mqtt_shared'; +import * as model from "./model"; + +/** + * This module contains three validation suites needed by the client implementation: + * + * 1. Validation of user-submitted outbound packets at submission time. This checks all public fields for valid + * types, values, and ranges. + * 2. Validation of internal outbound packets prior to encoding. This checks constraints on internally managed fields + * like packetId. It also checks constraints that can change across separate connection (maximum qos, for example). + * 3. Protocol-error validation of inbound packets from the server. This primarily checks protocol constraints + * and enumerated values. We don't validate against negotiated settings because the server breaking that contract + * isn't really a fatal flaw. In that case, it's better to be forgiving. + * + * Validation differs slightly based on what protocol level is being used (primarily reason codes). It is not + * considered a validation error to have 5-only fields set while operating in 311; the additional fields are just + * ignored. + * + * Validation failure is indicated by throwing a CrtError. + * + * There are a few validation checks that are done outside this module. Validation of binary properties that + * are lost on decoding (certain bits not set) is done in the decoder. Validation of maximum packet size constraints + * is done in the encoder (NYI though). + * + * We also skip validation of certain fields that are not relevant in the particular use case. For example, we don't + * validate subscription identifiers on outbound publishes because that field is ignored when converting the + * submitted packet into the internal model. + */ + +export function validateInitialOutboundPacket(packet: mqtt5_packet.IPacket, mode: model.ProtocolMode) { + switch(packet.type) { + case mqtt5_packet.PacketType.Publish: + validateUserSubmittedPublish(packet as mqtt5_packet.PublishPacket, mode); + break; + + case mqtt5_packet.PacketType.Subscribe: + validateUserSubmittedSubscribe(packet as mqtt5_packet.SubscribePacket, mode); + break; + + case mqtt5_packet.PacketType.Unsubscribe: + validateUserSubmittedUnsubscribe(packet as mqtt5_packet.UnsubscribePacket, mode); + break; + + case mqtt5_packet.PacketType.Disconnect: + validateUserSubmittedDisconnect(packet as mqtt5_packet.DisconnectPacket, mode); + break; + + default: + break; + } +} + +export function validateBinaryOutboundPacket(packet: model.IPacketBinary, mode: model.ProtocolMode, settings: mqtt5_common.NegotiatedSettings) { + switch(packet.type) { + case mqtt5_packet.PacketType.Publish: + validateBinaryPublish(packet as model.PublishPacketBinary, mode, settings, false); + break; + + case mqtt5_packet.PacketType.Subscribe: + validateBinarySubscribe(packet as model.SubscribePacketBinary, mode, settings); + break; + + case mqtt5_packet.PacketType.Unsubscribe: + validateBinaryUnsubscribe(packet as model.UnsubscribePacketBinary, mode, settings); + break; + + case mqtt5_packet.PacketType.Disconnect: + validateBinaryDisconnect(packet as model.DisconnectPacketBinary, mode, settings); + break; + + case mqtt5_packet.PacketType.Connect: + validateBinaryConnect(packet as model.ConnectPacketBinary, mode, settings); + break; + + case mqtt5_packet.PacketType.Puback: + validateBinaryPuback(packet as model.PubackPacketBinary, mode, settings); + break; + + default: + break; + } +} + +export function validateInboundPacket(packet: mqtt5_packet.IPacket, mode: model.ProtocolMode) { + switch(packet.type) { + case mqtt5_packet.PacketType.Publish: + validateInboundPublish(packet as model.PublishPacketInternal, mode); + break; + + case mqtt5_packet.PacketType.Puback: + validateInboundPuback(packet as model.PubackPacketInternal, mode); + break; + + case mqtt5_packet.PacketType.Connack: + validateInboundConnack(packet as model.ConnackPacketInternal, mode); + break; + + case mqtt5_packet.PacketType.Suback: + validateInboundSuback(packet as model.SubackPacketInternal, mode); + break; + + case mqtt5_packet.PacketType.Unsuback: + validateInboundUnsuback(packet as model.UnsubackPacketInternal, mode); + break; + + case mqtt5_packet.PacketType.Disconnect: + validateInboundDisconnect(packet as model.DisconnectPacketInternal, mode); + break; + + default: + break; + } +} + +// primitive fields + +// we don't validate booleans because we use truthiness to map to 0 or 1, so there's no need + +function validateU8(value: number, fieldName: string) { + if (!Number.isInteger(value) || value < 0 || value > 255) { + throw new CrtError(`Field "${fieldName}" with value "${value}" is not a valid u8`); + } +} + +function validateU16(value: number, fieldName: string) { + if (!Number.isInteger(value) || value < 0 || value > 65535) { + throw new CrtError(`Field "${fieldName}" with value "${value}" is not a valid u16`); + } +} + +function validateOptionalPositiveU16(value: number | undefined, fieldName: string) { + if (value != undefined) { + validateU16(value, fieldName); + if (value == 0) { + throw new CrtError(`Field "${fieldName}" with value "${value}" cannot be 0`); + } + } +} + +function validateU32(value: number, fieldName: string) { + if (!Number.isInteger(value) || value < 0 || value > (256 * 256 * 256 * 256 - 1)) { + throw new CrtError(`Field "${fieldName}" with value "${value}" is not a valid u32`); + } +} + +function validateOptionalU32(value: number | undefined, fieldName: string) { + if (value != undefined) { + validateU32(value, fieldName); + } +} + +function validateVli(value: number, fieldName: string) { + if (!Number.isInteger(value) || value < 0 || value > (128 * 128 * 128 * 128 - 1)) { + throw new CrtError(`Field "${fieldName}" with value "${value}" cannot be VLI-encoded`); + } +} + +function validateOptionalVli(value: number | undefined, fieldName: string) { + if (value != undefined) { + validateVli(value, fieldName); + } +} + +// we don't validate length here because we don't know encoding length without doing a utf-8 conversion. +// This means we validate string length in the internal validators which checks ArrayBuffer lengths. +function validateString(value: any, fieldName: string) { + if ((typeof value === 'string') || (value instanceof String)) { + return; + } + + throw new CrtError(`Field "${fieldName}" with value "${value}" is not a valid string`); +} + +function validateOptionalString(value: string | undefined, fieldName: string) { + if (value === undefined) { + return; + } + + validateString(value, fieldName); +} + + +function validateBufferLength(value : ArrayBuffer, fieldName: string) { + // we don't do typechecking here because this is only used by internal validators which are checking values + // that we explicitly constructed ourselves when converting to the binary model + if (value.byteLength > 65535) { + throw new CrtError(`Field "${fieldName}" is not a 16-bit length buffer`); + } +} + +function validateOptionalBufferLength(value : ArrayBuffer | undefined, fieldName: string) { + if (value == undefined) { + return; + } + + validateBufferLength(value, fieldName); +} + +// enum validation + +function validateSubackReasonCode(value: mqtt5_packet.SubackReasonCode, mode: model.ProtocolMode) { + validateU8(value, 'reasonCodes'); + + if (mode == model.ProtocolMode.Mqtt311) { + switch(value) { + case mqtt5_packet.SubackReasonCode.GrantedQoS0: + case mqtt5_packet.SubackReasonCode.GrantedQoS1: + case mqtt5_packet.SubackReasonCode.GrantedQoS2: + case mqtt5_packet.SubackReasonCode.Failure311: + break; + + default: + throw new CrtError(`"${value}" is not a valid MQTT311 SubackReasonCode`); + } + } else if (mode == model.ProtocolMode.Mqtt5) { + switch(value) { + case mqtt5_packet.SubackReasonCode.GrantedQoS0: + case mqtt5_packet.SubackReasonCode.GrantedQoS1: + case mqtt5_packet.SubackReasonCode.GrantedQoS2: + case mqtt5_packet.SubackReasonCode.UnspecifiedError: + case mqtt5_packet.SubackReasonCode.ImplementationSpecificError: + case mqtt5_packet.SubackReasonCode.NotAuthorized: + case mqtt5_packet.SubackReasonCode.TopicFilterInvalid: + case mqtt5_packet.SubackReasonCode.PacketIdentifierInUse: + case mqtt5_packet.SubackReasonCode.QuotaExceeded: + case mqtt5_packet.SubackReasonCode.SharedSubscriptionsNotSupported: + case mqtt5_packet.SubackReasonCode.SubscriptionIdentifiersNotSupported: + case mqtt5_packet.SubackReasonCode.WildcardSubscriptionsNotSupported: + break; + + default: + throw new CrtError(`"${value}" is not a valid MQTT5 SubackReasonCode`); + } + } +} + +function validatePubackReasonCode(value: mqtt5_packet.PubackReasonCode, mode: model.ProtocolMode) { + validateU8(value, 'reasonCode'); + if (mode == model.ProtocolMode.Mqtt5) { + switch(value) { + case mqtt5_packet.PubackReasonCode.Success: + case mqtt5_packet.PubackReasonCode.NoMatchingSubscribers: + case mqtt5_packet.PubackReasonCode.UnspecifiedError: + case mqtt5_packet.PubackReasonCode.ImplementationSpecificError: + case mqtt5_packet.PubackReasonCode.NotAuthorized: + case mqtt5_packet.PubackReasonCode.TopicNameInvalid: + case mqtt5_packet.PubackReasonCode.PacketIdentifierInUse: + case mqtt5_packet.PubackReasonCode.QuotaExceeded: + case mqtt5_packet.PubackReasonCode.PayloadFormatInvalid: + break; + + default: + throw new CrtError(`"${value}" is not a valid MQTT5 PubackReasonCode`); + } + } +} + +function validateUnsubackReasonCode(value: mqtt5_packet.UnsubackReasonCode, mode: model.ProtocolMode) { + validateU8(value, 'reasonCodes'); + if (mode == model.ProtocolMode.Mqtt5) { + switch(value) { + case mqtt5_packet.UnsubackReasonCode.Success: + case mqtt5_packet.UnsubackReasonCode.NoSubscriptionExisted: + case mqtt5_packet.UnsubackReasonCode.UnspecifiedError: + case mqtt5_packet.UnsubackReasonCode.ImplementationSpecificError: + case mqtt5_packet.UnsubackReasonCode.NotAuthorized: + case mqtt5_packet.UnsubackReasonCode.TopicFilterInvalid: + case mqtt5_packet.UnsubackReasonCode.PacketIdentifierInUse: + break; + + default: + throw new CrtError(`"${value}" is not a valid MQTT5 UnsubackReasonCode`); + } + } +} + +function validateConnectReasonCode(value: mqtt5_packet.ConnectReasonCode, mode: model.ProtocolMode) { + validateU8(value, 'reasonCode'); + + if (mode == model.ProtocolMode.Mqtt311) { + switch(value) { + case mqtt5_packet.ConnectReasonCode.Success: + case mqtt5_packet.ConnectReasonCode.UnacceptableProtocolVersion311: + case mqtt5_packet.ConnectReasonCode.ClientIdRejected311: + case mqtt5_packet.ConnectReasonCode.ServerUnavailable311: + case mqtt5_packet.ConnectReasonCode.InvalidUsernameOrPassword311: + case mqtt5_packet.ConnectReasonCode.NotAuthorized311: + break; + + default: + throw new CrtError(`"${value}" is not a valid MQTT311 ConnectReasonCode`); + } + } else if (mode == model.ProtocolMode.Mqtt5) { + switch(value) { + case mqtt5_packet.ConnectReasonCode.Success: + case mqtt5_packet.ConnectReasonCode.UnspecifiedError: + case mqtt5_packet.ConnectReasonCode.MalformedPacket: + case mqtt5_packet.ConnectReasonCode.ProtocolError: + case mqtt5_packet.ConnectReasonCode.ImplementationSpecificError: + case mqtt5_packet.ConnectReasonCode.UnsupportedProtocolVersion: + case mqtt5_packet.ConnectReasonCode.ClientIdentifierNotValid: + case mqtt5_packet.ConnectReasonCode.BadUsernameOrPassword: + case mqtt5_packet.ConnectReasonCode.NotAuthorized: + case mqtt5_packet.ConnectReasonCode.ServerUnavailable: + case mqtt5_packet.ConnectReasonCode.ServerBusy: + case mqtt5_packet.ConnectReasonCode.Banned: + case mqtt5_packet.ConnectReasonCode.BadAuthenticationMethod: + case mqtt5_packet.ConnectReasonCode.TopicNameInvalid: + case mqtt5_packet.ConnectReasonCode.PacketTooLarge: + case mqtt5_packet.ConnectReasonCode.QuotaExceeded: + case mqtt5_packet.ConnectReasonCode.PayloadFormatInvalid: + case mqtt5_packet.ConnectReasonCode.RetainNotSupported: + case mqtt5_packet.ConnectReasonCode.QosNotSupported: + case mqtt5_packet.ConnectReasonCode.UseAnotherServer: + case mqtt5_packet.ConnectReasonCode.ServerMoved: + case mqtt5_packet.ConnectReasonCode.ConnectionRateExceeded: + break; + + default: + throw new CrtError(`"${value}" is not a valid MQTT5 ConnectReasonCode`); + } + } + +} + +function validateDisconnectReasonCode(value: mqtt5_packet.DisconnectReasonCode, mode: model.ProtocolMode) { + validateU8(value, 'reasonCode'); + if (mode == model.ProtocolMode.Mqtt5) { + switch(value) { + case mqtt5_packet.DisconnectReasonCode.NormalDisconnection: + case mqtt5_packet.DisconnectReasonCode.DisconnectWithWillMessage: + case mqtt5_packet.DisconnectReasonCode.UnspecifiedError: + case mqtt5_packet.DisconnectReasonCode.MalformedPacket: + case mqtt5_packet.DisconnectReasonCode.ProtocolError: + case mqtt5_packet.DisconnectReasonCode.ImplementationSpecificError: + case mqtt5_packet.DisconnectReasonCode.NotAuthorized: + case mqtt5_packet.DisconnectReasonCode.ServerBusy: + case mqtt5_packet.DisconnectReasonCode.ServerShuttingDown: + case mqtt5_packet.DisconnectReasonCode.KeepAliveTimeout: + case mqtt5_packet.DisconnectReasonCode.SessionTakenOver: + case mqtt5_packet.DisconnectReasonCode.TopicFilterInvalid: + case mqtt5_packet.DisconnectReasonCode.TopicNameInvalid: + case mqtt5_packet.DisconnectReasonCode.ReceiveMaximumExceeded: + case mqtt5_packet.DisconnectReasonCode.TopicAliasInvalid: + case mqtt5_packet.DisconnectReasonCode.PacketTooLarge: + case mqtt5_packet.DisconnectReasonCode.MessageRateTooHigh: + case mqtt5_packet.DisconnectReasonCode.QuotaExceeded: + case mqtt5_packet.DisconnectReasonCode.AdministrativeAction: + case mqtt5_packet.DisconnectReasonCode.PayloadFormatInvalid: + case mqtt5_packet.DisconnectReasonCode.RetainNotSupported: + case mqtt5_packet.DisconnectReasonCode.QosNotSupported: + case mqtt5_packet.DisconnectReasonCode.UseAnotherServer: + case mqtt5_packet.DisconnectReasonCode.ServerMoved: + case mqtt5_packet.DisconnectReasonCode.SharedSubscriptionsNotSupported: + case mqtt5_packet.DisconnectReasonCode.ConnectionRateExceeded: + case mqtt5_packet.DisconnectReasonCode.MaximumConnectTime: + case mqtt5_packet.DisconnectReasonCode.SubscriptionIdentifiersNotSupported: + case mqtt5_packet.DisconnectReasonCode.WildcardSubscriptionsNotSupported: + break; + + default: + throw new CrtError(`"${value}" is not a valid MQTT5 DisconnectReasonCode`); + } + } +} + +function validateQos(qos: mqtt5_packet.QoS) { + validateU8(qos, 'QoS'); + switch (qos) { + case mqtt5_packet.QoS.AtLeastOnce: + case mqtt5_packet.QoS.AtMostOnce: + case mqtt5_packet.QoS.ExactlyOnce: + break; + + default: + throw new CrtError(`"${qos}" is not a valid QualityOfService`); + } +} + +function validateOptionalPayloadFormat(payloadFormat: mqtt5_packet.PayloadFormatIndicator | undefined, fieldName: string) { + if (payloadFormat === undefined) { + return; + } + + validateU8(payloadFormat, fieldName); + switch (payloadFormat) { + case mqtt5_packet.PayloadFormatIndicator.Bytes: + case mqtt5_packet.PayloadFormatIndicator.Utf8: + break; + + default: + throw new CrtError(`Field "${fieldName}" with value "${payloadFormat}" is not a valid PayloadFormatIndicator`); + } +} + +function validateOptionalRetainHandlingType(retainHandling: mqtt5_packet.RetainHandlingType | undefined, fieldName: string) { + if (retainHandling === undefined) { + return; + } + + validateU8(retainHandling, fieldName); + switch (retainHandling) { + case mqtt5_packet.RetainHandlingType.SendOnSubscribe: + case mqtt5_packet.RetainHandlingType.SendOnSubscribeIfNew: + case mqtt5_packet.RetainHandlingType.DontSend: + break; + + default: + throw new CrtError(`Field "${fieldName}" with value "${retainHandling}" is not a valid RetainHandlingType`); + } +} + +// misc validation utilities + +function validateUserProperties(userProperties: Array | undefined) { + if (!userProperties) { + return; + } + + if (!Array.isArray(userProperties)) { + throw new CrtError('UserProperties is not an array'); + } + + for (let userProperty of userProperties) { + validateString(userProperty.name, 'UserProperty.name'); + validateString(userProperty.value, 'UserProperty.value'); + } +} + +function validatePayload(payload: mqtt5_packet.Payload | undefined) { + if (!payload) { + return; + } + + if (!model.isValidPayload(payload)) { + throw new CrtError("Invalid payload value"); + } +} + +function validateBinaryData(value: mqtt5_packet.BinaryData, fieldName: string) { + if (!model.isValidBinaryData(value)) { + throw new CrtError(`Field ${fieldName} is not valid binary data`); + } +} + +function validateOptionalBinaryData(value: mqtt5_packet.BinaryData | undefined, fieldName: string) { + if (!value) { + return; + } + + return validateBinaryData(value, fieldName); +} + +function validateTopic(value: string, fieldName:string) { + validateString(value, fieldName); + if (!mqtt_shared.isValidTopic(value)) { + throw new CrtError(`value "${value}" of field "${fieldName}" is not a valid topic`); + } +} + +function validateOptionalTopic(value: string | undefined, fieldName:string) { + if (value == undefined) { + return; + } + + validateTopic(value, fieldName); +} + +function validateTopicFilter(value: string, fieldName:string) { + validateString(value, fieldName); + if (!mqtt_shared.isValidTopicFilter(value)) { + throw new CrtError(`value "${value}" of field "${fieldName}" is not a valid topic filter`); + } +} + +function validateRequiredPacketId(value: number | undefined, fieldName:string) { + if (value == undefined) { + throw new CrtError(`packet id field ${fieldName} must be defined"`); + } + + validateU16(value, fieldName); + if (value == 0) { + throw new CrtError(`packet id field "${fieldName}" is not a valid packetId`); + } +} + +// user-submitted outbound packet validators + +function validateUserSubmittedPublish(packet: mqtt5_packet.PublishPacket, mode: model.ProtocolMode) { + validateTopic(packet.topicName, 'topicName'); + validatePayload(packet.payload); + validateQos(packet.qos); + + if (mode == model.ProtocolMode.Mqtt5) { + validateOptionalPayloadFormat(packet.payloadFormat, "payloadFormat"); + validateOptionalU32(packet.messageExpiryIntervalSeconds, "messageExpiryIntervalSeconds"); + validateOptionalPositiveU16(packet.topicAlias, "topicAlias"); // 0 is also invalid + validateOptionalTopic(packet.responseTopic, "responseTopic"); + validateOptionalBinaryData(packet.correlationData, "correlationData"); + validateOptionalString(packet.contentType, "contentType"); + validateUserProperties(packet.userProperties); + } +} + +function validateSubscriptions(subscriptions: Array, mode: model.ProtocolMode) { + if (!Array.isArray(subscriptions)) { + throw new CrtError("Subscriptions must be an array"); + } + + if (subscriptions.length == 0) { + throw new CrtError("Subscriptions cannot be empty"); + } + + for (let subscription of subscriptions) { + validateTopicFilter(subscription.topicFilter, "topicFilter"); + validateQos(subscription.qos); + + if (mode == model.ProtocolMode.Mqtt5) { + // no need to validate noLocal or retainAsPublished booleans + validateOptionalRetainHandlingType(subscription.retainHandlingType, "Subscription.retainHandling"); + } + } +} + +function validateUserSubmittedSubscribe(packet: mqtt5_packet.SubscribePacket, mode: model.ProtocolMode) { + validateSubscriptions(packet.subscriptions, mode); + if (mode == model.ProtocolMode.Mqtt5) { + validateOptionalVli(packet.subscriptionIdentifier, "subscriptionIdentifier"); + validateUserProperties(packet.userProperties); + } +} + +function validateUserSubmittedUnsubscribe(packet: mqtt5_packet.UnsubscribePacket, mode: model.ProtocolMode) { + if (!packet.topicFilters || packet.topicFilters.length == 0) { + throw new CrtError("TopicFilters cannot be empty"); + } + + for (let filter of packet.topicFilters) { + validateTopicFilter(filter, "topicFilters"); + } + + if (mode == model.ProtocolMode.Mqtt5) { + validateUserProperties(packet.userProperties); + } +} + +function validateUserSubmittedDisconnect(packet: mqtt5_packet.DisconnectPacket, mode: model.ProtocolMode) { + if (mode == model.ProtocolMode.Mqtt5) { + validateDisconnectReasonCode(packet.reasonCode, mode); + validateOptionalU32(packet.sessionExpiryIntervalSeconds, "sessionExpiryIntervalSeconds"); + validateOptionalString(packet.reasonString, "reasonString"); + validateOptionalString(packet.serverReference, "serverReference"); + validateUserProperties(packet.userProperties); + } +} + +// binary outbound packet validators; user-submitted validation is not repeated here + +function validatePacketLength(packet: model.IPacketBinary, mode: model.ProtocolMode, maximumPacketSize: number) { + let length = encoder.computePacketEncodingLength(packet, mode); + if (length > maximumPacketSize) { + throw new CrtError(`Packet with length ${length} exceeds established maximum packet size of ${maximumPacketSize}`); + } +} + +function validateBinaryUserProperties(userProperties: Array | undefined) { + if (!userProperties) { + return; + } + + for (let userProperty of userProperties) { + validateBufferLength(userProperty.name, "UserProperty.name"); + validateBufferLength(userProperty.value, "UserProperty.value"); + } +} + +function validateBinaryPublish(packet: model.PublishPacketBinary, mode: model.ProtocolMode, settings: mqtt5_common.NegotiatedSettings, isWill: boolean) { + if (isWill) { + validatePacketLength(packet, mode, 65535); + } else { + validatePacketLength(packet, mode, settings.maximumPacketSizeToServer); + } + + if (!isWill) { + if (packet.qos == mqtt5_packet.QoS.AtMostOnce) { + if (packet.packetId != undefined) { + throw new CrtError("packetId must not be set on outbound publish packets with QoS 0"); + } + + if (packet.duplicate) { + throw new CrtError("duplicate must not be set on outbound publish packets with QoS 0"); + } + } else { + validateRequiredPacketId(packet.packetId, "packetId"); + } + } + + if (packet.retain && !settings.retainAvailable) { + throw new CrtError("retain cannot be set on outbound publish packets if the server does not support retained messages"); + } + + if (packet.qos > settings.maximumQos) { + throw new CrtError(`QoS ${packet.qos} is greater than the maximum QoS (${settings.maximumQos}) supported by the server`); + } + + validateBufferLength(packet.topicName, "topicName"); + + if (mode == model.ProtocolMode.Mqtt5) { + if (packet.subscriptionIdentifiers != undefined) { + throw new CrtError("subscriptionIdentifiers may not be set on outbound publish packets"); + } + + if (!isWill && packet.topicAlias != undefined) { + if (packet.topicAlias == 0) { + throw new CrtError("topicAlias cannot be zero"); + } else if (packet.topicAlias > settings.topicAliasMaximumToServer) { + throw new CrtError(`topicAlias value ${packet.topicAlias} is greater than the maximum topic alias (${settings.topicAliasMaximumToServer}) supported by the server`); + } + } + + validateOptionalBufferLength(packet.responseTopic, "responseTopic"); + validateOptionalBufferLength(packet.correlationData, "correlationData"); + validateOptionalBufferLength(packet.contentType, "contentType"); + validateBinaryUserProperties(packet.userProperties); + } +} + +function validateBinaryPuback(packet: model.PubackPacketBinary, mode: model.ProtocolMode, settings: mqtt5_common.NegotiatedSettings) { + validatePacketLength(packet, mode, settings.maximumPacketSizeToServer); + validateRequiredPacketId(packet.packetId, "packetId"); + + if (mode == model.ProtocolMode.Mqtt5) { + validateOptionalBufferLength(packet.reasonString, "reasonString"); + validateBinaryUserProperties(packet.userProperties); + } +} + +function validateSubscription(subscription: model.SubscriptionBinary, mode: model.ProtocolMode, settings: mqtt5_common.NegotiatedSettings) { + let properties = mqtt_shared.computeTopicProperties(subscription.topicFilterAsString, true); + if (properties.isShared) { + if (!settings.sharedSubscriptionsAvailable) { + throw new CrtError("Shared subscriptions are not supported by the server"); + } + + if (mode == model.ProtocolMode.Mqtt5) { + if (subscription.noLocal) { + throw new CrtError("noLocal may not be set on a shared subscriptions"); + } + } + } + + if (properties.hasWildcard && !settings.wildcardSubscriptionsAvailable) { + throw new CrtError("Wildcard subscriptions are not supported by the server"); + } + + validateBufferLength(subscription.topicFilter, "subscription.topicFilter"); +} + +function validateBinarySubscribe(packet: model.SubscribePacketBinary, mode: model.ProtocolMode, settings: mqtt5_common.NegotiatedSettings) { + validatePacketLength(packet, mode, settings.maximumPacketSizeToServer); + validateRequiredPacketId(packet.packetId, "packetId"); + + for (let subscription of packet.subscriptions) { + validateSubscription(subscription, mode, settings); + } + + if (mode == model.ProtocolMode.Mqtt5) { + validateBinaryUserProperties(packet.userProperties); + } +} + +function validateBinaryUnsubscribe(packet: model.UnsubscribePacketBinary, mode: model.ProtocolMode, settings: mqtt5_common.NegotiatedSettings) { + validatePacketLength(packet, mode, settings.maximumPacketSizeToServer); + validateRequiredPacketId(packet.packetId, "packetId"); + + for (let topicFilter of packet.topicFilters) { + validateBufferLength(topicFilter, "topicFilter"); + } + + if (mode == model.ProtocolMode.Mqtt5) { + validateBinaryUserProperties(packet.userProperties); + } +} + +function validateBinaryDisconnect(packet: model.DisconnectPacketBinary, mode: model.ProtocolMode, settings: mqtt5_common.NegotiatedSettings) { + validatePacketLength(packet, mode, settings.maximumPacketSizeToServer); + + if (mode == model.ProtocolMode.Mqtt5) { + if (settings.sessionExpiryInterval == 0) { + if (packet.sessionExpiryIntervalSeconds != undefined && packet.sessionExpiryIntervalSeconds > 0) { + throw new CrtError("sessionExpiryIntervalSeconds cannot be positive when the connection was established with a zero-valued session expiry interval"); + } + } + + validateOptionalBufferLength(packet.reasonString, "reasonString"); + validateOptionalBufferLength(packet.serverReference, "serverReference"); + validateBinaryUserProperties(packet.userProperties); + } +} + +// Connect packets are synthesized internally based on configuration settings and state +// we validate type and integer widths when we validate the corresponding component of client configuration +function validateBinaryConnect(packet: model.ConnectPacketBinary, mode: model.ProtocolMode, settings: mqtt5_common.NegotiatedSettings) { + validatePacketLength(packet, mode, settings.maximumPacketSizeToServer); + + validateOptionalBufferLength(packet.clientId, "clientId"); + validateOptionalBufferLength(packet.username, "username"); + validateOptionalBufferLength(packet.password, "password"); + + if (packet.will) { + validateBinaryPublish(packet.will, mode, settings, true); + } + + if (mode == model.ProtocolMode.Mqtt5) { + validateOptionalBufferLength(packet.authenticationMethod, "authenticationMethod"); + validateOptionalBufferLength(packet.authenticationData, "authenticationData"); + + validateBinaryUserProperties(packet.userProperties); + } +} + +// inbound packet validators - we don't type check or integer-width check anything because we're the ones +// who initialized the packet with appropriate byte-level decoding operations. We do check +// 1. enum values +// 2. packet ids (non-zero) +// 3. misc. property constraints + +function validateInboundPublish(packet: model.PublishPacketInternal, mode: model.ProtocolMode) { + validateQos(packet.qos); + if (packet.qos == mqtt5_packet.QoS.AtMostOnce) { + if (packet.packetId != undefined) { + throw new CrtError("packetId must not be set on QoS 0 publishes"); + } + } else { + validateRequiredPacketId(packet.packetId, "packetId"); + } + + if (packet.topicName.length == 0) { + throw new CrtError("topicName is empty (alias could not be resolved)"); + } +} + +function validateInboundPuback(packet: model.PubackPacketInternal, mode: model.ProtocolMode) { + validateRequiredPacketId(packet.packetId, "packetId"); + validatePubackReasonCode(packet.reasonCode, mode); +} + +function validateInboundConnack(packet: model.ConnackPacketInternal, mode: model.ProtocolMode) { + validateConnectReasonCode(packet.reasonCode, mode); + if (packet.sessionPresent) { + if (packet.reasonCode != mqtt5_packet.ConnectReasonCode.Success) { + throw new CrtError("sessionPresent cannot be true with an unsuccessful connect reason code"); + } + } + + if (mode == model.ProtocolMode.Mqtt5) { + if (packet.receiveMaximum != undefined && packet.receiveMaximum == 0) { + throw new CrtError("receiveMaximum must be a positive integer"); + } + + if (packet.maximumQos != undefined) { + if (packet.maximumQos != mqtt5_packet.QoS.AtLeastOnce && packet.maximumQos != mqtt5_packet.QoS.AtMostOnce) { + throw new CrtError("maximumQos can only be 0 or 1"); + } + } + + if (packet.maximumPacketSize != undefined && packet.maximumPacketSize == 0) { + throw new CrtError("maximumPacketSize must be a positive integer"); + } + } +} + +function validateInboundSuback(packet: model.SubackPacketInternal, mode: model.ProtocolMode) { + validateRequiredPacketId(packet.packetId, "packetId"); + for (let reasonCode of packet.reasonCodes) { + validateSubackReasonCode(reasonCode, mode); + } +} + +function validateInboundUnsuback(packet: model.UnsubackPacketInternal, mode: model.ProtocolMode) { + validateRequiredPacketId(packet.packetId, "packetId"); + for (let reasonCode of packet.reasonCodes) { + validateUnsubackReasonCode(reasonCode, mode); + } +} + +function validateInboundDisconnect(packet: model.DisconnectPacketInternal, mode: model.ProtocolMode) { + validateDisconnectReasonCode(packet.reasonCode, mode); + if (mode == model.ProtocolMode.Mqtt5) { + if (packet.sessionExpiryIntervalSeconds != undefined) { + throw new CrtError("server Disconnect packets must not define sessionExpiryIntervalSeconds"); + } + } +} From 2d52331c5899ab40733a4fa9b5ea99998c0af6a2 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 12 Mar 2026 09:08:57 -0700 Subject: [PATCH 2/2] Fixes for client config validation --- lib/browser/mqtt_internal/validate.spec.ts | 24 ++-- lib/browser/mqtt_internal/validate.ts | 121 ++++----------------- 2 files changed, 35 insertions(+), 110 deletions(-) diff --git a/lib/browser/mqtt_internal/validate.spec.ts b/lib/browser/mqtt_internal/validate.spec.ts index 3e457eb0..2d3862cc 100644 --- a/lib/browser/mqtt_internal/validate.spec.ts +++ b/lib/browser/mqtt_internal/validate.spec.ts @@ -139,16 +139,16 @@ test('External publish packet validation - undefined qos', async () => { let packet = createExternalPublishPacketMaximal(); // @ts-ignore delete packet.qos; - expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid u8"); - expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u8"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid QualityOfService"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid QualityOfService"); }); test('External publish packet validation - bad qos type', async () => { let packet = createExternalPublishPacketMaximal(); // @ts-ignore packet.qos = "hi"; - expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid u8"); - expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u8"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid QualityOfService"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid QualityOfService"); }); test('External publish packet validation - bad qos value', async () => { @@ -172,7 +172,7 @@ test('External publish packet validation - bad payload format type', async () => // @ts-ignore packet.payloadFormat = "hi"; validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); - expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u8"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid PayloadFormatIndicator"); }); test('External publish packet validation - bad payload format value', async () => { @@ -691,8 +691,8 @@ test('External subscribe packet validation - undefined qos', async () => { // @ts-ignore delete packet.subscriptions[0].qos; - expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid u8"); - expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u8"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid QualityOfService"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid QualityOfService"); }); test('External subscribe packet validation - qos bad type', async () => { @@ -700,8 +700,8 @@ test('External subscribe packet validation - qos bad type', async () => { // @ts-ignore packet.subscriptions[0].qos = "qos"; - expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid u8"); - expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u8"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); }).toThrow("not a valid QualityOfService"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid QualityOfService"); }); test('External subscribe packet validation - qos invalid', async () => { @@ -719,7 +719,7 @@ test('External subscribe packet validation - retain handling type bad type', asy packet.subscriptions[1].retainHandlingType = "qos"; validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); - expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u8"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid RetainHandlingType"); }); test('External subscribe packet validation - retain handling type invalid', async () => { @@ -1274,7 +1274,7 @@ test('External disconnect packet validation - undefined reason code', async () = delete packet.reasonCode; validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); - expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u8"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid MQTT5 DisconnectReasonCode"); }); test('External disconnect packet validation - reason code bad type', async () => { @@ -1283,7 +1283,7 @@ test('External disconnect packet validation - reason code bad type', async () => packet.reasonCode = "Success"; validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt311); - expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid u8"); + expect(() => { validate.validateInitialOutboundPacket(packet, model.ProtocolMode.Mqtt5); }).toThrow("not a valid MQTT5 DisconnectReasonCode"); }); test('External disconnect packet validation - reason code bad value', async () => { diff --git a/lib/browser/mqtt_internal/validate.ts b/lib/browser/mqtt_internal/validate.ts index 3582ae88..00344eb6 100644 --- a/lib/browser/mqtt_internal/validate.ts +++ b/lib/browser/mqtt_internal/validate.ts @@ -131,7 +131,7 @@ function validateU8(value: number, fieldName: string) { } } -function validateU16(value: number, fieldName: string) { +export function validateU16(value: number, fieldName: string) { if (!Number.isInteger(value) || value < 0 || value > 65535) { throw new CrtError(`Field "${fieldName}" with value "${value}" is not a valid u16`); } @@ -152,7 +152,7 @@ function validateU32(value: number, fieldName: string) { } } -function validateOptionalU32(value: number | undefined, fieldName: string) { +export function validateOptionalU32(value: number | undefined, fieldName: string) { if (value != undefined) { validateU32(value, fieldName); } @@ -180,7 +180,7 @@ function validateString(value: any, fieldName: string) { throw new CrtError(`Field "${fieldName}" with value "${value}" is not a valid string`); } -function validateOptionalString(value: string | undefined, fieldName: string) { +export function validateOptionalString(value: string | undefined, fieldName: string) { if (value === undefined) { return; } @@ -244,42 +244,18 @@ function validateSubackReasonCode(value: mqtt5_packet.SubackReasonCode, mode: mo } function validatePubackReasonCode(value: mqtt5_packet.PubackReasonCode, mode: model.ProtocolMode) { - validateU8(value, 'reasonCode'); - if (mode == model.ProtocolMode.Mqtt5) { - switch(value) { - case mqtt5_packet.PubackReasonCode.Success: - case mqtt5_packet.PubackReasonCode.NoMatchingSubscribers: - case mqtt5_packet.PubackReasonCode.UnspecifiedError: - case mqtt5_packet.PubackReasonCode.ImplementationSpecificError: - case mqtt5_packet.PubackReasonCode.NotAuthorized: - case mqtt5_packet.PubackReasonCode.TopicNameInvalid: - case mqtt5_packet.PubackReasonCode.PacketIdentifierInUse: - case mqtt5_packet.PubackReasonCode.QuotaExceeded: - case mqtt5_packet.PubackReasonCode.PayloadFormatInvalid: - break; - - default: - throw new CrtError(`"${value}" is not a valid MQTT5 PubackReasonCode`); - } + if (mqtt5_packet.PubackReasonCode[value] === undefined) { + throw new CrtError(`"${value}" is not a valid MQTT5 PubackReasonCode`); } } function validateUnsubackReasonCode(value: mqtt5_packet.UnsubackReasonCode, mode: model.ProtocolMode) { - validateU8(value, 'reasonCodes'); - if (mode == model.ProtocolMode.Mqtt5) { - switch(value) { - case mqtt5_packet.UnsubackReasonCode.Success: - case mqtt5_packet.UnsubackReasonCode.NoSubscriptionExisted: - case mqtt5_packet.UnsubackReasonCode.UnspecifiedError: - case mqtt5_packet.UnsubackReasonCode.ImplementationSpecificError: - case mqtt5_packet.UnsubackReasonCode.NotAuthorized: - case mqtt5_packet.UnsubackReasonCode.TopicFilterInvalid: - case mqtt5_packet.UnsubackReasonCode.PacketIdentifierInUse: - break; + if (mode == model.ProtocolMode.Mqtt311) { + return; + } - default: - throw new CrtError(`"${value}" is not a valid MQTT5 UnsubackReasonCode`); - } + if (mqtt5_packet.UnsubackReasonCode[value] === undefined) { + throw new CrtError(`"${value}" is not a valid MQTT5 UnsubackReasonCode`); } } @@ -333,56 +309,18 @@ function validateConnectReasonCode(value: mqtt5_packet.ConnectReasonCode, mode: } function validateDisconnectReasonCode(value: mqtt5_packet.DisconnectReasonCode, mode: model.ProtocolMode) { - validateU8(value, 'reasonCode'); - if (mode == model.ProtocolMode.Mqtt5) { - switch(value) { - case mqtt5_packet.DisconnectReasonCode.NormalDisconnection: - case mqtt5_packet.DisconnectReasonCode.DisconnectWithWillMessage: - case mqtt5_packet.DisconnectReasonCode.UnspecifiedError: - case mqtt5_packet.DisconnectReasonCode.MalformedPacket: - case mqtt5_packet.DisconnectReasonCode.ProtocolError: - case mqtt5_packet.DisconnectReasonCode.ImplementationSpecificError: - case mqtt5_packet.DisconnectReasonCode.NotAuthorized: - case mqtt5_packet.DisconnectReasonCode.ServerBusy: - case mqtt5_packet.DisconnectReasonCode.ServerShuttingDown: - case mqtt5_packet.DisconnectReasonCode.KeepAliveTimeout: - case mqtt5_packet.DisconnectReasonCode.SessionTakenOver: - case mqtt5_packet.DisconnectReasonCode.TopicFilterInvalid: - case mqtt5_packet.DisconnectReasonCode.TopicNameInvalid: - case mqtt5_packet.DisconnectReasonCode.ReceiveMaximumExceeded: - case mqtt5_packet.DisconnectReasonCode.TopicAliasInvalid: - case mqtt5_packet.DisconnectReasonCode.PacketTooLarge: - case mqtt5_packet.DisconnectReasonCode.MessageRateTooHigh: - case mqtt5_packet.DisconnectReasonCode.QuotaExceeded: - case mqtt5_packet.DisconnectReasonCode.AdministrativeAction: - case mqtt5_packet.DisconnectReasonCode.PayloadFormatInvalid: - case mqtt5_packet.DisconnectReasonCode.RetainNotSupported: - case mqtt5_packet.DisconnectReasonCode.QosNotSupported: - case mqtt5_packet.DisconnectReasonCode.UseAnotherServer: - case mqtt5_packet.DisconnectReasonCode.ServerMoved: - case mqtt5_packet.DisconnectReasonCode.SharedSubscriptionsNotSupported: - case mqtt5_packet.DisconnectReasonCode.ConnectionRateExceeded: - case mqtt5_packet.DisconnectReasonCode.MaximumConnectTime: - case mqtt5_packet.DisconnectReasonCode.SubscriptionIdentifiersNotSupported: - case mqtt5_packet.DisconnectReasonCode.WildcardSubscriptionsNotSupported: - break; + if (mode == model.ProtocolMode.Mqtt311) { + return; + } - default: - throw new CrtError(`"${value}" is not a valid MQTT5 DisconnectReasonCode`); - } + if (mqtt5_packet.DisconnectReasonCode[value] === undefined) { + throw new CrtError(`"${value}" is not a valid MQTT5 DisconnectReasonCode`); } } function validateQos(qos: mqtt5_packet.QoS) { - validateU8(qos, 'QoS'); - switch (qos) { - case mqtt5_packet.QoS.AtLeastOnce: - case mqtt5_packet.QoS.AtMostOnce: - case mqtt5_packet.QoS.ExactlyOnce: - break; - - default: - throw new CrtError(`"${qos}" is not a valid QualityOfService`); + if (mqtt5_packet.QoS[qos] === undefined) { + throw new CrtError(`"${qos}" is not a valid QualityOfService`); } } @@ -391,14 +329,8 @@ function validateOptionalPayloadFormat(payloadFormat: mqtt5_packet.PayloadFormat return; } - validateU8(payloadFormat, fieldName); - switch (payloadFormat) { - case mqtt5_packet.PayloadFormatIndicator.Bytes: - case mqtt5_packet.PayloadFormatIndicator.Utf8: - break; - - default: - throw new CrtError(`Field "${fieldName}" with value "${payloadFormat}" is not a valid PayloadFormatIndicator`); + if (mqtt5_packet.PayloadFormatIndicator[payloadFormat] === undefined) { + throw new CrtError(`Field "${fieldName}" with value "${payloadFormat}" is not a valid PayloadFormatIndicator`); } } @@ -407,21 +339,14 @@ function validateOptionalRetainHandlingType(retainHandling: mqtt5_packet.RetainH return; } - validateU8(retainHandling, fieldName); - switch (retainHandling) { - case mqtt5_packet.RetainHandlingType.SendOnSubscribe: - case mqtt5_packet.RetainHandlingType.SendOnSubscribeIfNew: - case mqtt5_packet.RetainHandlingType.DontSend: - break; - - default: - throw new CrtError(`Field "${fieldName}" with value "${retainHandling}" is not a valid RetainHandlingType`); + if (mqtt5_packet.RetainHandlingType[retainHandling] === undefined) { + throw new CrtError(`Field "${fieldName}" with value "${retainHandling}" is not a valid RetainHandlingType`); } } // misc validation utilities -function validateUserProperties(userProperties: Array | undefined) { +export function validateUserProperties(userProperties: Array | undefined) { if (!userProperties) { return; } @@ -452,7 +377,7 @@ function validateBinaryData(value: mqtt5_packet.BinaryData, fieldName: string) { } } -function validateOptionalBinaryData(value: mqtt5_packet.BinaryData | undefined, fieldName: string) { +export function validateOptionalBinaryData(value: mqtt5_packet.BinaryData | undefined, fieldName: string) { if (!value) { return; }