UNPKG

@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
/** * 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 }