/**
* @namespace discovery
* @description Discovery module
* @author Andrew D.Laptev <a.d.laptev@gmail.com>
* @licence MIT
*/
const
Cam = require('./cam').Cam
, events = require('events')
, guid = require('./utils').guid
, linerase = require('./utils').linerase
, parseSOAPString = require('./utils').parseSOAPString
, url = require('url')
, os = require('os')
;
/**
* Discovery singleton
* @type {Object}
* @class
*/
var Discovery = Object.create(new events.EventEmitter());
/**
* @callback Discovery~ProbeCallback
* @property {?Error} error
* @property {Array<Cam|object>} found devices
*/
/**
* Discover NVT devices in the subnetwork
* @param {object} [options]
* @param {number} [options.timeout=5000] timeout in milliseconds for discovery responses
* @param {boolean} [options.resolve=true] set to `false` if you want omit creating of Cam objects
* @param {string} [options.messageId=GUID] WS-Discovery message id
* @param {string} [options.device=defaultroute] Interface to bind on for discovery ex. `eth0`
* @param {number} [options.listeningPort=null] client will listen to discovery data device sent
* @param {Discovery~ProbeCallback} [callback] timeout callback
* @fires Discovery#device
* @fires Discovery#error
* @example
* var onvif = require('onvif');
* onvif.Discovery.on('device', function(cam){
* // function would be called as soon as NVT responses
* cam.username = <USERNAME>;
* cam.password = <PASSWORD>;
* cam.connect(console.log);
* })
* onvif.Discovery.probe();
* @example
* var onvif = require('onvif');
* onvif.Discovery.probe(function(err, cams) {
* // function would be called only after timeout (5 sec by default)
* if (err) { throw err; }
* cams.forEach(function(cam) {
* cam.username = <USERNAME>;
* cam.password = <PASSWORD>;
* cam.connect(console.log);
* });
* });
*/
Discovery.probe = function(options, callback) {
if (callback === undefined) {
if (typeof options === 'function') {
callback = options;
options = {};
} else {
options = options || {};
}
}
callback = callback || function() {};
var cams = {}
, errors = []
, messageID = 'urn:uuid:' + (options.messageId || guid())
, request = Buffer.from(
'<Envelope xmlns="http://www.w3.org/2003/05/soap-envelope" xmlns:dn="http://www.onvif.org/ver10/network/wsdl">' +
'<Header>' +
'<wsa:MessageID xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">' + messageID + '</wsa:MessageID>' +
'<wsa:To xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">urn:schemas-xmlsoap-org:ws:2005:04:discovery</wsa:To>' +
'<wsa:Action xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</wsa:Action>' +
'</Header>' +
'<Body>' +
'<Probe xmlns="http://schemas.xmlsoap.org/ws/2005/04/discovery" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' +
'<Types>dn:NetworkVideoTransmitter</Types>' +
'<Scopes />' +
'</Probe>' +
'</Body>' +
'</Envelope>'
)
, socket = require('dgram').createSocket('udp4');
socket.on('error', function(err) {
Discovery.emit('error', err);
callback(err);
});
const listener = function(msg, rinfo) {
parseSOAPString(msg.toString(), function(err, data, xml) {
// TODO check for matching RelatesTo field and messageId
if (err || !data[0].probeMatches) {
errors.push(err || new Error('Wrong SOAP message from ' + rinfo.address + ':' + rinfo.port, xml));
/**
* Indicates error response from device.
* @event Discovery#error
* @type {string}
*/
Discovery.emit('error', 'Wrong SOAP message from ' + rinfo.address + ':' + rinfo.port, xml);
} else {
data = linerase(data);
// Possible to get multiple matches for the same camera
// when your computer has more than one network adapter in the same subnet
var camAddr = data.probeMatches.probeMatch.endpointReference.address;
if (!cams[camAddr]) {
var cam;
if (options.resolve !== false) {
// Create cam with one of the XAddrs uri
var camUris = data.probeMatches.probeMatch.XAddrs.split(' ').map(url.parse)
, camUri = matchXAddr(camUris, rinfo.address)
;
cam = new Cam({
hostname: camUri.hostname
, port: camUri.port
, path: camUri.path
, urn: camAddr
});
/**
* All available XAddr fields from discovery
* @name xaddrs
* @memberOf Cam#
* @type {Array.<Url>}
*/
cam.xaddrs = camUris;
} else {
cam = data;
}
cams[camAddr] = cam;
/**
* Indicates discovered device.
* @event Discovery#device
* @type {Cam|object}
*/
Discovery.emit('device', cam, rinfo, xml);
}
}
});
};
// If device is specified try to bind to that interface
if (options.device) {
var interfaces = os.networkInterfaces();
// Try to find the interface based on the device name
if (options.device in interfaces) {
interfaces[options.device].some(function(address) {
// Only use IPv4 addresses
if (address.family === 'IPv4') {
socket.bind(options.listeningPort || null, address.address);
return true;
}
});
}
}
socket.on('message', listener);
socket.send(request, 0, request.length, 3702, '239.255.255.250');
setTimeout(function() {
socket.removeListener('message', listener);
socket.close();
callback(errors.length ? errors : null, Object.keys(cams).map(function(addr) { return cams[addr]; }));
}.bind(this), options.timeout || 5000);
};
/**
* Try to find the most suitable record
* Now it is simple ip match
* @param {Array<Url>} xaddrs
* @param {string} address
*/
function matchXAddr(xaddrs, address) {
var ipMatch = xaddrs.filter(function(xaddr) {
return xaddr.hostname === address;
});
return ipMatch[0] || xaddrs[0];
}
module.exports = {
Discovery: Discovery
};