onvif-nvt
Version:
Wrapper for ONVIF spec to control NVT (Network Video Transitter) devices.
385 lines (321 loc) • 10.7 kB
JavaScript
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;