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
JavaScript
;
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 <s:Body> 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