UNPKG

onvif-nvt-ts

Version:

Wrapper for ONVIF spec to control NVT (Network Video Transitter) devices. Forked and added TypeScript support from onvif-nvt.

406 lines 15.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const crypto_1 = __importDefault(require("crypto")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const request_1 = __importDefault(require("request")); const xml2js_1 = __importDefault(require("xml2js")); const Save = require('./save-xml'); const Config = require('./config'); /** * SOAP management (sending, receiving, parsing) class for ONVIF modules. */ class Soap { constructor() { this.username = ''; this.password = ''; this.HTTP_TIMEOUT = 3000; } /** * Internal method for parsing SOAP responses. * @param soap The XML to parse. */ parse(soap) { return new Promise((resolve, reject) => { const prefix = soap.substring(0, 2); if (prefix === '--') { // this is a multi-part xml with attachment // it is up to the receiver to parse. This is // usually from GetSystemLog and the binary // part could be anything. Reference your // camera specs for dealing with it. resolve({ raw: true, soap }); } else { const opts = { explicitRoot: false, explicitArray: false, // 'ignoreAttrs' : true, ignoreAttrs: false, tagNameProcessors: [ function (name) { // strip namespaces /* eslint-disable no-useless-escape */ const m = name.match(/^([^\:]+)\:([^\:]+)$/); /* eslint-enable no-useless-escape */ return m ? m[2] : name; } ] }; // console.log(soap) xml2js_1.default.parseString(soap, opts, (error, results) => { if (error) { error['soap'] = soap; reject(error); } else { resolve({ parsed: results, soap }); } }); } }); } /** * Internal method used by the module classes. * @param params Object containing required parameters to create a SOAP request. * @param params.body Description in the &lt;s:Body&gt; of the generated xml. * @param params.xmlns A list of xmlns attributes used in the body * e.g., xmlns:tds="http://www.onvif.org/ver10/device/wsdl". * @param params.diff Time difference [ms]. * @param params.username The user name. * @param params.password The user Password. * @param params.subscriptionId */ createRequest(params) { let soap = ''; soap += '<?xml version="1.0" encoding="UTF-8"?>'; soap += '<s:Envelope'; soap += ' xmlns:s="http://www.w3.org/2003/05/soap-envelope"'; soap += ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'; soap += ' xmlns:xsd="http://www.w3.org/2001/XMLSchema"'; soap += ' xmlns:wsa5="http://www.w3.org/2005/08/addressing"'; soap += ' xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"'; soap += ' xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"'; soap += ' xmlns:tt="http://www.onvif.org/ver10/schema"'; soap += ' xmlns:ter="http://www.onvif.org/ver10/error"'; if (params.xmlns && Array.isArray(params.xmlns)) { params.xmlns.forEach(ns => { // make sure we don't add it twice (hikvision will fail) const index = soap.indexOf(ns); if (index < 0) { soap += ' ' + ns; } }); } soap += '>'; soap += '<s:Header>'; if (params.subscriptionId) { const address = this.getAddress(params.subscriptionId); if (address) { soap += '<wsa5:To s:mustUnderstand="true">'; soap += address; soap += '</wsa5:To>'; } } // soap += '<wsa5:Action s:mustUnderstand="1">' // soap += 'http://www.onvif.org/ver10/events/wsdl/EventPortType/CreatePullPointSubscriptionRequest' // soap += '</wsa5:Action>' // soap += '<wsa5:MessageID>' // soap += 'urn:uuid:cca999f8-b0e1-4e4e-ac7e-04a074d49fbf' // soap += '</wsa5:MessageID>' soap += '<wsa5:ReplyTo>'; soap += '<wsa5:Address>http://www.w3.org/2005/08/addressing/anonymous</wsa5:Address>'; soap += '</wsa5:ReplyTo>'; if (params.username) { this.username = params.username; this.password = params.password; soap += this.createUserToken(params.diff, params.username, params.password); } soap += '</s:Header>'; soap += '<s:Body>' + params.body + '</s:Body>'; soap += '</s:Envelope>'; return soap.replace(/\>\s+\</g, '><'); } /** * Internal method to send a SOAP request to the specified serviceAddress. * @param service The service name. * @param serviceAddress The service address. * @param methodName The request name. * @param soapEnvelope The request SOAP envelope. */ async makeRequest(service, serviceAddress, methodName, soapEnvelope) { Save.saveXml(service, methodName + '.Request', soapEnvelope); const xml = await this.runRequest(service, serviceAddress, methodName, soapEnvelope); const response = await this.parse(xml); if (response.raw) { Save.saveXml(service, methodName + '.Response', response.soap); return response; } const fault = this.getFault(response.parsed); if (fault) { Save.saveXml(service, methodName + '.Error', xml); const err = new Error(`${methodName}`); err['fault'] = fault; err['soap'] = xml; throw err; } else { const parsed = this.parseResponse(methodName, response.parsed); if (parsed) { const res = { soap: xml, schemas: parsed.parsed.$ || '', data: parsed }; Save.saveXml(service, methodName + '.Response', xml); return res; } else { throw new Error(methodName + ':The device seems to not support the ' + methodName + '() method.'); } } } /** * Internal method to send a SOAP request. * @param service The service. * @param serviceAddress The service address. * @param methodName The request name. * @param soapEnvelope The request SOAP envelope. */ runRequest(service, serviceAddress, methodName, soapEnvelope) { return new Promise((resolve, reject) => { if (Config.isTest()) { // in testing mode (for Jest) const testCameraType = Config.getCameraType(); const testService = service; let filePath = path_1.default.resolve(__dirname, `../../test/data/xml/${testCameraType}/${testService}/${methodName}.Response.xml`); // see if the file exists if (!fs_1.default.existsSync(filePath)) { // see if there is an Error file filePath = path_1.default.resolve(__dirname, `../../test/data/xml/${testCameraType}/${testService}/${methodName}.Error.xml`); } if (!fs_1.default.existsSync(filePath)) { throw new Error(`File does not exist for test: ${filePath}`); } // it's good, read it in const xml = fs_1.default.readFileSync(filePath, 'utf8'); resolve(xml); } else { // some cameras enable HTTP digest or digest realm, // so using 'Request' to handle this for us. const options = { method: 'POST', uri: serviceAddress.href, // gzip: true, encoding: 'utf8', headers: { 'Content-Type': 'application/soap+xml; charset=utf-8;', 'Content-Length': Buffer.byteLength(soapEnvelope) }, body: soapEnvelope, auth: { user: this.username, pass: this.password, sendImmediately: false } }; request_1.default(options, (error, response, body) => { if (error) { console.error(error); reject(error); } else { if (response.statusCode === 200) { resolve(body); } else { reject(response); } } }); } }); } parseResponse(methodName, response) { const s0 = response.Body; if (!s0) { return null; } const responseName = methodName + 'Response'; if (responseName in s0) { return s0; } else { return null; } } /** * Parses results to see if there is a fault. * @param results The results of a communication with a server. */ getFault(results) { if (results.Body?.Fault) { const bodyFault = results.Body.Fault; const r1 = this.parseForReason(bodyFault); const c1 = this.parseForCode(bodyFault); const d1 = this.parseForDetail(bodyFault); return { reason: r1, code: c1, detail: d1 }; } } parseForCode(fault) { let code = ''; if ('Code' in fault) { const faultCode = fault.Code; if ('Value' in faultCode) { const faultValue = faultCode.Value; if ('Subcode' in faultCode) { const faultSubcode = faultCode.Subcode; if ('Value' in faultSubcode) { const faultSubcodeValue = faultSubcode.Value; code = faultValue + '|' + faultSubcodeValue; } else { code = faultSubcode; } } else { code = faultValue; } } } return code; } parseForDetail(fault) { let detail = ''; if ('Detail' in fault) { const faultDetail = fault.Detail; if ('Text' in faultDetail) { const faultText = faultDetail.Text; if (typeof faultText === 'string') { detail = faultText; } else if (typeof faultText === 'object' && '_' in faultText) { detail = faultText._; } } } return detail; } parseForReason(fault) { let reason = ''; if ('Reason' in fault) { const faultReason = fault.Reason; if ('Text' in faultReason) { const faultText = faultReason.Text; if (typeof faultText === 'string') { reason = faultText; } else if (typeof faultText === 'object' && '_' in faultText) { reason = faultText._; } } } else if ('faultstring' in fault) { reason = fault.faultstring; } return reason; } /** * Internal method used to create the user token xml. * @param diff The server timeDiff [ms]. * @param user The user name. * @param pass The user password. */ createUserToken(diff, user, pass) { if (!diff) { diff = 0; } if (!pass) { pass = ''; } const created = new Date(Date.now() + diff).toISOString(); // 10 second expiry const expires = new Date(Date.now() + diff + 10000).toISOString(); const nonceBuffer = this.createNonce(16); const nonceBase64 = nonceBuffer.toString('base64'); const shasum = crypto_1.default.createHash('sha1'); shasum.update(Buffer.concat([nonceBuffer, Buffer.from(created), Buffer.from(pass)])); const digest = shasum.digest('base64'); let soap = ''; soap += '<wsse:Security s:mustUnderstand="1">'; soap += ' <wsu:Timestamp wsu:Id="Time">'; soap += ' <wsu:Created>' + created + '</wsu:Created>'; soap += ' <wsu:Expires>' + expires + '</wsu:Expires>'; soap += ' </wsu:Timestamp>'; soap += ' <wsse:UsernameToken wsu:Id="User">'; soap += ' <wsse:Username>' + user + '</wsse:Username>'; soap += ' <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">' + digest + '</wsse:Password>'; soap += ' <wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">' + nonceBase64 + '</wsse:Nonce>'; soap += ' <wsu:Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">' + created + '</wsu:Created>'; soap += ' </wsse:UsernameToken>'; soap += '</wsse:Security>'; return soap; } createNonce(digit) { const nonce = Buffer.alloc(digit); for (let i = 0; i < digit; i++) { nonce.writeUInt8(Math.floor(Math.random() * 256), i); } return nonce; } getAddress(subscriptionid) { if (subscriptionid) { if (subscriptionid.Address) { return subscriptionid.Address; } } return null; } getCustomSubscriptionIdXml(subscriptionId) { if (subscriptionId) { if (subscriptionId._) { const id = subscriptionId._; let xml = null; if (subscriptionId.$) { const keys = Object.keys(subscriptionId.$); const tag = keys[0]; const url = subscriptionId.$[tag]; if (id && tag && url) { const tags = tag.split(':'); // don't need Action tag xml = '<SubscriptionId s:mustUnderstand="1" s:IsReferenceParameter="1" ' + tags[0] + '="' + url + '">' + id + '</SubscriptionId>'; console.log(xml); } } return xml; } } return null; } } exports.default = Soap; //# sourceMappingURL=soap.js.map