@koush/ring-client-api
Version:
Unofficial API for Ring doorbells, cameras, security alarm system and smart lighting
1,593 lines (1,397 loc) • 37.7 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
}