UNPKG

unifi-protect

Version:

A complete implementation of the UniFi Protect API.

122 lines 6.67 kB
/* Copyright(C) 2019-2026, HJD (https://github.com/hjdhjd). All rights reserved. * * protect-api-events.ts: Our UniFi Protect realtime events API packet utilities. */ import { promisify } from "node:util"; import zlib from "node:zlib"; // Async zlib inflate for non-blocking decompression on the libuv threadpool. const inflateAsync = promisify(zlib.inflate); // UniFi Protect events API packet header size, in bytes. const EVENT_PACKET_HEADER_SIZE = 8; // Update realtime API packet types. var ProtectEventPacketType; (function (ProtectEventPacketType) { ProtectEventPacketType[ProtectEventPacketType["ACTION"] = 1] = "ACTION"; ProtectEventPacketType[ProtectEventPacketType["DATA"] = 2] = "DATA"; })(ProtectEventPacketType || (ProtectEventPacketType = {})); // Update realtime API payload types. var EventPayloadType; (function (EventPayloadType) { EventPayloadType[EventPayloadType["JSON"] = 1] = "JSON"; EventPayloadType[EventPayloadType["STRING"] = 2] = "STRING"; EventPayloadType[EventPayloadType["BUFFER"] = 3] = "BUFFER"; })(EventPayloadType || (EventPayloadType = {})); /** * @internal * * A packet header is composed of 8 bytes in this order: * * | Byte Offset | Description | Bits | Values | * |-------------|----------------|------|-----------------------------------------------------------------------| * | 0 | Packet Type | 8 | 1 - action frame, 2 - data frame. | * | 1 | Payload Format | 8 | 1 - JSON object, 2 - UTF8-encoded string, 3 - Node Buffer. | * | 2 | Deflated | 8 | 0 - uncompressed, 1 - compressed / deflated (zlib-based compression). | * | 3 | Unknown | 8 | Always 0. Possibly reserved for future use by Ubiquiti? | * | 4-7 | Payload Size: | 32 | Size of payload in network-byte order (big endian). | */ var ProtectEventPacketHeader; (function (ProtectEventPacketHeader) { ProtectEventPacketHeader[ProtectEventPacketHeader["TYPE"] = 0] = "TYPE"; ProtectEventPacketHeader[ProtectEventPacketHeader["PAYLOAD_FORMAT"] = 1] = "PAYLOAD_FORMAT"; ProtectEventPacketHeader[ProtectEventPacketHeader["DEFLATED"] = 2] = "DEFLATED"; ProtectEventPacketHeader[ProtectEventPacketHeader["UNKNOWN"] = 3] = "UNKNOWN"; ProtectEventPacketHeader[ProtectEventPacketHeader["PAYLOAD_SIZE"] = 4] = "PAYLOAD_SIZE"; })(ProtectEventPacketHeader || (ProtectEventPacketHeader = {})); /** * Decode a UniFi Protect event packet. * * @param log - Logging functions to use. * @param packet - Input packet to decode. * * @returns Promise resolving to a decoded event packet, or `null` if decoding fails. * * @remarks A UniFi Protect event packet is an encoded representation of state updates that occur in a UniFi Protect controller. This utility function takes an * encoded packet as an input, and decodes it into an event header and payload that can be acted upon. An example of its use is in {@link ProtectApi} where, once * successfully logged into the Protect controller, events are generated automatically and can be accessed by listening to `message` events emitted by * {@link ProtectApi}. */ export async function decodePacket(log, packet) { // What we need to do here is to split this packet into the header and payload, and decode them. let dataOffset; try { // The payload size begins at byte offset 4 as a big-endian 32-bit integer. When you add the payload size to our header frame size, you get the location of the // data header frame. dataOffset = packet.readUInt32BE(ProtectEventPacketHeader.PAYLOAD_SIZE) + EVENT_PACKET_HEADER_SIZE; // Validate our packet size, just in case we have more or less data than we expect. If we do, we're done for now. if (packet.length !== (dataOffset + EVENT_PACKET_HEADER_SIZE + packet.readUInt32BE(dataOffset + ProtectEventPacketHeader.PAYLOAD_SIZE))) { throw new Error("Packet length doesn't match header information."); } } catch (error) { log.error("Realtime events API: error decoding update packet: %s.", error); return null; } // Decode the action and payload frames in parallel. Both frames are independent and may require zlib decompression, so inflating them concurrently maximizes // throughput on the libuv threadpool. try { const [actionFrame, dataFrame] = await Promise.all([ decodeFrame(log, packet.subarray(0, dataOffset), ProtectEventPacketType.ACTION), decodeFrame(log, packet.subarray(dataOffset), ProtectEventPacketType.DATA) ]); if (!actionFrame || !dataFrame) { return null; } return ({ header: actionFrame, payload: dataFrame }); } catch (error) { log.error("Realtime events API: error decoding update packet: %s.", error); return null; } } // Decode a frame, composed of a header and payload, received through the update events API. async function decodeFrame(log, packet, packetType) { // Read the packet frame type. const frameType = packet.readUInt8(ProtectEventPacketHeader.TYPE); // This isn't the frame type we were expecting - we're done. if (packetType !== frameType) { return null; } // Read the payload format. const payloadFormat = packet.readUInt8(ProtectEventPacketHeader.PAYLOAD_FORMAT); // Decompress the payload if the deflated flag is set, using async inflate to avoid blocking the event loop. For uncompressed payloads, we just slice past the header. const payload = packet.readUInt8(ProtectEventPacketHeader.DEFLATED) ? await inflateAsync(packet.subarray(EVENT_PACKET_HEADER_SIZE)) : packet.subarray(EVENT_PACKET_HEADER_SIZE); // If it's an action frame, it can only have one format. if (frameType === ProtectEventPacketType.ACTION) { return (payloadFormat === EventPayloadType.JSON) ? JSON.parse(payload.toString()) : null; } // Process the payload format accordingly. switch (payloadFormat) { case EventPayloadType.JSON: // If it's data payload, it can be anything. return JSON.parse(payload.toString()); case EventPayloadType.STRING: return payload.toString("utf8"); case EventPayloadType.BUFFER: return payload; default: log.error("Unknown payload packet type received in the realtime events API: %s.", payloadFormat); return null; } } //# sourceMappingURL=protect-api-events.js.map