UNPKG

aws-crt

Version:

NodeJS/browser bindings to the aws-c-* libraries

561 lines (448 loc) 24.5 kB
/** * @packageDocumentation * @module mqtt5 */ import * as mqtt from "mqtt"; import * as mqtt_shared from "../common/mqtt_shared"; import * as mqtt5 from "./mqtt5"; import { CrtError } from "./error"; export const MAXIMUM_VARIABLE_LENGTH_INTEGER : number= 268435455; export const MAXIMUM_PACKET_SIZE : number = 5 + MAXIMUM_VARIABLE_LENGTH_INTEGER; export const DEFAULT_RECEIVE_MAXIMUM : number = 65535; export const DEFAULT_CONNECT_TIMEOUT_MS : number = 30000; export const DEFAULT_MIN_RECONNECT_DELAY_MS : number = 1000; export const DEFAULT_MAX_RECONNECT_DELAY_MS : number = 120000; export const DEFAULT_MIN_CONNECTED_TIME_TO_RESET_RECONNECT_DELAY_MS : number = 30000; /** @internal */ function set_defined_property(object: any, propertyName: string, value: any) : boolean { if (value === undefined || value == null) { return false; } object[propertyName] = value; return true; } /** @internal */ export function transform_mqtt_js_connack_to_crt_connack(mqtt_js_connack: mqtt.IConnackPacket) : mqtt5.ConnackPacket { if (mqtt_js_connack == null || mqtt_js_connack == undefined) { throw new CrtError("transform_mqtt_js_connack_to_crt_connack: mqtt_js_connack not defined"); } let connack : mqtt5.ConnackPacket = { type: mqtt5.PacketType.Connack, sessionPresent: mqtt_js_connack.sessionPresent, reasonCode : mqtt_js_connack.reasonCode ?? mqtt5.ConnectReasonCode.Success }; set_defined_property(connack, "sessionExpiryInterval", mqtt_js_connack.properties?.sessionExpiryInterval); set_defined_property(connack, "receiveMaximum", mqtt_js_connack.properties?.receiveMaximum); set_defined_property(connack, "maximumQos", mqtt_js_connack.properties?.maximumQoS); set_defined_property(connack, "retainAvailable", mqtt_js_connack.properties?.retainAvailable); set_defined_property(connack, "maximumPacketSize", mqtt_js_connack.properties?.maximumPacketSize); set_defined_property(connack, "assignedClientIdentifier", mqtt_js_connack.properties?.assignedClientIdentifier); set_defined_property(connack, "topicAliasMaximum", mqtt_js_connack.properties?.topicAliasMaximum); set_defined_property(connack, "reasonString", mqtt_js_connack.properties?.reasonString); set_defined_property(connack, "userProperties", transform_mqtt_js_user_properties_to_crt_user_properties(mqtt_js_connack.properties?.userProperties)); set_defined_property(connack, "wildcardSubscriptionsAvailable", mqtt_js_connack.properties?.wildcardSubscriptionAvailable); set_defined_property(connack, "subscriptionIdentifiersAvailable", mqtt_js_connack.properties?.subscriptionIdentifiersAvailable); set_defined_property(connack, "sharedSubscriptionsAvailable", mqtt_js_connack.properties?.sharedSubscriptionAvailable); set_defined_property(connack, "serverKeepAlive", mqtt_js_connack.properties?.serverKeepAlive); set_defined_property(connack, "responseInformation", mqtt_js_connack.properties?.responseInformation); set_defined_property(connack, "serverReference", mqtt_js_connack.properties?.serverReference); return connack; } /** @internal */ export function create_negotiated_settings(config : mqtt5.Mqtt5ClientConfig, connack: mqtt5.ConnackPacket) : mqtt5.NegotiatedSettings { if (config == null || config == undefined) { throw new CrtError("create_negotiated_settings: config not defined"); } if (connack == null || connack == undefined) { throw new CrtError("create_negotiated_settings: connack not defined"); } return { maximumQos: Math.min(connack.maximumQos ?? mqtt5.QoS.ExactlyOnce, mqtt5.QoS.AtLeastOnce), sessionExpiryInterval: connack.sessionExpiryInterval ?? config.connectProperties?.sessionExpiryIntervalSeconds ?? 0, receiveMaximumFromServer: connack.receiveMaximum ?? DEFAULT_RECEIVE_MAXIMUM, maximumPacketSizeToServer: connack.maximumPacketSize ?? MAXIMUM_PACKET_SIZE, serverKeepAlive: connack.serverKeepAlive ?? config.connectProperties?.keepAliveIntervalSeconds ?? mqtt_shared.DEFAULT_KEEP_ALIVE, retainAvailable: connack.retainAvailable ?? true, wildcardSubscriptionsAvailable: connack.wildcardSubscriptionsAvailable ?? true, subscriptionIdentifiersAvailable: connack.subscriptionIdentifiersAvailable ?? true, sharedSubscriptionsAvailable: connack.sharedSubscriptionsAvailable ?? true, rejoinedSession: connack.sessionPresent, clientId: connack.assignedClientIdentifier ?? config.connectProperties?.clientId ?? "" }; } /** @internal */ function create_mqtt_js_will_from_crt_config(connectProperties? : mqtt5.ConnectPacket) : any { if (!connectProperties || !connectProperties.will) { return undefined; } let crtWill : mqtt5.PublishPacket = connectProperties.will; let hasWillProperties : boolean = false; let willProperties : any = {}; hasWillProperties = set_defined_property(willProperties, "willDelayInterval", connectProperties.willDelayIntervalSeconds) || hasWillProperties; if (crtWill.payloadFormat !== undefined) { hasWillProperties = set_defined_property(willProperties, "payloadFormatIndicator", crtWill.payloadFormat == mqtt5.PayloadFormatIndicator.Utf8) || hasWillProperties; } hasWillProperties = set_defined_property(willProperties, "messageExpiryInterval", crtWill.messageExpiryIntervalSeconds) || hasWillProperties; hasWillProperties = set_defined_property(willProperties, "contentType", crtWill.contentType) || hasWillProperties; hasWillProperties = set_defined_property(willProperties, "responseTopic", crtWill.responseTopic) || hasWillProperties; hasWillProperties = set_defined_property(willProperties, "correlationData", crtWill.correlationData) || hasWillProperties; hasWillProperties = set_defined_property(willProperties, "userProperties", transform_crt_user_properties_to_mqtt_js_user_properties(crtWill.userProperties)) || hasWillProperties; let will : any = { topic: crtWill.topicName, payload: crtWill.payload ?? "", qos: crtWill.qos, retain: crtWill.retain ?? false }; if (hasWillProperties) { will["properties"] = willProperties; } return will; } /** @internal */ export function getOrderedReconnectDelayBounds(configMin?: number, configMax?: number) : [number, number] { const minDelay : number = Math.max(1, configMin ?? DEFAULT_MIN_RECONNECT_DELAY_MS); const maxDelay : number = Math.max(1, configMax ?? DEFAULT_MAX_RECONNECT_DELAY_MS); if (minDelay > maxDelay) { return [maxDelay, minDelay]; } else { return [minDelay, maxDelay]; } } /** @internal */ function should_mqtt_js_use_clean_start(session_behavior? : mqtt5.ClientSessionBehavior) : boolean { return session_behavior !== mqtt5.ClientSessionBehavior.RejoinPostSuccess && session_behavior !== mqtt5.ClientSessionBehavior.RejoinAlways; } /** @internal */ export function compute_mqtt_js_reconnect_delay_from_crt_max_delay(maxReconnectDelayMs : number) : number { /* * This is an attempt to guarantee that the mqtt-js will never try to reconnect on its own and instead always * be controlled by our reconnection scheduler logic. */ return maxReconnectDelayMs * 2 + 60000; } function validate_required_uint16(propertyName : string, value: number) { if (value < 0 || value > 65535) { throw new CrtError(`Invalid value for property ${propertyName}: ` + value); } } function validate_optional_uint16(propertyName : string, value?: number) { if (value !== undefined) { validate_required_uint16(propertyName, value); } } function validate_required_uint32(propertyName : string, value: number) { if (value < 0 || value >= 4294967296) { throw new CrtError(`Invalid value for property ${propertyName}: ` + value); } } function validate_optional_uint32(propertyName : string, value?: number) { if (value !== undefined) { validate_required_uint32(propertyName, value); } } function validate_required_nonnegative_uint32(propertyName : string, value: number) { if (value <= 0 || value >= 4294967296) { throw new CrtError(`Invalid value for property ${propertyName}: ` + value); } } function validate_optional_nonnegative_uint32(propertyName : string, value?: number) { if (value !== undefined) { validate_required_nonnegative_uint32(propertyName, value); } } function validate_mqtt5_client_config(crtConfig : mqtt5.Mqtt5ClientConfig) { if (crtConfig == null || crtConfig == undefined) { throw new CrtError("validate_mqtt5_client_config: crtConfig not defined"); } validate_required_uint16("keepAliveIntervalSeconds", crtConfig.connectProperties?.keepAliveIntervalSeconds ?? 0); validate_optional_uint32("sessionExpiryIntervalSeconds", crtConfig.connectProperties?.sessionExpiryIntervalSeconds); validate_optional_uint16("receiveMaximum", crtConfig.connectProperties?.receiveMaximum); validate_optional_nonnegative_uint32("maximumPacketSizeBytes", crtConfig.connectProperties?.maximumPacketSizeBytes); validate_optional_uint32("willDelayIntervalSeconds", crtConfig.connectProperties?.willDelayIntervalSeconds); } /** @internal */ export function create_mqtt_js_client_config_from_crt_client_config(crtConfig : mqtt5.Mqtt5ClientConfig) : mqtt.IClientOptions { validate_mqtt5_client_config(crtConfig); let [_, maxDelay] = getOrderedReconnectDelayBounds(crtConfig.minReconnectDelayMs, crtConfig.maxReconnectDelayMs); maxDelay = compute_mqtt_js_reconnect_delay_from_crt_max_delay(maxDelay); let mqttJsClientConfig : mqtt.IClientOptions = { protocolVersion: 5, keepalive: crtConfig.connectProperties?.keepAliveIntervalSeconds ?? mqtt_shared.DEFAULT_KEEP_ALIVE, connectTimeout: crtConfig.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS, clean: should_mqtt_js_use_clean_start(crtConfig.sessionBehavior), reconnectPeriod: maxDelay, queueQoSZero : false, // @ts-ignore autoUseTopicAlias : false, // @ts-ignore autoAssignTopicAlias : false, transformWsUrl: undefined, /* TODO */ resubscribe : false }; /* * If you leave clientId undefined, mqtt-js will make up some weird thing for you, but the intent is that it * should pass the empty client id so that the server assigns you one. */ set_defined_property(mqttJsClientConfig, "clientId", crtConfig.connectProperties?.clientId ?? ""); set_defined_property(mqttJsClientConfig, "username", crtConfig.connectProperties?.username); set_defined_property(mqttJsClientConfig, "password", crtConfig.connectProperties?.password); set_defined_property(mqttJsClientConfig, "will", create_mqtt_js_will_from_crt_config(crtConfig.connectProperties)); let hasProperties : boolean = false; let properties: any = {}; hasProperties = set_defined_property(properties, "sessionExpiryInterval", crtConfig.connectProperties?.sessionExpiryIntervalSeconds) || hasProperties; hasProperties = set_defined_property(properties, "receiveMaximum", crtConfig.connectProperties?.receiveMaximum) || hasProperties; hasProperties = set_defined_property(properties, "maximumPacketSize", crtConfig.connectProperties?.maximumPacketSizeBytes) || hasProperties; hasProperties = set_defined_property(properties, "requestResponseInformation", crtConfig.connectProperties?.requestResponseInformation) || hasProperties; hasProperties = set_defined_property(properties, "requestProblemInformation", crtConfig.connectProperties?.requestProblemInformation) || hasProperties; hasProperties = set_defined_property(properties, "userProperties", transform_crt_user_properties_to_mqtt_js_user_properties(crtConfig.connectProperties?.userProperties)) || hasProperties; if (hasProperties) { mqttJsClientConfig["properties"] = properties; } return mqttJsClientConfig; } /** @internal */ export function transform_crt_user_properties_to_mqtt_js_user_properties(userProperties?: mqtt5.UserProperty[]) : mqtt.UserProperties | undefined { if (!userProperties) { return undefined; } /* * More restricted version of mqtt.UserProperties so that we can have type-checking but don't need to handle * the non-array case. */ let mqttJsProperties : {[key : string] : string[] } = {}; for (const property of userProperties) { const key : string = property.name; if (!(key in mqttJsProperties)) { mqttJsProperties[key] = []; } mqttJsProperties[key].push(property.value); } return mqttJsProperties; } /** @internal */ export function transform_mqtt_js_user_properties_to_crt_user_properties(userProperties?: mqtt.UserProperties) : mqtt5.UserProperty[] | undefined { if (!userProperties) { return undefined; } let crtProperties : mqtt5.UserProperty[] | undefined = undefined; for (const [propName, propValue] of Object.entries(userProperties)) { let values : string[] = (typeof propValue === 'string') ? [propValue] : propValue; for (const valueIter of values) { let propertyEntry = {name : propName, value : valueIter}; if (!crtProperties) { crtProperties = [propertyEntry]; } else { crtProperties.push(propertyEntry); } } } return crtProperties; } function validate_crt_disconnect(disconnect: mqtt5.DisconnectPacket) { if (disconnect == null || disconnect == undefined) { throw new CrtError("validate_crt_disconnect: disconnect not defined"); } validate_optional_uint32("sessionExpiryIntervalSeconds", disconnect.sessionExpiryIntervalSeconds); } /** @internal */ export function transform_crt_disconnect_to_mqtt_js_disconnect(disconnect: mqtt5.DisconnectPacket) : mqtt.IDisconnectPacket { validate_crt_disconnect(disconnect); let properties = {}; let propertiesValid : boolean = false; propertiesValid = set_defined_property(properties, "sessionExpiryInterval", disconnect.sessionExpiryIntervalSeconds) || propertiesValid; propertiesValid = set_defined_property(properties, "reasonString", disconnect.reasonString) || propertiesValid; propertiesValid = set_defined_property(properties, "userProperties", transform_crt_user_properties_to_mqtt_js_user_properties(disconnect.userProperties)) || propertiesValid; propertiesValid = set_defined_property(properties, "serverReference", disconnect.serverReference) || propertiesValid; let mqttJsDisconnect : mqtt.IDisconnectPacket = { cmd: 'disconnect', reasonCode : disconnect.reasonCode }; if (propertiesValid) { mqttJsDisconnect["properties"] = properties; } return mqttJsDisconnect; } /** @internal **/ export function transform_mqtt_js_disconnect_to_crt_disconnect(disconnect: mqtt.IDisconnectPacket) : mqtt5.DisconnectPacket { if (disconnect == null || disconnect == undefined) { throw new CrtError("transform_mqtt_js_disconnect_to_crt_disconnect: disconnect not defined"); } let crtDisconnect : mqtt5.DisconnectPacket = { type: mqtt5.PacketType.Disconnect, reasonCode : disconnect.reasonCode ?? mqtt5.DisconnectReasonCode.NormalDisconnection }; set_defined_property(crtDisconnect, "sessionExpiryIntervalSeconds", disconnect.properties?.sessionExpiryInterval); set_defined_property(crtDisconnect, "reasonString", disconnect.properties?.reasonString); set_defined_property(crtDisconnect, "userProperties", transform_mqtt_js_user_properties_to_crt_user_properties(disconnect.properties?.userProperties)); set_defined_property(crtDisconnect, "serverReference", disconnect.properties?.serverReference); return crtDisconnect; } function validate_crt_subscribe(subscribe: mqtt5.SubscribePacket) { if (subscribe == null || subscribe == undefined) { throw new CrtError("validate_crt_subscribe: subscribe not defined"); } validate_optional_uint32("subscriptionIdentifier", subscribe.subscriptionIdentifier); } /** @internal **/ export function transform_crt_subscribe_to_mqtt_js_subscription_map(subscribe: mqtt5.SubscribePacket) : mqtt.ISubscriptionMap { validate_crt_subscribe(subscribe); let subscriptionMap : mqtt.ISubscriptionMap = {}; for (const subscription of subscribe.subscriptions) { let mqttJsSub = { qos: subscription.qos, nl : subscription.noLocal ?? false, rap: subscription.retainAsPublished ?? false, rh: subscription.retainHandlingType ?? mqtt5.RetainHandlingType.SendOnSubscribe }; subscriptionMap[subscription.topicFilter] = mqttJsSub; } return subscriptionMap; } /** @internal **/ export function transform_crt_subscribe_to_mqtt_js_subscribe_options(subscribe: mqtt5.SubscribePacket) : mqtt.IClientSubscribeOptions { let properties = {}; let propertiesValid : boolean = false; if (subscribe == null || subscribe == undefined) { throw new CrtError("transform_crt_subscribe_to_mqtt_js_subscribe_options: subscribe not defined"); } propertiesValid = set_defined_property(properties, "subscriptionIdentifier", subscribe.subscriptionIdentifier) || propertiesValid; propertiesValid = set_defined_property(properties, "userProperties", transform_crt_user_properties_to_mqtt_js_user_properties(subscribe.userProperties)) || propertiesValid; let options : mqtt.IClientSubscribeOptions = { qos: 0 } if (propertiesValid) { options["properties"] = properties; } return options; } /** @internal **/ export function transform_mqtt_js_subscription_grants_to_crt_suback(subscriptionsGranted: mqtt.ISubscriptionGrant[]) : mqtt5.SubackPacket { if (subscriptionsGranted == null || subscriptionsGranted == undefined) { throw new CrtError("transform_mqtt_js_subscription_grants_to_crt_suback: subscriptionsGranted not defined"); } let crtSuback : mqtt5.SubackPacket = { type: mqtt5.PacketType.Suback, reasonCodes : subscriptionsGranted.map((subscription: mqtt.ISubscriptionGrant, index: number, array : mqtt.ISubscriptionGrant[]) : mqtt5.SubackReasonCode => { return subscription.qos; }) } /* * TODO: mqtt-js does not expose the suback packet to subscribe's completion callback, so we cannot extract * reasonString and userProperties atm. * * Revisit if this changes. */ return crtSuback; } function validate_crt_publish(publish: mqtt5.PublishPacket) { if (publish == null || publish == undefined) { throw new CrtError("validate_crt_publish: publish not defined"); } validate_optional_uint32("messageExpiryIntervalSeconds", publish.messageExpiryIntervalSeconds); } /** @internal */ export function transform_crt_publish_to_mqtt_js_publish_options(publish: mqtt5.PublishPacket) : mqtt.IClientPublishOptions { validate_crt_publish(publish); let properties = {}; let propertiesValid : boolean = false; if (publish.payloadFormat !== undefined) { propertiesValid = set_defined_property(properties, "payloadFormatIndicator", publish.payloadFormat == mqtt5.PayloadFormatIndicator.Utf8) || propertiesValid; } propertiesValid = set_defined_property(properties, "messageExpiryInterval", publish.messageExpiryIntervalSeconds) || propertiesValid; propertiesValid = set_defined_property(properties, "responseTopic", publish.responseTopic) || propertiesValid; propertiesValid = set_defined_property(properties, "correlationData", publish.correlationData) || propertiesValid; propertiesValid = set_defined_property(properties, "userProperties", transform_crt_user_properties_to_mqtt_js_user_properties(publish.userProperties)) || propertiesValid; propertiesValid = set_defined_property(properties, "contentType", publish.contentType) || propertiesValid; let mqttJsPublish : mqtt.IClientPublishOptions = { qos: publish.qos, retain: publish.retain ?? false, }; if (propertiesValid) { mqttJsPublish["properties"] = properties; } return mqttJsPublish; } /** @internal **/ export function transform_mqtt_js_publish_to_crt_publish(publish: mqtt.IPublishPacket) : mqtt5.PublishPacket { if (publish == null || publish == undefined) { throw new CrtError("transform_mqtt_js_publish_to_crt_publish: publish not defined"); } let crtPublish : mqtt5.PublishPacket = { type: mqtt5.PacketType.Publish, qos: publish.qos, retain: publish.retain, topicName: publish.topic, payload: publish.payload }; if (publish.properties) { if (publish.properties.payloadFormatIndicator !== undefined) { set_defined_property(crtPublish, "payloadFormat", publish.properties.payloadFormatIndicator ? mqtt5.PayloadFormatIndicator.Utf8 : mqtt5.PayloadFormatIndicator.Bytes); } set_defined_property(crtPublish, "messageExpiryIntervalSeconds", publish.properties?.messageExpiryInterval); set_defined_property(crtPublish, "responseTopic", publish.properties?.responseTopic); set_defined_property(crtPublish, "correlationData", publish.properties?.correlationData); set_defined_property(crtPublish, "userProperties", transform_mqtt_js_user_properties_to_crt_user_properties(publish.properties?.userProperties)); set_defined_property(crtPublish, "contentType", publish.properties?.contentType); let subIds : number | number[] | undefined = publish.properties?.subscriptionIdentifier; let subIdsType : string = typeof subIds; if (subIds) { if (subIdsType == 'number') { crtPublish["subscriptionIdentifiers"] = [subIds]; } else if (Array.isArray(subIds)) { crtPublish["subscriptionIdentifiers"] = subIds; } } } return crtPublish; } /** @internal **/ export function transform_mqtt_js_puback_to_crt_puback(puback: mqtt.IPubackPacket) : mqtt5.PubackPacket { if (puback == null || puback == undefined) { throw new CrtError("transform_mqtt_js_puback_to_crt_puback: puback not defined"); } let crtPuback : mqtt5.PubackPacket = { type: mqtt5.PacketType.Puback, reasonCode: puback.reasonCode ?? mqtt5.PubackReasonCode.Success, }; if (puback.properties) { set_defined_property(crtPuback, "reasonString", puback.properties?.reasonString); set_defined_property(crtPuback, "userProperties", transform_mqtt_js_user_properties_to_crt_user_properties(puback.properties?.userProperties)); } return crtPuback; } /** @internal **/ export function transform_crt_unsubscribe_to_mqtt_js_unsubscribe_options(unsubscribe: mqtt5.UnsubscribePacket) : Object { if (unsubscribe == null || unsubscribe == undefined) { throw new CrtError("transform_crt_unsubscribe_to_mqtt_js_unsubscribe_options: unsubscribe not defined"); } let properties = {}; let propertiesValid : boolean = false; propertiesValid = set_defined_property(properties, "userProperties", transform_crt_user_properties_to_mqtt_js_user_properties(unsubscribe.userProperties)); let options : any = {}; if (propertiesValid) { options["properties"] = properties; } return options; } /** @internal **/ export function transform_mqtt_js_unsuback_to_crt_unsuback(packet: mqtt.IUnsubackPacket) : mqtt5.UnsubackPacket { if (packet == null || packet == undefined) { throw new CrtError("transform_mqtt_js_unsuback_to_crt_unsuback: packet not defined"); } let reasonCodes : number | number[] | undefined = packet.reasonCode; let codes : number[]; if (Array.isArray(reasonCodes)) { codes = reasonCodes; } else if (typeof reasonCodes == 'number') { codes = [reasonCodes]; } else { codes = []; } let crtUnsuback : mqtt5.UnsubackPacket = { type: mqtt5.PacketType.Unsuback, reasonCodes : codes } if (packet.properties) { set_defined_property(crtUnsuback, "reasonString", packet.properties?.reasonString); set_defined_property(crtUnsuback, "userProperties", transform_mqtt_js_user_properties_to_crt_user_properties(packet.properties?.userProperties)); } return crtUnsuback; }