UNPKG

jssip

Version:

the Javascript SIP library

785 lines (663 loc) 17.5 kB
const sdp_transform = require('sdp-transform'); const Logger = require('./Logger'); const JsSIP_C = require('./Constants'); const Utils = require('./Utils'); const NameAddrHeader = require('./NameAddrHeader'); const Grammar = require('./Grammar'); const logger = new Logger('SIPMessage'); /** * -param {String} method request method * -param {String} ruri request uri * -param {UA} ua * -param {Object} params parameters that will have priority over ua.configuration parameters: * <br> * - cseq, call_id, from_tag, from_uri, from_display_name, to_uri, to_tag, route_set * -param {Object} [headers] extra headers * -param {String} [body] */ class OutgoingRequest { constructor(method, ruri, ua, params, extraHeaders, body) { // Mandatory parameters check. if (!method || !ruri || !ua) { return null; } params = params || {}; this.ua = ua; this.headers = {}; this.method = method; this.ruri = ruri; this.body = body; this.extraHeaders = Utils.cloneArray(extraHeaders); if (this.ua.configuration.extra_headers) { this.extraHeaders = this.extraHeaders.concat(this.ua.configuration.extra_headers); } // Fill the Common SIP Request Headers. // Route. if (params.route_set) { this.setHeader('route', params.route_set); } else if (ua.configuration.use_preloaded_route) { this.setHeader('route', `<${ua.transport.sip_uri};lr>`); } // Via. // Empty Via header. Will be filled by the client transaction. this.setHeader('via', ''); // Max-Forwards. this.setHeader('max-forwards', JsSIP_C.MAX_FORWARDS); // To const to_uri = params.to_uri || ruri; const to_params = params.to_tag ? { tag: params.to_tag } : null; const to_display_name = typeof params.to_display_name !== 'undefined' ? params.to_display_name : null; this.to = new NameAddrHeader(to_uri, to_display_name, to_params); this.setHeader('to', this.to.toString()); // From. const from_uri = params.from_uri || ua.configuration.uri; const from_params = { tag: params.from_tag || Utils.newTag() }; let display_name; if (typeof params.from_display_name !== 'undefined') { display_name = params.from_display_name; } else if (ua.configuration.display_name) { display_name = ua.configuration.display_name; } else { display_name = null; } this.from = new NameAddrHeader(from_uri, display_name, from_params); this.setHeader('from', this.from.toString()); // Call-ID. const call_id = params.call_id || (ua.configuration.jssip_id + Utils.createRandomToken(15)); this.call_id = call_id; this.setHeader('call-id', call_id); // CSeq. const cseq = params.cseq || Math.floor(Math.random() * 10000); this.cseq = cseq; this.setHeader('cseq', `${cseq} ${method}`); } /** * Replace the the given header by the given value. * -param {String} name header name * -param {String | Array} value header value */ setHeader(name, value) { // Remove the header from extraHeaders if present. const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i'); for (let idx=0; idx<this.extraHeaders.length; idx++) { if (regexp.test(this.extraHeaders[idx])) { this.extraHeaders.splice(idx, 1); } } this.headers[Utils.headerize(name)] = (Array.isArray(value)) ? value : [ value ]; } /** * Get the value of the given header name at the given position. * -param {String} name header name * -returns {String|undefined} Returns the specified header, null if header doesn't exist. */ getHeader(name) { const headers = this.headers[Utils.headerize(name)]; if (headers) { if (headers[0]) { return headers[0]; } } else { const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i'); for (const header of this.extraHeaders) { if (regexp.test(header)) { return header.substring(header.indexOf(':')+1).trim(); } } } return; } /** * Get the header/s of the given name. * -param {String} name header name * -returns {Array} Array with all the headers of the specified name. */ getHeaders(name) { const headers = this.headers[Utils.headerize(name)]; const result = []; if (headers) { for (const header of headers) { result.push(header); } return result; } else { const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i'); for (const header of this.extraHeaders) { if (regexp.test(header)) { result.push(header.substring(header.indexOf(':')+1).trim()); } } return result; } } /** * Verify the existence of the given header. * -param {String} name header name * -returns {boolean} true if header with given name exists, false otherwise */ hasHeader(name) { if (this.headers[Utils.headerize(name)]) { return true; } else { const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i'); for (const header of this.extraHeaders) { if (regexp.test(header)) { return true; } } } return false; } /** * Parse the current body as a SDP and store the resulting object * into this.sdp. * -param {Boolean} force: Parse even if this.sdp already exists. * * Returns this.sdp. */ parseSDP(force) { if (!force && this.sdp) { return this.sdp; } else { this.sdp = sdp_transform.parse(this.body || ''); return this.sdp; } } toString() { let msg = `${this.method} ${this.ruri} SIP/2.0\r\n`; for (const headerName in this.headers) { if (Object.prototype.hasOwnProperty.call(this.headers, headerName)) { for (const headerValue of this.headers[headerName]) { msg += `${headerName}: ${headerValue}\r\n`; } } } for (const header of this.extraHeaders) { msg += `${header.trim()}\r\n`; } // Supported. const supported = []; switch (this.method) { case JsSIP_C.REGISTER: supported.push('path', 'gruu'); break; case JsSIP_C.INVITE: if (this.ua.configuration.session_timers) { supported.push('timer'); } if (this.ua.contact.pub_gruu || this.ua.contact.temp_gruu) { supported.push('gruu'); } supported.push('ice', 'replaces'); break; case JsSIP_C.UPDATE: if (this.ua.configuration.session_timers) { supported.push('timer'); } supported.push('ice'); break; } supported.push('outbound'); const userAgent = this.ua.configuration.user_agent || JsSIP_C.USER_AGENT; // Allow. msg += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`; msg += `Supported: ${supported}\r\n`; msg += `User-Agent: ${userAgent}\r\n`; if (this.body) { const length = Utils.str_utf8_length(this.body); msg += `Content-Length: ${length}\r\n\r\n`; msg += this.body; } else { msg += 'Content-Length: 0\r\n\r\n'; } return msg; } clone() { const request = new OutgoingRequest(this.method, this.ruri, this.ua); Object.keys(this.headers).forEach(function(name) { request.headers[name] = this.headers[name].slice(); }, this); request.body = this.body; request.extraHeaders = Utils.cloneArray(this.extraHeaders); request.to = this.to; request.from = this.from; request.call_id = this.call_id; request.cseq = this.cseq; return request; } } class InitialOutgoingInviteRequest extends OutgoingRequest { constructor(ruri, ua, params, extraHeaders, body) { super(JsSIP_C.INVITE, ruri, ua, params, extraHeaders, body); this.transaction = null; } cancel(reason) { this.transaction.cancel(reason); } clone() { const request = new InitialOutgoingInviteRequest(this.ruri, this.ua); Object.keys(this.headers).forEach(function(name) { request.headers[name] = this.headers[name].slice(); }, this); request.body = this.body; request.extraHeaders = Utils.cloneArray(this.extraHeaders); request.to = this.to; request.from = this.from; request.call_id = this.call_id; request.cseq = this.cseq; request.transaction = this.transaction; return request; } } class IncomingMessage { constructor() { this.data = null; this.headers = null; this.method = null; this.via = null; this.via_branch = null; this.call_id = null; this.cseq = null; this.from = null; this.from_tag = null; this.to = null; this.to_tag = null; this.body = null; this.sdp = null; } /** * Insert a header of the given name and value into the last position of the * header array. */ addHeader(name, value) { const header = { raw: value }; name = Utils.headerize(name); if (this.headers[name]) { this.headers[name].push(header); } else { this.headers[name] = [ header ]; } } /** * Get the value of the given header name at the given position. */ getHeader(name) { const header = this.headers[Utils.headerize(name)]; if (header) { if (header[0]) { return header[0].raw; } } else { return; } } /** * Get the header/s of the given name. */ getHeaders(name) { const headers = this.headers[Utils.headerize(name)]; const result = []; if (!headers) { return []; } for (const header of headers) { result.push(header.raw); } return result; } /** * Verify the existence of the given header. */ hasHeader(name) { return (this.headers[Utils.headerize(name)]) ? true : false; } /** * Parse the given header on the given index. * -param {String} name header name * -param {Number} [idx=0] header index * -returns {Object|undefined} Parsed header object, undefined if the header * is not present or in case of a parsing error. */ parseHeader(name, idx = 0) { name = Utils.headerize(name); if (!this.headers[name]) { logger.debug(`header "${name}" not present`); return; } else if (idx >= this.headers[name].length) { logger.debug(`not so many "${name}" headers present`); return; } const header = this.headers[name][idx]; const value = header.raw; if (header.parsed) { return header.parsed; } // Substitute '-' by '_' for grammar rule matching. const parsed = Grammar.parse(value, name.replace(/-/g, '_')); if (parsed === -1) { this.headers[name].splice(idx, 1); // delete from headers logger.debug(`error parsing "${name}" header field with value "${value}"`); return; } else { header.parsed = parsed; return parsed; } } /** * Message Header attribute selector. Alias of parseHeader. * -param {String} name header name * -param {Number} [idx=0] header index * -returns {Object|undefined} Parsed header object, undefined if the header * is not present or in case of a parsing error. * * -example * message.s('via',3).port */ s(name, idx) { return this.parseHeader(name, idx); } /** * Replace the value of the given header by the value. * -param {String} name header name * -param {String} value header value */ setHeader(name, value) { const header = { raw: value }; this.headers[Utils.headerize(name)] = [ header ]; } /** * Parse the current body as a SDP and store the resulting object * into this.sdp. * -param {Boolean} force: Parse even if this.sdp already exists. * * Returns this.sdp. */ parseSDP(force) { if (!force && this.sdp) { return this.sdp; } else { this.sdp = sdp_transform.parse(this.body || ''); return this.sdp; } } toString() { return this.data; } } class IncomingRequest extends IncomingMessage { constructor(ua) { super(); this.ua = ua; this.headers = {}; this.ruri = null; this.transport = null; this.server_transaction = null; } /** * Stateful reply. * -param {Number} code status code * -param {String} reason reason phrase * -param {Object} headers extra headers * -param {String} body body * -param {Function} [onSuccess] onSuccess callback * -param {Function} [onFailure] onFailure callback */ reply(code, reason, extraHeaders, body, onSuccess, onFailure) { const supported = []; let to = this.getHeader('To'); code = code || null; reason = reason || null; // Validate code and reason values. if (!code || (code < 100 || code > 699)) { throw new TypeError(`Invalid status_code: ${code}`); } else if (reason && typeof reason !== 'string' && !(reason instanceof String)) { throw new TypeError(`Invalid reason_phrase: ${reason}`); } reason = reason || JsSIP_C.REASON_PHRASE[code] || ''; extraHeaders = Utils.cloneArray(extraHeaders); if (this.ua.configuration.extra_headers) { extraHeaders = extraHeaders.concat(this.ua.configuration.extra_headers); } let response = `SIP/2.0 ${code} ${reason}\r\n`; if (this.method === JsSIP_C.INVITE && code > 100 && code <= 200) { const headers = this.getHeaders('record-route'); for (const header of headers) { response += `Record-Route: ${header}\r\n`; } } const vias = this.getHeaders('via'); for (const via of vias) { response += `Via: ${via}\r\n`; } if (!this.to_tag && code > 100) { to += `;tag=${Utils.newTag()}`; } else if (this.to_tag && !this.s('to').hasParam('tag')) { to += `;tag=${this.to_tag}`; } response += `To: ${to}\r\n`; response += `From: ${this.getHeader('From')}\r\n`; response += `Call-ID: ${this.call_id}\r\n`; response += `CSeq: ${this.cseq} ${this.method}\r\n`; for (const header of extraHeaders) { response += `${header.trim()}\r\n`; } // Supported. switch (this.method) { case JsSIP_C.INVITE: if (this.ua.configuration.session_timers) { supported.push('timer'); } if (this.ua.contact.pub_gruu || this.ua.contact.temp_gruu) { supported.push('gruu'); } supported.push('ice', 'replaces'); break; case JsSIP_C.UPDATE: if (this.ua.configuration.session_timers) { supported.push('timer'); } if (body) { supported.push('ice'); } supported.push('replaces'); } supported.push('outbound'); // Allow and Accept. if (this.method === JsSIP_C.OPTIONS) { response += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`; response += `Accept: ${JsSIP_C.ACCEPTED_BODY_TYPES}\r\n`; } else if (code === 405) { response += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`; } else if (code === 415) { response += `Accept: ${JsSIP_C.ACCEPTED_BODY_TYPES}\r\n`; } response += `Supported: ${supported}\r\n`; if (body) { const length = Utils.str_utf8_length(body); response += 'Content-Type: application/sdp\r\n'; response += `Content-Length: ${length}\r\n\r\n`; response += body; } else { response += `Content-Length: ${0}\r\n\r\n`; } this.server_transaction.receiveResponse(code, response, onSuccess, onFailure); } /** * Stateless reply. * -param {Number} code status code * -param {String} reason reason phrase */ reply_sl(code = null, reason = null) { const vias = this.getHeaders('via'); // Validate code and reason values. if (!code || (code < 100 || code > 699)) { throw new TypeError(`Invalid status_code: ${code}`); } else if (reason && typeof reason !== 'string' && !(reason instanceof String)) { throw new TypeError(`Invalid reason_phrase: ${reason}`); } reason = reason || JsSIP_C.REASON_PHRASE[code] || ''; let response = `SIP/2.0 ${code} ${reason}\r\n`; for (const via of vias) { response += `Via: ${via}\r\n`; } let to = this.getHeader('To'); if (!this.to_tag && code > 100) { to += `;tag=${Utils.newTag()}`; } else if (this.to_tag && !this.s('to').hasParam('tag')) { to += `;tag=${this.to_tag}`; } response += `To: ${to}\r\n`; response += `From: ${this.getHeader('From')}\r\n`; response += `Call-ID: ${this.call_id}\r\n`; response += `CSeq: ${this.cseq} ${this.method}\r\n`; if (this.ua.configuration.extra_headers) { for (const header of this.ua.configuration.extra_headers) { response += `${header.trim()}\r\n`; } } response += `Content-Length: ${0}\r\n\r\n`; this.transport.send(response); } } class IncomingResponse extends IncomingMessage { constructor() { super(); this.headers = {}; this.status_code = null; this.reason_phrase = null; } } module.exports = { OutgoingRequest, InitialOutgoingInviteRequest, IncomingRequest, IncomingResponse };