ts-onvif
Version:
Client to ONVIF devices
390 lines • 12.4 kB
JavaScript
;
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