UNPKG

node-onvif

Version:

The node-onvif is a Node.js module which allows you to communicate with the network camera which supports the ONVIF specifications.

323 lines (304 loc) 9.26 kB
/* ------------------------------------------------------------------ * node-onvif - soap.js * * Copyright (c) 2016-2018, Futomi Hatano, All rights reserved. * Released under the MIT license * Date: 2018-08-13 * ---------------------------------------------------------------- */ 'use strict'; const mXml2Js = require('xml2js'); const mHttp = require('http'); const mCrypto = require('crypto'); let mHtml = null; try { mHtml = require('html'); } catch(e) {} /* ------------------------------------------------------------------ * Constructor: OnvifSoap() * ---------------------------------------------------------------- */ function OnvifSoap() { this.HTTP_TIMEOUT = 3000; // ms } /* ------------------------------------------------------------------ * Method: parse(soap) * ---------------------------------------------------------------- */ OnvifSoap.prototype.parse = function(soap) { let promise = new Promise((resolve, reject) => { let opts = { 'explicitRoot' : false, 'explicitArray' : false, 'ignoreAttrs' : false, // Never change to `true` 'tagNameProcessors': [function(name) { let m = name.match(/^([^\:]+)\:([^\:]+)$/); return (m ? m[2] : name); }] }; mXml2Js.parseString(soap, opts, (error, result) => { if(error) { reject(error); } else { resolve(result); } }); }); return promise; }; /* ------------------------------------------------------------------ * Method: requestCommand(oxaddr, method_name, soap) * ---------------------------------------------------------------- */ OnvifSoap.prototype.requestCommand = function(oxaddr, method_name, soap) { let promise = new Promise((resolve, reject) => { let xml = ''; this._request(oxaddr, soap).then((res) => { xml = res; return this.parse(xml); }).then((result) => { let fault = this._getFaultReason(result); if(fault) { let err = new Error(fault); reject(err); } else { let parsed = this._parseResponseResult(method_name, result); if(parsed) { let res = { 'soap' : xml, 'formatted': mHtml ? mHtml.prettyPrint(xml, {indent_size: 2}) : '', 'converted': result, 'data': parsed }; resolve(res); } else { let err = new Error('The device seems to not support the ' + method_name + '() method.'); reject(err); } } }).catch((error) => { reject(error); }); }); return promise; }; OnvifSoap.prototype._parseResponseResult = function(method_name, res) { let s0 = res['Body']; if(!s0) {return null;} if((method_name + 'Response') in s0) { return s0; } else { null; } }; OnvifSoap.prototype._request = function(oxaddr, soap) { let promise = new Promise((resolve, reject) => { let post_opts = { protocol: oxaddr.protocol, //auth : oxaddr.auth, hostname: oxaddr.hostname, port : oxaddr.port || 80, path : oxaddr.pathname, method : 'POST', headers: { //'Content-Type': 'application/soap+xml; charset=utf-8; action="http://www.onvif.org/ver10/device/wsdl/GetScopes"', 'Content-Type': 'application/soap+xml; charset=utf-8;', 'Content-Length': Buffer.byteLength(soap) } }; let req = mHttp.request(post_opts, (res) => { res.setEncoding('utf8'); let xml = ''; res.on('data', (chunk) => { xml += chunk; }); res.on('end', () => { if(req) { req.removeAllListeners('error'); req.removeAllListeners('timeout'); req = null; } if(res) { res.removeAllListeners('data'); res.removeAllListeners('end'); } if(res.statusCode === 200) { resolve(xml); } else { let err = new Error(res.statusCode + ' ' + res.statusMessage); let code = res.statusCode; let text = res.statusMessage; if(xml) { this.parse(xml).then((parsed) => { let msg = ''; try { msg = parsed['Body']['Fault']['Reason']['Text']; if(typeof(msg) === 'object') { msg = msg['_']; } } catch(e) {} if(msg) { reject(new Error(code + ' ' + text + ' - ' + msg)); } else { reject(err); } }).catch((error) => { reject(err); }); } else { reject(err); } } res = null; }); }); req.setTimeout(this.HTTP_TIMEOUT); req.on('timeout', () => { req.abort(); }); req.on('error', (error) => { req.removeAllListeners('error'); req.removeAllListeners('timeout'); req = null; reject(new Error('Network Error: ' + (error ? error.message : ''))); }); req.write(soap, 'utf8'); req.end(); }); return promise; }; OnvifSoap.prototype._getFaultReason = function(r) { let reason = ''; try { let reason_el = r['Body']['Fault']['Reason']; if(reason_el['Text']) { reason = reason_el['Text']; } else { let code_el = r['Body']['Fault']['Code']; if(code_el['Value']) { reason = code_el['Value']; let subcode_el = code_el['Subcode']; if(subcode_el['Value']) { reason += ' ' + subcode_el['Value']; } } } } catch(e) {} return reason; }; /* ------------------------------------------------------------------ * Method: createRequestSoap(params) * - params: * - body: description in the <s:Body> * - xmlns: a list of xmlns attributes used in the body * e.g., xmlns:tds="http://www.onvif.org/ver10/device/wsdl" * - diff: Time difference [ms] * - user: user name * - pass: password * ---------------------------------------------------------------- */ OnvifSoap.prototype.createRequestSoap = function(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"'; if(params['xmlns'] && Array.isArray(params['xmlns'])) { params['xmlns'].forEach((ns) => { soap += ' ' + ns; }); } soap += '>'; soap += '<s:Header>'; if(params['user']) { soap += this._createSoapUserToken(params['diff'], params['user'], params['pass']); } soap += '</s:Header>'; soap += '<s:Body>' + params['body'] + '</s:Body>'; soap += '</s:Envelope>'; soap = soap.replace(/\>\s+\</g, '><'); return soap; }; OnvifSoap.prototype._createSoapUserToken = function(diff, user, pass) { if(!diff) {diff = 0;} if(!pass) {pass = '';} let date = (new Date(Date.now() + diff)).toISOString(); let nonce_buffer = this._createNonce(16); let nonce_base64 = nonce_buffer.toString('base64'); let shasum = mCrypto.createHash('sha1'); shasum.update(Buffer.concat([nonce_buffer, new Buffer(date), new Buffer(pass)])); let digest = shasum.digest('base64'); let soap = ''; soap += '<Security s:mustUnderstand="1" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">'; soap += ' <UsernameToken>'; soap += ' <Username>' + user + '</Username>'; soap += ' <Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">' + digest + '</Password>'; soap += ' <Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">' + nonce_base64 + '</Nonce>'; soap += ' <Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">' + date + '</Created>'; soap += ' </UsernameToken>'; soap += '</Security>'; return soap; }; OnvifSoap.prototype._createNonce = function(digit) { let nonce = new Buffer(digit); for(let i=0; i<digit; i++){ nonce.writeUInt8(Math.floor(Math.random() * 256), i); } return nonce; }; /* ------------------------------------------------------------------ * Method: isInvalidValue(value, type, allow_empty) * - type: 'undefined', 'null', 'array', 'integer', 'float', 'boolean', 'object' * ---------------------------------------------------------------- */ OnvifSoap.prototype.isInvalidValue = function(value, type, allow_empty) { let vt = this._getTypeOfValue(value); if(type === 'float') { if(!vt.match(/^(float|integer)$/)) { return 'The type of the value must be "' + type + '".'; } } else { if(vt !== type) { return 'The type of the value must be "' + type + '".'; } } if(!allow_empty) { if(vt === 'array' && value.length === 0) { return 'The value must not be an empty array.'; } else if(vt === 'string' && value === '') { return 'The value must not be an empty string.'; } } if(typeof(value) === 'string') { if(value.match(/[^\x20-\x7e]/)) { return 'The value must consist of ascii characters.'; } if(value.match(/[\<\>]/)) { return 'Invalid characters were found in the value ("<", ">")'; } } return ''; }; OnvifSoap.prototype._getTypeOfValue = function(value) { if(value === undefined) { return 'undefined'; } else if(value === null) { return 'null'; } else if(Array.isArray(value)) { return 'array'; } let t = typeof(value); if(t === 'boolean') { return 'boolean'; } else if(t === 'string') { return 'string'; } else if(t === 'number') { if(value % 1 === 0) { return 'integer'; } else { return 'float'; } } else if(t === 'object') { if(Object.prototype.toString.call(value) === '[object Object]') { return 'object'; } else { return 'unknown'; } } else { return 'unknown'; } } module.exports = new OnvifSoap();