UNPKG

ts-onvif

Version:

Client to ONVIF devices

390 lines 12.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OnvifError = void 0; exports.linerase = linerase; exports.guid = guid; exports.splitArgs = splitArgs; exports.camelCase = camelCase; exports.parseSOAPString = parseSOAPString; exports.struct = struct; exports.build = build; exports.toIsoDuration = toIsoDuration; exports.toMs = toMs; exports.getDigestHeaders = getDigestHeaders; exports.formatXMLValues = formatXMLValues; const fast_xml_parser_1 = require("fast-xml-parser"); const toOnvifXMLSchemaObject_1 = require("./utils/toOnvifXMLSchemaObject"); const NUMBER_RE = /^-?([1-9]\d*|0)(\.\d*)?$/; const DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(.\d+)?Z$/; const PREFIX_MATCH_RE = /(?!xmlns)^.*:/; const ISO_DURATION_RE = /^P(?:\d+Y)?(?:\d+M)?(?:\d+D)?(?:T(?=\d+)(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/; class OnvifError extends Error { xml; constructor(message, options) { super(message); this.name = 'OnvifError'; if (options) { this.xml = options.xml; } } } exports.OnvifError = OnvifError; /** * Parse SOAP object to pretty JS-object * @param xml xml2js object * @param options * @param options.array these tags will always be treated as arrays * @param options.rawXML values of these tags will be in xml2js format */ function linerase(xml, options = { array: [], rawXML: [] }) { if (options.rawXML === undefined) { options.rawXML = []; } /* if we have xs:any put it content to the Symbol.any */ if (options.rawXML.includes(options.name)) { if (options.array.includes(options.name)) { return xml.map((item) => linerase(item, { ...options, name: toOnvifXMLSchemaObject_1.xsany, rawXML: [toOnvifXMLSchemaObject_1.xsany] })); } if (Array.isArray(xml)) { [xml] = xml; } const rawXMLObject = linerase(xml, { ...options, rawXML: [] }); Object.defineProperty(rawXMLObject, toOnvifXMLSchemaObject_1.xsany, { value: xml, writable: true, enumerable: true, // false, configurable: true, }); return rawXMLObject; } if (Array.isArray(xml)) { /* trim empty nodes in xml ex.: <Node> </Node> becomes text node { node: ["\r\n"] }, this is not what we expected */ xml = xml.filter((item) => !(typeof item === 'string' && item.trim() === '')); if (xml.length === 1 && !options.array.includes(options.name) /* do not simplify array if its key in array prop */) { [xml] = xml; } else { return xml.map((item) => linerase(item, options)); } } if (typeof xml === 'object') { let obj = {}; Object.keys(xml).forEach((key) => { if (key === '$') { // for the xml attributes obj = { ...obj, ...linerase(xml.$, options), }; } else { obj[camelCase(key)] = linerase(xml[key], { ...options, name: camelCase(key) }); } }); return obj; } if (xml === 'true') { return true; } if (xml === 'false') { return false; } if (NUMBER_RE.test(xml)) { return parseFloat(xml); } if (DATE_RE.test(xml)) { return new Date(xml); } return xml; } function s4() { return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); } /** * Generate GUID */ function guid() { return `${s4() + s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; } /** * Split Digest authentication string */ function splitArgs(args) { let buffer = ''; const result = []; let quoteOpen = false; for (const i of args) { if (quoteOpen) { if (i === '"') { quoteOpen = false; } buffer += i; continue; } if (i === ',') { result.push(buffer.trim()); buffer = ''; } else { if (i === '"') { quoteOpen = true; } buffer += i; } } result.push(buffer.trim()); return result; } /** * @param tagName */ function camelCase(tagName) { const str = tagName.replace(PREFIX_MATCH_RE, ''); if (str.length === 1) { return str.toLowerCase(); } const secondLetter = str.charAt(1); if (secondLetter && secondLetter.toUpperCase() !== secondLetter) { return str.charAt(0).toLowerCase() + str.slice(1); } return str; } function toCamelCase(name) { const secondLetter = name.charAt(1); if (secondLetter && secondLetter.toUpperCase() !== secondLetter) { return name.charAt(0).toLowerCase() + name.slice(1); } return name; } function toPascalCase(name) { const secondLetter = name.charAt(1); if (secondLetter && secondLetter.toUpperCase() !== secondLetter) { return name.charAt(0).toUpperCase() + name.slice(1); } return name; } function parse(xml, options) { const xml2jsMode = options?.attributesGroupName === '$'; const parser = new fast_xml_parser_1.XMLParser({ ignoreAttributes: false, attributesGroupName: options?.attributesGroupName, attributeNamePrefix: options?.attributeNamePrefix ?? '', textNodeName: '_', parseTagValue: false, parseAttributeValue: false, trimValues: true, isArray: xml2jsMode ? (_tagName, _jPath, _isLeafNode, isAttribute) => !isAttribute : (tagName) => !!options?.array?.includes(tagName), removeNSPrefix: !xml2jsMode, ...(!xml2jsMode && { transformTagName: toCamelCase, transformAttributeName: toCamelCase, }), stopNodes: options?.rawXML?.map((tag) => `..${tag}`), }); return parser.parse(xml); } function hydrateStopNode(value, options) { if (Array.isArray(value)) { return value.map((item) => hydrateStopNode(item, options)); } const xmlToParse = `<root>${value._ ?? value}</root>`; const parsed = parse(xmlToParse, { array: options.array }).root || {}; const wrapped = parse(xmlToParse, { attributesGroupName: '$' }).root; let xsAnyParsed = (Array.isArray(wrapped) ? wrapped[0] : wrapped) || {}; if (typeof value === 'object') { Object.assign(parsed, value); delete parsed._; if (typeof xsAnyParsed !== 'object') { xsAnyParsed = { _: xsAnyParsed }; } const $ = {}; for (const [key, attrValue] of Object.entries(value)) { if (key !== '_') { $[toPascalCase(key)] = attrValue; } } if (Object.keys($).length) { xsAnyParsed.$ = { ...$, ...xsAnyParsed.$ }; } } formatXMLValues(parsed, { array: options.array }); parsed[toOnvifXMLSchemaObject_1.xsany] = xsAnyParsed; return parsed; } /** * Parse SOAP response * @param xml * @param options */ async function parseSOAPString(xml, options) { /* Filter out xml namespaces */ // const xml = rawXml.replace(/xmlns([^=]*?)=(".*?")/g, ''); const result = parse(xml, options); formatXMLValues(result, options); const body = result.envelope?.body; if (!body) { throw new OnvifError('Wrong ONVIF SOAP response, not a SOAP message, envelope and body are expected', { xml, }); } if (body.fault) { const fault = body.fault; let reason = ''; let detail = ''; try { const text = fault.reason.text; reason = (typeof text === 'object' ? text._ : text) || JSON.stringify(fault.code); } catch (_e) { // Ignore error if reason extraction fails } try { [detail] = fault.detail.text; } catch (_e) { // Ignore error if detail extraction fails } throw new Error(`ONVIF SOAP Fault: ${reason}${detail}`); } return [body, xml]; } /** * Create a record from the list where the key is commonly used parameter * For example, from the profiles array get an object where we can have rapid access to profile using its token * @param list * @param groupKey */ function struct(list, groupKey) { return Object.fromEntries(list.map((item) => [item[groupKey], item])); } // old builder with xml2js library // const builder = new xml2js.Builder({ // headless: true, // renderOpts: { // pretty: false, // }, // }); // // export function build(object: any) { // return builder.buildObject(object); // } const newBuilder = new fast_xml_parser_1.XMLBuilder({ ignoreAttributes: false, attributesGroupName: '$', attributeNamePrefix: '', textNodeName: '_', format: true, indentBy: ' ', }); function build(object) { return newBuilder.build(object); } /** * Use ISO duration or convert milliseconds to ISO duration * @param duration */ function toIsoDuration(duration) { if (typeof duration === 'string') { if (!ISO_DURATION_RE.test(duration)) { throw new Error(`"${duration}" is not a valid ISO duration value`); } return duration; } if (duration <= 0) return 'PT0S'; let totalSeconds = Math.floor(duration / 1000); const hours = Math.floor(totalSeconds / 3600); totalSeconds %= 3600; const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; let result = 'PT'; if (hours > 0) result += `${hours}H`; if (minutes > 0) result += `${minutes}M`; if (seconds > 0 || result === 'PT') result += `${seconds}S`; return result; } /** * Converts an ISO Duration (H, M, S) to milliseconds. * @param duration - The duration string (e.g., "PT5S", "PT1M", "PT1H30M") */ function toMs(duration) { if (typeof duration === 'number') { return duration; } // Matches strict time duration: T followed by Hours, Minutes, and/or Seconds (including decimals) const regex = /^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)(?:\.(\d+))?S)?$/; const matches = duration.match(regex); if (!matches) { throw new Error(`Invalid ISO Time Duration format: ${duration}`); } const hours = parseInt(matches[1]) || 0; const minutes = parseInt(matches[2]) || 0; const seconds = parseInt(matches[3]) || 0; const ms = matches[4] ? Math.round(parseFloat(`0.${matches[4]}`) * 1000) : 0; return hours * 3600000 + minutes * 60000 + seconds * 1000 + ms; } /** * Get Digest headers from headers array * @param headersArray */ function getDigestHeaders(headersArray) { const wwwAuthenticateArray = []; for (let x = 0; x < headersArray.length; x = x + 2) { if (headersArray[x].toLowerCase() === 'www-authenticate' && headersArray[x + 1].startsWith('Digest')) { wwwAuthenticateArray.push(headersArray[x + 1]); } } return wwwAuthenticateArray; } /** * Mutable function to convert string values to their appropriate types. * Tags in `rawXML` are re-parsed and get `__any__` as the xml2js object. */ function formatXMLValues(xml, options = {}) { const rawXML = options.rawXML ?? []; // if (Array.isArray(xml)) { // return xml.forEach((item) => formatXMLValues(item, options)); // } if (typeof xml === 'object' && xml !== null) { for (const [key, value] of Object.entries(xml)) { if (key === toOnvifXMLSchemaObject_1.xsany) { continue; } if (rawXML.includes(key)) { xml[key] = hydrateStopNode(value, options); continue; } if (value === 'true') { xml[key] = true; } if (value === 'false') { xml[key] = false; } if (typeof value === 'string') { if (NUMBER_RE.test(value)) { xml[key] = Number.parseFloat(value); } if (DATE_RE.test(value)) { xml[key] = new Date(value); } } if (typeof value === 'object') { formatXMLValues(value, options); } } } } //# sourceMappingURL=utils.js.map