UNPKG

onvif-nvt

Version:

Wrapper for ONVIF spec to control NVT (Network Video Transitter) devices.

385 lines (321 loc) 10.7 kB
const Xml2js = require('xml2js'); const Crypto = require('crypto'); const Save = require('./save-xml'); const Request = require('request'); const Config = require('./config'); class Soap { constructor() { this.username = ''; this.password = ''; this.HTTP_TIMEOUT = 3000; } parse(soap) { const promise = new Promise((resolve, reject) => { const prefix = soap.substring(0, 2); if (prefix === '--') { resolve({ raw: true, soap }); } else { const opts = { explicitRoot: false, explicitArray: false, ignoreAttrs: false, tagNameProcessors: [function (name) { const m = name.match(/^([^\:]+)\:([^\:]+)$/); return m ? m[2] : name; }] }; Xml2js.parseString(soap, opts, (error, results) => { if (error) { error.soap = soap; reject(error); } else { resolve({ parsed: results, soap }); } }); } }); return promise; } 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 => { 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: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>'; soap = soap.replace(/\>\s+\</g, '><'); return soap; } makeRequest(service, serviceAddress, methodName, soapEnvelope, params) { const promise = new Promise((resolve, reject) => { Save.saveXml(service, methodName + '.Request', soapEnvelope); let xml = ''; return this.runRequest(service, serviceAddress, methodName, soapEnvelope).then(results => { xml = results; return this.parse(xml); }).then(results => { if ('raw' in results) { Save.saveXml(service, methodName + '.Response', results.soap); resolve(results); return; } const fault = this.getFault(results.parsed); if (fault) { Save.saveXml(service, methodName + '.Error', xml); const err = new Error(`${methodName}`); err.fault = fault; err.soap = xml; reject(err); } else { const parsed = this.parseResponse(methodName, results.parsed); if (parsed) { const res = { soap: xml, schemas: results.parsed.$ ? results.parsed.$ : '', data: parsed }; Save.saveXml(service, methodName + '.Response', xml); resolve(res); } else { const err = new Error(methodName + ':The device seems to not support the ' + methodName + '() method.'); reject(err); } } }).catch(error => { reject(error); }); }); return promise; } runRequest(service, serviceAddress, methodName, soapEnvelope) { return new Promise((resolve, reject) => { if (Config.isTest()) { const Fs = require('fs'); const Path = require('path'); const testCameraType = Config.getCameraType(); const testService = service; let filePath = Path.resolve(__dirname, `../../test/data/xml/${testCameraType}/${testService}/${methodName}.Response.xml`); if (!Fs.existsSync(filePath)) { filePath = Path.resolve(__dirname, `../../test/data/xml/${testCameraType}/${testService}/${methodName}.Error.xml`); } if (!Fs.existsSync(filePath)) { throw new Error(`File does not exist for test: ${filePath}`); } const xml = Fs.readFileSync(filePath, 'utf8'); resolve(xml); } else { const options = { method: 'POST', uri: serviceAddress.href, 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(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; } } getFault(results) { let fault = ''; if ('Fault' in results.Body) { const bodyFault = results.Body.Fault; const r1 = this.parseForReason(bodyFault); const c1 = this.parseForCode(bodyFault); const d1 = this.parseForDetail(bodyFault); fault = {}; fault.reason = r1; fault.code = c1; fault.detail = d1; } return fault; } 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; } createUserToken(diff, user, pass) { if (!diff) { diff = 0; } if (!pass) { pass = ''; } const created = new Date(Date.now() + diff).toISOString(); const expires = new Date(Date.now() + diff + 10000).toISOString(); const nonceBuffer = this.createNonce(16); const nonceBase64 = nonceBuffer.toString('base64'); const shasum = Crypto.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(':'); xml = '<SubscriptionId s:mustUnderstand="1" s:IsReferenceParameter="1" ' + tags[0] + '="' + url + '">' + id + '</SubscriptionId>'; console.log(xml); } } return xml; } } return null; } } module.exports = Soap;