@koush/ring-client-api
Version:
Unofficial API for Ring doorbells, cameras, security alarm system and smart lighting
1,315 lines (1,314 loc) • 43.4 kB
JavaScript
;
/**
* Modified from https://github.com/kirm/sip.js/blob/master/sip.js
*
* Copyright (c) 2010 Kirill Mikhailov (kirill.mikhailov@gmail.com)
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/* eslint-disable */
var net = require('net'), dns = require('dns'), assert = require('assert'), tls = require('tls'), os = require('os'), crypto = require('crypto');
const RING_KEEPALIVE_PING = '\r\n\r\n';
function toBase64(s) {
switch (s.length % 3) {
case 1:
s += ' ';
break;
case 2:
s += ' ';
break;
default:
}
return new Buffer.from(s)
.toString('base64')
.replace(/\//g, '_')
.replace(/\+/g, '-');
}
// Actual stack code begins here
function parseResponse(rs, m) {
var r = rs.match(/^SIP\/(\d+\.\d+)\s+(\d+)\s*(.*)\s*$/);
if (r) {
m.version = r[1];
m.status = +r[2];
m.reason = r[3];
return m;
}
}
function parseRequest(rq, m) {
var r = rq.match(/^([\w\-.!%*_+`'~]+)\s([^\s]+)\sSIP\s*\/\s*(\d+\.\d+)/);
if (r) {
m.method = unescape(r[1]);
m.uri = r[2];
m.version = r[3];
return m;
}
}
function applyRegex(regex, data) {
regex.lastIndex = data.i;
var r = regex.exec(data.s);
if (r && r.index === data.i) {
data.i = regex.lastIndex;
return r;
}
}
function parseParams(data, hdr) {
hdr.params = hdr.params || {};
var re = /\s*;\s*([\w\-.!%*_+`'~]+)(?:\s*=\s*([\w\-.!%*_+`'~]+|"[^"\\]*(\\.[^"\\]*)*"))?/g;
for (var r = applyRegex(re, data); r; r = applyRegex(re, data)) {
hdr.params[r[1].toLowerCase()] = r[2] || null;
}
return hdr;
}
function parseMultiHeader(parser, d, h) {
h = h || [];
var re = /\s*,\s*/g;
do {
h.push(parser(d));
} while (d.i < d.s.length && applyRegex(re, d));
return h;
}
function parseGenericHeader(d, h) {
return h ? h + ',' + d.s : d.s;
}
function parseAOR(data) {
var r = applyRegex(/((?:[\w\-.!%*_+`'~]+)(?:\s+[\w\-.!%*_+`'~]+)*|"[^"\\]*(?:\\.[^"\\]*)*")?\s*\<\s*([^>]*)\s*\>|((?:[^\s@"<]@)?[^\s;]+)/g, data);
return parseParams(data, { name: r[1], uri: r[2] || r[3] || '' });
}
exports.parseAOR = parseAOR;
function parseAorWithUri(data) {
var r = parseAOR(data);
r.uri = parseUri(r.uri);
return r;
}
function parseVia(data) {
var r = applyRegex(/SIP\s*\/\s*(\d+\.\d+)\s*\/\s*([\S]+)\s+([^\s;:]+)(?:\s*:\s*(\d+))?/g, data);
return parseParams(data, {
version: r[1],
protocol: r[2],
host: r[3],
port: r[4] && +r[4],
});
}
function parseCSeq(d) {
var r = /(\d+)\s*([\S]+)/.exec(d.s);
return { seq: +r[1], method: unescape(r[2]) };
}
function parseAuthHeader(d) {
var r1 = applyRegex(/([^\s]*)\s+/g, d), a = { scheme: r1[1] }, r2 = applyRegex(/([^\s,"=]*)\s*=\s*([^\s,"]+|"[^"\\]*(?:\\.[^"\\]*)*")\s*/g, d);
a[r2[1]] = r2[2];
while ((r2 = applyRegex(/,\s*([^\s,"=]*)\s*=\s*([^\s,"]+|"[^"\\]*(?:\\.[^"\\]*)*")\s*/g, d))) {
a[r2[1]] = r2[2];
}
return a;
}
function parseAuthenticationInfoHeader(d) {
var a = {}, r = applyRegex(/([^\s,"=]*)\s*=\s*([^\s,"]+|"[^"\\]*(?:\\.[^"\\]*)*")\s*/g, d);
a[r[1]] = r[2];
while ((r = applyRegex(/,\s*([^\s,"=]*)\s*=\s*([^\s,"]+|"[^"\\]*(?:\\.[^"\\]*)*")\s*/g, d))) {
a[r[1]] = r[2];
}
return a;
}
var compactForm = {
i: 'call-id',
m: 'contact',
e: 'contact-encoding',
l: 'content-length',
c: 'content-type',
f: 'from',
s: 'subject',
k: 'supported',
t: 'to',
v: 'via',
}, parsers = {
to: parseAOR,
from: parseAOR,
contact: function (v, h) {
if (v == '*')
return v;
return parseMultiHeader(parseAOR, v, h);
},
route: parseMultiHeader.bind(0, parseAorWithUri),
'record-route': parseMultiHeader.bind(0, parseAorWithUri),
path: parseMultiHeader.bind(0, parseAorWithUri),
cseq: parseCSeq,
'content-length': function (v) {
return +v.s;
},
via: parseMultiHeader.bind(0, parseVia),
'www-authenticate': parseMultiHeader.bind(0, parseAuthHeader),
'proxy-authenticate': parseMultiHeader.bind(0, parseAuthHeader),
authorization: parseMultiHeader.bind(0, parseAuthHeader),
'proxy-authorization': parseMultiHeader.bind(0, parseAuthHeader),
'authentication-info': parseAuthenticationInfoHeader,
'refer-to': parseAOR,
};
function parse(data) {
data = data.split(/\r\n(?![ \t])/);
if (data[0] === '')
return;
var m = {};
if (!(parseResponse(data[0], m) || parseRequest(data[0], m)))
return;
m.headers = {};
for (var i = 1; i < data.length; ++i) {
var r = data[i].match(/^([\S]*?)\s*:\s*([\s\S]*)$/);
if (!r) {
return;
}
var name = unescape(r[1]).toLowerCase();
name = compactForm[name] || name;
try {
m.headers[name] = (parsers[name] || parseGenericHeader)({ s: r[2], i: 0 }, m.headers[name]);
}
catch (e) { }
}
return m;
}
function parseUri(s) {
if (typeof s === 'object')
return s;
var re = /^(sips?):(?:([^\s>:@]+)(?::([^\s@>]+))?@)?([\w\-\.]+)(?::(\d+))?((?:;[^\s=\?>;]+(?:=[^\s?\;]+)?)*)(?:\?(([^\s&=>]+=[^\s&=>]+)(&[^\s&=>]+=[^\s&=>]+)*))?$/, r = re.exec(s);
if (r) {
return {
schema: r[1],
user: r[2],
password: r[3],
host: r[4],
port: +r[5],
params: (r[6].match(/([^;=]+)(=([^;=]+))?/g) || [])
.map(function (s) {
return s.split('=');
})
.reduce(function (params, x) {
params[x[0]] = x[1] || null;
return params;
}, {}),
headers: ((r[7] || '').match(/[^&=]+=[^&=]+/g) || [])
.map(function (s) {
return s.split('=');
})
.reduce(function (params, x) {
params[x[0]] = x[1];
return params;
}, {}),
};
}
}
exports.parseUri = parseUri;
function stringifyVersion(v) {
return v || '2.0';
}
function stringifyParams(params) {
var s = '';
for (var n in params) {
s += ';' + n + (params[n] ? '=' + params[n] : '');
}
return s;
}
function stringifyUri(uri) {
if (typeof uri === 'string')
return uri;
var s = (uri.schema || 'sip') + ':';
if (uri.user) {
if (uri.password)
s += uri.user + ':' + uri.password + '@';
else
s += uri.user + '@';
}
s += uri.host;
if (uri.port)
s += ':' + uri.port;
if (uri.params)
s += stringifyParams(uri.params);
if (uri.headers) {
var h = Object.keys(uri.headers)
.map(function (x) {
return x + '=' + uri.headers[x];
})
.join('&');
if (h.length)
s += '?' + h;
}
return s;
}
exports.stringifyUri = stringifyUri;
function stringifyAOR(aor) {
return ((aor.name || '') +
' <' +
stringifyUri(aor.uri) +
'>' +
stringifyParams(aor.params));
}
function stringifyAuthHeader(a) {
var s = [];
for (var n in a) {
if (n !== 'scheme' && a[n] !== undefined) {
s.push(n + '=' + a[n]);
}
}
return a.scheme ? a.scheme + ' ' + s.join(',') : s.join(',');
}
exports.stringifyAuthHeader = stringifyAuthHeader;
var stringifiers = {
via: function (h) {
return h
.map(function (via) {
if (via.host) {
return ('Via: SIP/' +
stringifyVersion(via.version) +
'/' +
via.protocol.toUpperCase() +
' ' +
via.host +
(via.port ? ':' + via.port : '') +
stringifyParams(via.params) +
'\r\n');
}
return '';
})
.join('');
},
to: function (h) {
return 'To: ' + stringifyAOR(h) + '\r\n';
},
from: function (h) {
return 'From: ' + stringifyAOR(h) + '\r\n';
},
contact: function (h) {
return ('Contact: ' +
(h !== '*' && h.length ? h.map(stringifyAOR).join(', ') : '*') +
'\r\n');
},
route: function (h) {
return h.length ? 'Route: ' + h.map(stringifyAOR).join(', ') + '\r\n' : '';
},
'record-route': function (h) {
return h.length
? 'Record-Route: ' + h.map(stringifyAOR).join(', ') + '\r\n'
: '';
},
path: function (h) {
return h.length ? 'Path: ' + h.map(stringifyAOR).join(', ') + '\r\n' : '';
},
cseq: function (cseq) {
return 'CSeq: ' + cseq.seq + ' ' + cseq.method + '\r\n';
},
'www-authenticate': function (h) {
return h
.map(function (x) {
return 'WWW-Authenticate: ' + stringifyAuthHeader(x) + '\r\n';
})
.join('');
},
'proxy-authenticate': function (h) {
return h
.map(function (x) {
return 'Proxy-Authenticate: ' + stringifyAuthHeader(x) + '\r\n';
})
.join('');
},
authorization: function (h) {
return h
.map(function (x) {
return 'Authorization: ' + stringifyAuthHeader(x) + '\r\n';
})
.join('');
},
'proxy-authorization': function (h) {
return h
.map(function (x) {
return 'Proxy-Authorization: ' + stringifyAuthHeader(x) + '\r\n';
})
.join('');
},
'authentication-info': function (h) {
return 'Authentication-Info: ' + stringifyAuthHeader(h) + '\r\n';
},
'refer-to': function (h) {
return 'Refer-To: ' + stringifyAOR(h) + '\r\n';
},
};
function prettifyHeaderName(s) {
if (s == 'call-id')
return 'Call-ID';
return s.replace(/\b([a-z])/g, function (a) {
return a.toUpperCase();
});
}
function stringify(m) {
var s;
if (m.status) {
s =
'SIP/' +
stringifyVersion(m.version) +
' ' +
m.status +
' ' +
m.reason +
'\r\n';
}
else {
s =
m.method +
' ' +
stringifyUri(m.uri) +
' SIP/' +
stringifyVersion(m.version) +
'\r\n';
}
m.headers['content-length'] = (m.content || '').length;
for (var n in m.headers) {
if (typeof m.headers[n] !== 'undefined') {
if (typeof m.headers[n] === 'string' || !stringifiers[n]) {
s += prettifyHeaderName(n) + ': ' + m.headers[n] + '\r\n';
}
else
s += stringifiers[n](m.headers[n], n);
}
}
s += '\r\n';
if (m.content)
s += m.content;
return s;
}
exports.stringify = stringify;
function makeResponse(rq, status, reason, extension) {
var rs = {
status: status,
reason: reason || '',
version: rq.version,
headers: {
via: rq.headers.via,
to: rq.headers.to,
from: rq.headers.from,
'call-id': rq.headers['call-id'],
cseq: rq.headers.cseq,
},
};
if (extension) {
if (extension.headers) {
Object.keys(extension.headers).forEach(function (h) {
rs.headers[h] = extension.headers[h];
});
}
rs.content = extension.content;
}
return rs;
}
exports.makeResponse = makeResponse;
function clone(o, deep) {
if (o !== null && typeof o === 'object') {
var r = Array.isArray(o) ? [] : {};
Object.keys(o).forEach(function (k) {
r[k] = deep ? clone(o[k], deep) : o[k];
});
return r;
}
return o;
}
exports.copyMessage = function (msg, deep) {
if (deep)
return clone(msg, true);
var r = {
uri: deep ? clone(msg.uri, deep) : msg.uri,
method: msg.method,
status: msg.status,
reason: msg.reason,
headers: clone(msg.headers, deep),
content: msg.content,
};
// always copy via array
r.headers.via = clone(msg.headers.via);
return r;
};
function defaultPort(proto) {
return proto.toUpperCase() === 'TLS' ? 5061 : 5060;
}
function makeStreamParser(onMessage, onFlood, maxBytesHeaders, maxContentLength) {
onFlood = onFlood || function () { };
maxBytesHeaders = maxBytesHeaders || 60480;
maxContentLength = maxContentLength || 604800;
var m, r = '';
function headers(data) {
r += data;
if (r.length > maxBytesHeaders) {
r = '';
onFlood();
return;
}
var a = r.match(/^\s*([\S\s]*?)\r\n\r\n([\S\s]*)$/);
if (a) {
r = a[2];
m = parse(a[1]);
if (m && m.headers['content-length'] !== undefined) {
if (m.headers['content-length'] > maxContentLength) {
r = '';
onFlood();
}
state = content;
content('');
}
else
headers('');
}
}
function content(data) {
r += data;
if (r.length >= m.headers['content-length']) {
m.content = r.substring(0, m.headers['content-length']);
onMessage(m);
var s = r.substring(m.headers['content-length']);
state = headers;
r = '';
headers(s);
}
}
var state = headers;
return function (data) {
if (data === RING_KEEPALIVE_PING) {
// Received PONG from Ring
return;
}
state(data);
};
}
exports.makeStreamParser = makeStreamParser;
function parseMessage(s) {
var r = s.toString('binary').match(/^\s*([\S\s]*?)\r\n\r\n([\S\s]*)$/);
if (r) {
var m = parse(r[1]);
if (m) {
if (m.headers['content-length']) {
var c = Math.max(0, Math.min(m.headers['content-length'], r[2].length));
m.content = r[2].substring(0, c);
}
else {
m.content = r[2];
}
return m;
}
}
}
exports.parse = parseMessage;
function checkMessage(msg) {
return ((msg.method || (msg.status >= 100 && msg.status <= 999)) &&
msg.headers &&
Array.isArray(msg.headers.via) &&
msg.headers.via.length > 0 &&
msg.headers['call-id'] &&
msg.headers.to &&
msg.headers.from &&
msg.headers.cseq);
}
function makeStreamTransport(protocol, maxBytesHeaders, maxContentLength, connect, createServer, callback) {
var remotes = Object.create(null), flows = Object.create(null);
function init(stream, remote) {
var remoteid = [remote.address, remote.port].join(), flowid = undefined, refs = 0;
const pingInterval = setInterval(() => {
stream.write(RING_KEEPALIVE_PING, 'binary');
}, 5000);
function register_flow() {
flowid = [remoteid, stream.localAddress, stream.localPort].join();
flows[flowid] = remotes[remoteid];
}
var onMessage = function (m) {
if (checkMessage(m)) {
if (m.method)
m.headers.via[0].params.received = remote.address;
callback(m, {
protocol: remote.protocol,
address: stream.remoteAddress,
port: stream.remotePort,
local: { address: stream.localAddress, port: stream.localPort },
}, stream);
}
}, onFlood = function () {
console.log('Flood attempt, destroying stream');
stream.destroy();
};
stream.setEncoding('binary');
stream.on('data', makeStreamParser(onMessage, onFlood, maxBytesHeaders, maxContentLength));
stream.on('close', function () {
clearInterval(pingInterval);
if (flowid)
delete flows[flowid];
delete remotes[remoteid];
});
stream.on('connect', register_flow);
stream.on('error', function () { });
stream.on('end', function () {
if (refs !== 0) {
stream.emit('error', new Error('remote peer disconnected'));
}
stream.end();
});
stream.on('timeout', function () {
if (refs === 0)
stream.destroy();
});
stream.setTimeout(120000);
stream.setMaxListeners(10000);
remotes[remoteid] = function (onError) {
++refs;
if (onError)
stream.on('error', onError);
return {
release: function () {
if (onError)
stream.removeListener('error', onError);
if (--refs === 0)
stream.emit('no_reference');
},
send: function (m) {
stream.write(stringify(m), 'binary');
},
protocol: protocol,
};
};
if (stream.localPort)
register_flow();
return remotes[remoteid];
}
var server = createServer(function (stream) {
init(stream, {
protocol: protocol,
address: stream.remoteAddress,
port: stream.remotePort,
});
});
return {
open: function (remote, error) {
var remoteid = [remote.address, remote.port].join();
if (remoteid in remotes)
return remotes[remoteid](error);
return init(connect(remote.port, remote.address), remote)(error);
},
get: function (address, error) {
var c = address.local
? flows[[
address.address,
address.port,
address.local.address,
address.local.port,
].join()]
: remotes[[address.address, address.port].join()];
return c && c(error);
},
destroy: function () {
server.close();
},
};
}
function makeTlsTransport(options, callback) {
return makeStreamTransport('TLS', options.maxBytesHeaders, options.maxContentLength, function (port, host, callback) {
return tls.connect(port, host, options.tls, callback);
}, function (callback) {
var server = tls.createServer(options.tls, callback);
server.listen();
return server;
}, callback);
}
function makeTransport(options, callback) {
var protocols = {}, callbackAndLog = callback;
if (options.logger && options.logger.recv) {
callbackAndLog = function (m, remote, stream) {
options.logger.recv(m, remote);
callback(m, remote, stream);
};
}
protocols.TLS = makeTlsTransport(options, callbackAndLog);
function wrap(obj, target) {
return Object.create(obj, {
send: {
value: function (m) {
if (m.method) {
m.headers.via[0].host =
options.publicAddress ||
options.address ||
options.hostname ||
os.hostname();
m.headers.via[0].port = options.port || defaultPort(this.protocol);
m.headers.via[0].protocol = this.protocol;
if (this.protocol === 'UDP' &&
(!options.hasOwnProperty('rport') || options.rport)) {
m.headers.via[0].params.rport = null;
}
}
options.logger &&
options.logger.send &&
options.logger.send(m, target);
obj.send(m);
},
},
});
}
return {
open: function (target, error) {
return wrap(protocols[target.protocol.toUpperCase()].open(target, error), target);
},
get: function (target, error) {
var flow = protocols[target.protocol.toUpperCase()].get(target, error);
return flow && wrap(flow, target);
},
send: function (target, message) {
var cn = this.open(target);
try {
cn.send(message);
}
finally {
cn.release();
}
},
destroy: function () {
var protos = protocols;
protocols = [];
Object.keys(protos).forEach(function (key) {
protos[key].destroy();
});
},
};
}
exports.makeTransport = makeTransport;
function makeWellBehavingResolver(resolve) {
var outstanding = Object.create(null);
return function (name, cb) {
if (outstanding[name]) {
outstanding[name].push(cb);
}
else {
outstanding[name] = [cb];
resolve(name, function () {
var o = outstanding[name];
delete outstanding[name];
var args = arguments;
o.forEach(function (x) {
x.apply(null, args);
});
});
}
};
}
var resolveSrv = makeWellBehavingResolver(dns.resolveSrv), resolve4 = makeWellBehavingResolver(dns.resolve4), resolve6 = makeWellBehavingResolver(dns.resolve6);
function resolve(uri, action) {
if (uri.params.transport === 'ws') {
return action([
{
protocol: uri.schema === 'sips' ? 'WSS' : 'WS',
host: uri.host,
port: uri.port || (uri.schema === 'sips' ? 433 : 80),
},
]);
}
if (net.isIP(uri.host)) {
var protocol = uri.params.transport || 'UDP';
return action([
{
protocol: protocol,
address: uri.host,
port: uri.port || defaultPort(protocol),
},
]);
}
function resolve46(host, cb) {
resolve4(host, function (e4, a4) {
resolve6(host, function (e6, a6) {
if ((a4 || a6) && (a4 || a6).length) {
cb(null, (a4 || []).concat(a6 || []));
}
else
cb(e4 || e6, []);
});
});
}
if (uri.port) {
var protocols = uri.params.transport
? [uri.params.transport]
: ['UDP', 'TCP', 'TLS'];
resolve46(uri.host, function (err, address) {
address = (address || [])
.map(function (x) {
return protocols.map(function (p) {
return { protocol: p, address: x, port: uri.port || defaultPort(p) };
});
})
.reduce(function (arr, v) {
return arr.concat(v);
}, []);
action(address);
});
}
else {
var protocols = uri.params.transport
? [uri.params.transport]
: ['tcp', 'udp', 'tls'], n = protocols.length, addresses = [];
protocols.forEach(function (proto) {
resolveSrv('_sip._' + proto + '.' + uri.host, function (e, r) {
--n;
if (Array.isArray(r)) {
n += r.length;
r.forEach(function (srv) {
resolve46(srv.name, function (e, r) {
addresses = addresses.concat((r || []).map(function (a) {
return { protocol: proto, address: a, port: srv.port };
}));
if (--n === 0) {
// all outstanding requests has completed
action(addresses);
}
});
});
}
else if (0 === n) {
if (addresses.length) {
action(addresses);
}
else {
// all srv requests failed
resolve46(uri.host, function (err, address) {
address = (address || [])
.map(function (x) {
return protocols.map(function (p) {
return {
protocol: p,
address: x,
port: uri.port || defaultPort(p),
};
});
})
.reduce(function (arr, v) {
return arr.concat(v);
}, []);
action(address);
});
}
}
});
});
}
}
exports.resolve = resolve;
//transaction layer
function generateBranch() {
return ['z9hG4bK', Math.round(Math.random() * 1000000)].join('');
}
exports.generateBranch = generateBranch;
function makeSM() {
var state;
return {
enter: function (newstate) {
if (state && state.leave)
state.leave();
state = newstate;
Array.prototype.shift.apply(arguments);
if (state.enter)
state.enter.apply(this, arguments);
},
signal: function (s) {
if (state && state[s]) {
state[Array.prototype.shift.apply(arguments)].apply(state, arguments);
}
},
};
}
function createInviteServerTransaction(transport, cleanup) {
var sm = makeSM(), rs, proceeding = {
message: function () {
if (rs)
transport(rs);
},
send: function (message) {
rs = message;
if (message.status >= 300)
sm.enter(completed);
else if (message.status >= 200)
sm.enter(accepted);
transport(rs);
},
}, g, h, completed = {
enter: function () {
g = setTimeout(function retry(t) {
g = setTimeout(retry, t * 2, t * 2);
transport(rs);
}, 500, 500);
h = setTimeout(sm.enter.bind(sm, terminated), 32000);
},
leave: function () {
clearTimeout(g);
clearTimeout(h);
},
message: function (m) {
if (m.method === 'ACK')
sm.enter(confirmed);
else
transport(rs);
},
}, timer_i, confirmed = {
enter: function () {
timer_i = setTimeout(sm.enter.bind(sm, terminated), 5000);
},
leave: function () {
clearTimeout(timer_i);
},
}, l, accepted = {
enter: function () {
l = setTimeout(sm.enter.bind(sm, terminated), 32000);
},
leave: function () {
clearTimeout(l);
},
send: function (m) {
rs = m;
transport(rs);
},
}, terminated = { enter: cleanup };
sm.enter(proceeding);
return {
send: sm.signal.bind(sm, 'send'),
message: sm.signal.bind(sm, 'message'),
shutdown: function () {
sm.enter(terminated);
},
};
}
function createServerTransaction(transport, cleanup) {
var sm = makeSM(), rs, trying = {
message: function () {
if (rs)
transport(rs);
},
send: function (m) {
rs = m;
transport(m);
if (m.status >= 200)
sm.enter(completed);
},
}, j, completed = {
message: function () {
transport(rs);
},
enter: function () {
j = setTimeout(function () {
sm.enter(terminated);
}, 32000);
},
leave: function () {
clearTimeout(j);
},
}, terminated = { enter: cleanup };
sm.enter(trying);
return {
send: sm.signal.bind(sm, 'send'),
message: sm.signal.bind(sm, 'message'),
shutdown: function () {
sm.enter(terminated);
},
};
}
function createInviteClientTransaction(rq, transport, tu, cleanup, options) {
var sm = makeSM(), a, b, calling = {
enter: function () {
transport(rq);
if (!transport.reliable) {
a = setTimeout(function resend(t) {
transport(rq);
a = setTimeout(resend, t * 2, t * 2);
}, 500, 500);
}
b = setTimeout(function () {
tu(makeResponse(rq, 408));
sm.enter(terminated);
}, 32000);
},
leave: function () {
clearTimeout(a);
clearTimeout(b);
},
message: function (message) {
tu(message);
if (message.status < 200)
sm.enter(proceeding);
else if (message.status < 300)
sm.enter(accepted);
else
sm.enter(completed, message);
},
}, proceeding = {
message: function (message) {
tu(message);
if (message.status >= 300)
sm.enter(completed, message);
else if (message.status >= 200)
sm.enter(accepted);
},
}, ack = {
method: 'ACK',
uri: rq.uri,
headers: {
from: rq.headers.from,
cseq: { method: 'ACK', seq: rq.headers.cseq.seq },
'call-id': rq.headers['call-id'],
via: [rq.headers.via[0]],
'max-forwards': (options && options['max-forwards']) || 70,
},
}, d, completed = {
enter: function (rs) {
ack.headers.to = rs.headers.to;
transport(ack);
d = setTimeout(sm.enter.bind(sm, terminated), 32000);
},
leave: function () {
clearTimeout(d);
},
message: function (message, remote) {
if (remote)
transport(ack); // we don't want to ack internally generated messages
},
}, timer_m, accepted = {
enter: function () {
timer_m = setTimeout(function () {
sm.enter(terminated);
}, 32000);
},
leave: function () {
clearTimeout(timer_m);
},
message: function (m) {
if (m.status >= 200 && m.status <= 299)
tu(m);
},
}, terminated = { enter: cleanup };
process.nextTick(function () {
sm.enter(calling);
});
return {
message: sm.signal.bind(sm, 'message'),
shutdown: function () {
sm.enter(terminated);
},
};
}
function createClientTransaction(rq, transport, tu, cleanup) {
assert.ok(rq.method !== 'INVITE');
var sm = makeSM(), e, f, trying = {
enter: function () {
transport(rq);
if (!transport.reliable) {
e = setTimeout(function () {
sm.signal('timerE', 500);
}, 500);
}
f = setTimeout(function () {
sm.signal('timerF');
}, 32000);
},
leave: function () {
clearTimeout(e);
clearTimeout(f);
},
message: function (message, remote) {
if (message.status >= 200)
sm.enter(completed);
else
sm.enter(proceeding);
tu(message);
},
timerE: function (t) {
transport(rq);
e = setTimeout(function () {
sm.signal('timerE', t * 2);
}, t * 2);
},
timerF: function () {
tu(makeResponse(rq, 408));
sm.enter(terminated);
},
}, proceeding = {
message: function (message, remote) {
if (message.status >= 200)
sm.enter(completed);
tu(message);
},
}, k, completed = {
enter: function () {
k = setTimeout(function () {
sm.enter(terminated);
}, 5000);
},
leave: function () {
clearTimeout(k);
},
}, terminated = { enter: cleanup };
process.nextTick(function () {
sm.enter(trying);
});
return {
message: sm.signal.bind(sm, 'message'),
shutdown: function () {
sm.enter(terminated);
},
};
}
function makeTransactionId(m) {
if (m.method === 'ACK') {
return [
'INVITE',
m.headers['call-id'],
m.headers.via[0].params.branch,
].join();
}
return [
m.headers.cseq.method,
m.headers['call-id'],
m.headers.via[0].params.branch,
].join();
}
function makeTransactionLayer(options, transport) {
var server_transactions = Object.create(null), client_transactions = Object.create(null);
return {
createServerTransaction: function (rq, cn) {
var id = makeTransactionId(rq);
return (server_transactions[id] = (rq.method === 'INVITE'
? createInviteServerTransaction
: createServerTransaction)(cn.send.bind(cn), function () {
delete server_transactions[id];
cn.release();
}));
},
createClientTransaction: function (connection, rq, callback) {
if (rq.method !== 'CANCEL') {
rq.headers.via[0].params.branch = generateBranch();
}
if (typeof rq.headers.cseq !== 'object') {
rq.headers.cseq = parseCSeq({ s: rq.headers.cseq, i: 0 });
}
var send = connection.send.bind(connection);
send.reliable = connection.protocol.toUpperCase() !== 'UDP';
var id = makeTransactionId(rq);
return (client_transactions[id] = (rq.method === 'INVITE'
? createInviteClientTransaction
: createClientTransaction)(rq, send, callback, function () {
delete client_transactions[id];
connection.release();
}, options));
},
getServer: function (m) {
return server_transactions[makeTransactionId(m)];
},
getClient: function (m) {
return client_transactions[makeTransactionId(m)];
},
destroy: function () {
Object.keys(client_transactions).forEach(function (x) {
client_transactions[x].shutdown();
});
Object.keys(server_transactions).forEach(function (x) {
server_transactions[x].shutdown();
});
},
};
}
exports.makeTransactionLayer = makeTransactionLayer;
function sequentialSearch(transaction, connect, addresses, rq, callback) {
if (rq.method !== 'CANCEL') {
if (!rq.headers.via)
rq.headers.via = [];
rq.headers.via.unshift({ params: {} });
}
var onresponse, lastStatusCode;
function next() {
onresponse = searching;
if (addresses.length > 0) {
try {
var address = addresses.shift(), client = transaction(connect(address, function (err) {
if (err) {
console.log('err: ', err);
}
client.message(makeResponse(rq, 503));
}), rq, function () {
onresponse.apply(null, arguments);
});
}
catch (e) {
onresponse(address.local ? makeResponse(rq, 430) : makeResponse(rq, 503));
}
}
else {
onresponse = callback;
onresponse(makeResponse(rq, lastStatusCode || 404));
}
}
function searching(rs) {
lastStatusCode = rs.status;
if (rs.status === 503)
return next();
else if (rs.status > 100)
onresponse = callback;
callback(rs);
}
next();
}
exports.create = function (options, callback) {
var errorLog = (options.logger && options.logger.error) || function () { }, transport = makeTransport(options, function (m, remote) {
try {
var t = m.method ? transaction.getServer(m) : transaction.getClient(m);
if (!t) {
if (m.method && m.method !== 'ACK') {
var t = transaction.createServerTransaction(m, transport.get(remote));
try {
callback(m, remote);
}
catch (e) {
t.send(makeResponse(m, '500', 'Internal Server Error'));
throw e;
}
}
else if (m.method === 'ACK') {
callback(m, remote);
}
}
else {
t.message && t.message(m, remote);
}
}
catch (e) {
errorLog(e);
}
}), transaction = makeTransactionLayer(options, transport.open.bind(transport)), hostname = options.publicAddress ||
options.address ||
options.hostname ||
os.hostname(), rbytes = crypto.randomBytes(20);
function encodeFlowToken(flow) {
var s = [
flow.protocol,
flow.address,
flow.port,
flow.local.address,
flow.local.port,
].join(), h = crypto.createHmac('sha1', rbytes);
h.update(s);
return toBase64([h.digest('base64'), s].join());
}
function decodeFlowToken(token) {
var s = new Buffer.from(token, 'base64').toString('ascii').split(',');
if (s.length !== 6)
return;
var flow = {
protocol: s[1],
address: s[2],
port: +s[3],
local: { address: s[4], port: +s[5] },
};
return encodeFlowToken(flow) === token ? flow : undefined;
}
return {
send: function (m, callback) {
if (m.method === undefined) {
var t = transaction.getServer(m);
t && t.send && t.send(m);
}
else {
var hop = parseUri(m.uri);
if (typeof m.headers.route === 'string') {
try {
m.headers.route = parsers.route({ s: m.headers.route, i: 0 });
}
catch (e) {
m.headers.route = undefined;
}
}
if (m.headers.route && m.headers.route.length > 0) {
hop = parseUri(m.headers.route[0].uri);
if (hop.host === hostname) {
m.headers.route.shift();
}
else if (hop.params.lr === undefined) {
m.headers.route.shift();
m.headers.route.push({ uri: m.uri });
m.uri = hop;
}
}
;
(function (callback) {
if (hop.host === hostname) {
var flow = decodeFlowToken(hop.user);
callback(flow ? [flow] : []);
}
else
resolve(hop, callback);
})(function (addresses) {
if (m.method === 'ACK') {
if (!Array.isArray(m.headers.via))
m.headers.via = [];
if (m.headers.via.length === 0) {
m.headers.via.unshift({ params: { branch: generateBranch() } });
}
if (addresses.length === 0) {
errorLog(new Error("ACK: couldn't resolve " + stringifyUri(m.uri)));
return;
}
var cn = transport.open(addresses[0], errorLog);
try {
cn.send(m);
}
catch (e) {
errorLog(e);
}
finally {
cn.release();
}
}
else
sequentialSearch(transaction.createClientTransaction.bind(transaction), transport.open.bind(transport), addresses, m, callback || function () { });
});
}
},
encodeFlowUri: function (flow) {
return {
schema: flow.protocol === 'TLS' ? 'sips' : 'sip',
user: encodeFlowToken(flow),
host: hostname,
params: {},
};
},
decodeFlowUri: function (uri) {
uri = parseUri(uri);
return uri.host === hostname ? decodeFlowToken(uri.user) : undefined;
},
isFlowUri: function (uri) {
return !decodeFlowUri(uri);
},
hostname: function () {
return hostname;
},
destroy: function () {
transaction.destroy();
transport.destroy();
},
makeResponse,
};
};
exports.start = function (options, callback) {
var r = exports.create(options, callback);
exports.send = r.send;
exports.stop = r.destroy;
exports.encodeFlowUri = r.encodeFlowUri;
exports.decodeFlowUri = r.decodeFlowUri;
exports.isFlowUri = r.isFlowUri;
exports.hostname = r.hostname;
};