UNPKG

chai-sip

Version:
1,683 lines (1,323 loc) 61.5 kB
"use strict"; var sip = require("sip"); var digest = require("sip/digest"); var crypto = require("crypto"); var ip = require("ip"); var transform = require("sdp-transform"); var fs = require("fs"); var l = require("winston"); var util = require("util"); const { execFile } = require("child_process"); var mediatool; var mediaProcesses = {}; if (process.env.LOG_LEVEL) { l.level = process.env.LOG_LEVEL; } else { l.level = "warn"; } function clone(obj) { if (null == obj || "object" != typeof obj) { return obj; } var copy = obj.constructor(); for (var attr in obj) { if (obj.hasOwnProperty(attr)) { copy[attr] = obj[attr]; } } return copy; } function rstring() { return Math.floor(Math.random() * 1e6).toString(); } if (process.env.useMediatool) { var Mediatool = require("mediatool"); if (!mediatool) { mediatool = new Mediatool({rtpStart:30000,rtpEnd:31000}); mediatool.on("serverStarted", () => { l.verbose("mediatool started"); }); mediatool.start(); l.verbose("chai-sip started mediatool"); } } // Warning! This line has to be in this spot after the // mediatool require since it otherwise might // interact with a identical code line in mediatool. global.__basedir = __dirname; /// end warning function createHash(request) { let stringToHash = request.headers.from.uri+":"+request.headers.to.uri+":"+request.headers.cseq.seq; return crypto.createHash("md5").update(stringToHash).digest("hex"); } module.exports = function (chai, utils, sipStack) { var assert = chai.assert; if (!sipStack) { sip = require("sip"); } else { sip = sipStack; } utils.addMethod(chai.Assertion.prototype, "status", function (code) { var obj = utils.flag(this, "object"); this.assert( obj.status == code , "expected SIP response to have status code #{exp} but got #{act}" , "expected SIP response to not have status code #{act}" , code // expected , obj.status // actual ); return; }); assert.status = function (val, exp) { new chai.Assertion(val).to.be.status(exp); }; utils.addMethod(chai.Assertion.prototype, "method", function (method) { var obj = utils.flag(this, "object"); this.assert( obj.method == method , "expected SIP method to be #{exp} but got #{act}" , "expected SIP methid to not be #{act}" , method // expected , obj.method // actual ); //new Assertion(obj.status).to.equal(code); return; //new chai.Assertion(obj.status).to.be.equal(code); }); assert.method = function (val, exp) { new chai.Assertion(val).to.be.method(exp); }; chai.terminateMediatool = function () { if (mediatool) { mediatool.stop(0); } if (mediaProcesses) { for (let dialogId in mediaProcesses) { if (Array.isArray(mediaProcesses[dialogId])) { mediaProcesses[dialogId].forEach(function (mediaProcess) { try { process.kill(mediaProcess.pid); } catch(e) { l.warn("Could not kill mediaProcess.pid",mediaProcess.pid); } }); } } } }; chai.sip = function (params) { var mySip; var requestCallback; var ackCallback; var dialogs = {}; var request; var playing = {}; var prompt0 = __basedir + "/caller.wav"; var prompt1 = __basedir + "/callee.wav"; var mediaclient = {}; var currentMediaclient; var lastMediaId; var remoteUri; var sessionExpires; var reInviteDisabled; var refresherDisabled; var refreshUsingUpdate; var updateRefreshBody; var onRefreshFinalResponse; var lateOffer; var dropAck; var ackDelay=0; var useTelUri=false; var expirationTimers = {}; var sipParams = {}; var disabledMediaUsers = []; var dtmfCallback = params.dtmfCallback; var requestReady = false; var sipTransactionLog = {}; var sipTransactionIndex = 0; sipParams = params; sipParams.logger = { send: function (message) { l.debug("SND\n" + util.inspect(message, false, null)); }, recv: function (message) { l.debug("RCV\n" + util.inspect(message, false, null)); }, error: function (message) { l.error("ERR\n" + util.inspect(message, false, null)); } }; function handleTraceLogging (msg) { var wrappedRequestObj; let ts = Date.now(); var hash = createHash(msg); if (msg.headers["content-type"] == "application/sdp") { msg.content = transform.parse(msg.content); } if (!sipTransactionLog["transactionId_"+hash]) { wrappedRequestObj = { index: sipTransactionIndex, timeStamp: ts, transactionId: hash, request: [], provResp:[], finalResp:[], ack:[]}; sipTransactionLog["transactionId_"+hash] = wrappedRequestObj; sipTransactionLog["timestamp_"+ts] = wrappedRequestObj; sipTransactionLog["index_"+ sipTransactionIndex] = wrappedRequestObj; sipTransactionIndex++; } else { wrappedRequestObj = sipTransactionLog["transactionId_"+hash]; } if (msg.method) { if (msg.method === "ACK") { wrappedRequestObj.ack.push(msg); } else { wrappedRequestObj.request.push(msg); } } else { // handle responses if (msg.status < 200) { // provsional responses wrappedRequestObj.provResp.push(msg); } else { // final responses wrappedRequestObj.finalResp.push(msg); } } } function wrappedSipSend (messageOut, callback) { handleTraceLogging (clone(messageOut)); mySip.send(messageOut, function (messageIn) { handleTraceLogging(clone(messageIn)); if (callback) { callback(messageIn); } }); } function stopMedia(id) { l.verbose("stopMedia called, id", id); if (process.env.useMediatool) { if (mediaclient[id]) { if(Array.isArray(mediaclient[id])) { for(let mc of mediaclient[id]) { mc.stop(); } } else { mediaclient[id].stop(); } delete mediaclient[id]; return; } } if(mediaProcesses[id]) { for(var pid of mediaProcesses[id]) { try{ l.verbose("Stopping mediaprocess... " + pid.pid); process.kill(pid.pid); } catch(err) { if(!err.code=="ESRCH") { l.verbose("Error killing process",JSON.stringify(err)); } } } delete mediaProcesses[id]; return; } l.warn("No matching mediaclient for " + id); } function getGstStrFromSdpMedia(dialogId, sdpMedia, ip, prompt) { const encrypter = ["RTP/SAVP", "RTP/SAVPF"].includes(sdpMedia.protocol) && sdpMedia.crypto.length > 0 ? `! srtpenc key="${sdpMedia.crypto[0].config.split("|")[0].slice(7)}" ` : ""; let gstStr; for (const rtpPayload of sdpMedia.rtp) { if (rtpPayload.codec.toUpperCase() === "PCMA") { gstStr = `-m multifilesrc name=${dialogId} location=${prompt} do-timestamp=true loop=1 ! wavparse ignore-length=1 ! audioresample ! audioconvert ! capsfilter caps="audio/x-raw,format=(string)S16LE,rate=(int)8000,channel-mask=(bitmask)0x0000000000000000,channels=(int)1,layout=(string)interleaved" ! alawenc ! rtppcmapay min-ptime=20000000 max-ptime=20000000 ptime-multiple=20000000 ! capsfilter caps="application/x-rtp,media=(string)audio,maxptime=(uint)20,encoding-name=(string)PCMA,payload=(int)8,clock-rate=(int)8000" ${encrypter}! udpsink host=${ip} port=${sdpMedia.port}`; l.debug("Will send PCMA codec"); break; } else if (rtpPayload.codec.toUpperCase() === "PCMU") { gstStr = `-m multifilesrc name=${dialogId} location=${prompt} loop=1 ! wavparse ignore-length=1 ! audioresample ! audioconvert ! capsfilter caps="audio/x-raw,format=(string)S16LE,rate=(int)8000,channel-mask=(bitmask)0x0000000000000000,channels=(int)1,layout=(string)interleaved" ! mulawenc ! rtppcmupay min-ptime=20000000 max-ptime=20000000 ! capsfilter caps="application/x-rtp,media=(string)audio,maxptime=(uint)20,encoding-name=(string)PCMU,payload=(int)0,clock-rate=(int)8000" ${encrypter}! udpsink host=${ip} port=${sdpMedia.port}`; l.debug("Will send PCMU codec"); break; } else if (rtpPayload.codec.toUpperCase() === "G722") { gstStr = `-m multifilesrc name=${dialogId} location=${prompt} loop=1 ! wavparse ignore-length=1 ! audioresample ! audioconvert ! avenc_g722 name=rtpenc ! rtpg722pay name=rtppay min-ptime=20000000 max-ptime=20000000 ptime-multiple=20000000 ! capsfilter name=rtpcaps caps="application/x-rtp,media=(string)audio,encoding-name=(string)G722,payload=(int)9,clock-rate=(int)8000" ${encrypter}! udpsink host=${ip} port=${sdpMedia.port}`; l.debug("Will send G722 codec"); break; } else if (rtpPayload.codec.toUpperCase() === "OPUS") { gstStr = `-m multifilesrc name=${dialogId} location=${prompt} loop=1 ! wavparse ignore-length=1 ! audioresample ! audioconvert ! capsfilter caps="audio/x-raw,format=(string)S16LE,rate=(int)8000,channel-mask=(bitmask)0x0000000000000000,channels=(int)1,layout=(string)interleaved" ! opusenc ! rtpopuspay pt=${rtpPayload.payload} min-ptime=20000000 max-ptime=20000000 ${encrypter}! udpsink host=${ip} port=${sdpMedia.port}`; l.debug("Will send OPUS codec"); break; } } return gstStr; } function playGstMedia(dialogId, sdpMedia, sdpOrigin, prompt) { l.verbose("media: play GST RTP audio for", JSON.stringify(sdpMedia, null, 2)); const ip = sdpMedia.connection?.ip ?? sdpOrigin; const gstStr = getGstStrFromSdpMedia(dialogId, sdpMedia, ip, prompt); l.debug("Will send media to " + ip + ":" + sdpMedia.port); const gstArr = gstStr.split(" "); l.debug("gstArr", JSON.stringify(gstArr)); const pid = execFile("gst-launch-1.0", gstArr, (err) => { if (err) { if (err.signal != "SIGTERM") { l.error("Could not execute gst-launch-1.0", JSON.stringify(err), null, 2); } return; } l.debug("Completed gst-launch-1.0"); }); l.verbose("RTP audio playing, pid ", dialogId); if (!mediaProcesses[dialogId]) { mediaProcesses[dialogId] = []; } if (!pid) { throw "Could not start gst-launch-1.0"; } else { mediaProcesses[dialogId].push(pid); lastMediaId = dialogId; } } function setDtmfPt(pt,dialogId) { let client; l.verbose("setDtmfPt",dialogId) if(dialogId) { client = mediaclient[dialogId]; } else if (currentMediaclient) { l.verbose("currentMediaclient localPort",currentMediaclient.localPort) client = currentMediaclient; } if(client) { client.setDtmfPt(pt); } else { l.error("chai-sip is not configured with mediatool media component. setDtmfPt is not implemented without it."); } } function sendDTMF(digit,duration=80.0,dialogId) { let client; if(dialogId) { client = mediaclient[dialogId]; } else if (currentMediaclient) { l.verbose("currentMediaclient localPort",currentMediaclient.localPort) client = currentMediaclient; } if(client) { if(Array.isArray(client)) { client[0].sendDTMF(digit,duration); } else { client.sendDTMF(digit,duration); } } else { l.error("chai-sip is not configured with mediatool media component. This is not implemented without it."); } } function sendTone(frequency,duration,dialogId) { let client; if(dialogId) { client = mediaclient[dialogId]; } else if (currentMediaclient) { l.verbose("currentMediaclient localPort",currentMediaclient.localPort) client = currentMediaclient; } if(client) { if(Array.isArray(client)) { client[0].sendTone(frequency,duration); } else { client.sendTone(frequency,duration); } } else { l.error("chai-sip is not configured with mediatool media component. This is not implemented without it."); } } function getGstStrFromSdpMediaPcap(dialogId, sdpMedia, ip, pcapFile) { let gstStr; const encrypter = ["RTP/SAVP", "RTP/SAVPF"].includes(sdpMedia.protocol) && sdpMedia.crypto.length > 0 ? `! srtpenc key="${sdpMedia.crypto[0].config.split("|")[0].slice(7)}" ` : ""; for (const rtpPayload of sdpMedia.rtp) { if (rtpPayload.codec.toUpperCase() === "PCMA") { gstStr = `filesrc name=${dialogId} location=${pcapFile} ! pcapparse ! capsfilter caps="application/x-rtp,media=(string)audio,encoding-name=(string)PCMA,payload=(int)8,clock-rate=(int)8000" ${encrypter}! udpsink host=${ip} port=${sdpMedia.port}`; l.debug("Will send PCMA codec"); break; } else if (rtpPayload.codec.toUpperCase() === "PCMU") { gstStr = `filesrc name=${dialogId} location=${pcapFile} ! pcapparse ! capsfilter caps="application/x-rtp,media=(string)audio,encoding-name=(string)PCMU,payload=(int)0,clock-rate=(int)8000" ${encrypter}! udpsink host=${ip} port=${sdpMedia.port}`; l.debug("Will send PCMU codec"); break; } else if (rtpPayload.codec.toUpperCase() === "G722") { gstStr = `filesrc name=${dialogId} location=${pcapFile} ! pcapparse ! capsfilter caps="application/x-rtp,media=(string)audio,encoding-name=(string)G722,payload=(int)9,clock-rate=(int)8000" ${encrypter}! udpsink host=${ip} port=${sdpMedia.port}`; l.debug("Will send G722 codec"); break; } else if (rtpPayload.codec.toUpperCase() === "OPUS") { gstStr = `filesrc name=${dialogId} location=${pcapFile} ! pcapparse ! capsfilter caps="application/x-rtp,encoding-name=OPUS,payload=(int)99,media=(string)audio,clock-rate=(int)48000" ${encrypter}! udpsink host=${ip} port=${sdpMedia.port}`; l.debug("Will send OPUS codec"); break; } } return gstStr; } function playPcapFile(dialogId, sdpMedia, sdpOrigin, pcapFile) { l.verbose("media: play pcapFile for", JSON.stringify(sdpMedia, null, 2)); sipParams.pcapFile = pcapFile; const ip = sdpMedia.connection?.ip ?? sdpOrigin; l.verbose("Send pcap to ", ip, "listen on port ", sipParams.rtpPort); const gstStr = getGstStrFromSdpMediaPcap(dialogId, sdpMedia, ip, pcapFile); l.debug("Will send pcap to " + ip + ":" + sdpMedia.port); const gstArr = gstStr.split(" "); l.verbose("gstArr", JSON.stringify(gstArr)); //var packetSize = 172;//sdp.media[0].ptime*8; //var pid =exec(ffmpeg.path + " -stream_loop -1 -re -i "+ prompt +" -filter_complex 'aresample=8000,asetnsamples=n="+packetSize+"' -ac 1 -vn -acodec pcm_alaw -f rtp rtp://" + ip + ":" + sdpMedia.port , (err, stdout, stderr) => { var pid = execFile("gst-launch-1.0", gstArr, (err) => { if (err) { if (err.signal != "SIGTERM") { l.error("Could not execute gst-launch-1.0", JSON.stringify(err), null, 2); } return; } l.debug("Completed gst-launch-1.0"); // the *entire* stdout and stderr (buffered) //l.debug("gst-launch-1.0 stdout:",stdout); //l.debug("gst-launch-1.0 stderr:",stderr); }); l.debug("RTP pcap playing, pid "); if (!mediaProcesses[dialogId]) { mediaProcesses[dialogId] = []; } if (!pid) { throw "Could not start gst-launch"; } else { mediaProcesses[dialogId].push(pid); lastMediaId = dialogId; } } function createPipeline(dialogId,siprecIndex=0) { return new Promise( (resolve) => { if (process.env.useMediatool) { if(mediaclient[dialogId] && siprecIndex>0) { l.info("Mediaclient already running for dialogId",dialogId); } l.verbose("createPipeline called, using mediatool", dialogId); const msparams = { pipeline: sipParams.clientType === "webrtc" ? "webrtc" : "dtmfclient", dialogId: dialogId}; mediatool.createPipeline(msparams, (client,localPort) => { client.on("pipelineStarted", () => { l.verbose("dtmfclient pipelineStarted"); }); client.on("error", (err) => { l.error("dtmfclient error", err); }); client.on("stopped", (params) => { l.verbose("dtmfclient mediatool client stopped", JSON.stringify(params)); }); client.on("rtpTimeout", (params) => { l.verbose("Got rtpTimeout event for ", params, ", will stop IVR with timeoutreason"); }); client.on("promptPlayed", (params) => { l.verbose("Prompt playout complete", JSON.stringify(params)); }); client.on("dtmf", (args) => { l.verbose("mediatool dtmfclient got dtmf",args); if(dtmfCallback) { dtmfCallback(args); } }); if(siprecIndex===0) { mediaclient[dialogId] = client; } else if(siprecIndex==1){ mediaclient[dialogId] = [client] } else if(siprecIndex==2) { mediaclient[dialogId].push(client) } currentMediaclient = mediaclient[dialogId]; client.localPort = localPort; l.verbose("createPipeline localPort",localPort); resolve(localPort) }); } else { resolve(sipParams.rtpPort) } }); } function getDtmfPt(sdpMedia) { let dtmfPt; if(sdpMedia && sdpMedia.rtp && Array.isArray(sdpMedia.rtp)) { for(let rtp of sdpMedia.rtp) { if(rtp.codec=="telephone-event") { return rtp.payload; } } } } function playMedia(dialogId, sdpMedia, sdpOrigin, prompt,channel=0) { if (process.env.useMediatool) { l.verbose("playMedia called, using mediatool", dialogId, prompt); let ip; if (sdpMedia.connection) { ip = sdpMedia.connection.ip; } else { ip = sdpOrigin; } if (ip === "0.0.0.0") { l.verbose("Got hold SDP, not playing media"); resolve(); return; } let remoteCodec = "PCMA"; let remotePt = 8; if(sdpMedia.rtp && sdpMedia.rtp[0] && (sdpMedia.rtp[0].codec === "PCMU" || sdpMedia.rtp[0].codec === "G722" || sdpMedia.rtp[0].codec?.toUpperCase() === "OPUS")) { remoteCodec = sdpMedia.rtp[0].codec; remotePt = sdpMedia.rtp[0].payload; } l.debug("playMedia sdpMedia",JSON.stringify(sdpMedia,null,2),"channel",channel); let remoteDtmfPt = getDtmfPt(sdpMedia); const msparams = { pipeline: sipParams.clientType === "webrtc" ? "webrtc" : "dtmfclient", dialogId: dialogId, remoteIp: ip, remotePort: sdpMedia.port, prompt: prompt, remoteCodec: remoteCodec, remotePt: remotePt, remoteDtmfPt:remoteDtmfPt }; if(mediaclient && mediaclient[dialogId]) { if(Array.isArray(mediaclient[dialogId])) { msparams.dialogId = msparams.dialogId+":"+channel; mediaclient[dialogId][channel].start(msparams); } else { mediaclient[dialogId].start(msparams); } } else { l.info("No mediaclient found for ",dialogId); } } else { playGstMedia(dialogId, sdpMedia, sdpOrigin, prompt); } } function gotFinalResponse(response, callback) { l.verbose("Function gotFinalResponse"); try { if (callback) { callback(response); } } catch (e) { l.error("Error", e); throw e; } } function getInviteBody(params = {}) { if(params.body) { l.verbose("getInviteBody returning passed body.",params.body) return params.body; } const rtpAddress = params.rtpAddress ?? sipParams.rtpAddress ?? ip.address(); const rtpPort= params.rtpPort ?? sipParams.rtpPort ?? 30000; const protocol= params.protocol ?? sipParams.protocol ?? "RTP/AVP"; let pt = 8; let codec = "PCMA"; if (sipParams.codec === "PCMU" || params.codec === "PCMU") { pt = 0; codec = "PCMU"; } else if (sipParams.codec === "G722" || params.codec === "G722") { pt = 9; codec = "G722"; } else if (sipParams.codec === "opus" || (params.codec && params.codec.toLowerCase() === "opus")) { pt = 111; codec = "opus"; } const body = [ "v=0", `o=- ${rstring()} ${rstring()} IN IP4 ${rtpAddress}`, "s=-", `c=IN IP4 ${rtpAddress}`, "t=0 0", `m=audio ${rtpPort} ${protocol} ${pt} 101`, `a=rtpmap:${pt} ${codec}/8000`, "a=ptime:20", "a=sendrecv", "a=rtpmap:101 telephone-event/8000", "a=fmtp:101 0-15", "a=ptime:20", "a=sendrecv" ].join("\r\n"); return body; } function makeRequest(method, destination, headers, contentType, body, user,params={}) { l.debug("makeRequest", method); var ipAddress; if (!sipParams.publicAddress) { ipAddress = ip.address(); } else { ipAddress = sipParams.publicAddress; } let contactUser = sipParams.userid; if (user) { contactUser = user; } let contactObj = { uri: "sip:"+contactUser+"@" + ipAddress + ":" + (sipParams.tunnelPort || sipParams.port) + ";transport="+sipParams.transport, params: {} }; if(params.regId && params.instanceId) { contactObj.params["+sip.instance"] = `"${params.instanceId}"`; contactObj.params["reg-id"] = params.regId; } if(params.qValue) { contactObj.params.q = params.qValue; } let callId = rstring() + Date.now().toString(); if(params.callId) { callId = params.callId; } var req = { method: method, uri: destination, headers: { to: { uri: destination + ";transport=" + sipParams.transport }, from: { uri: "sip:" + sipParams.userid + "@" + sipParams.domain + "", params: { tag: rstring() } }, "call-id": callId, cseq: { method: method, seq: Math.floor(Math.random() * 1e5) }, contact: [contactObj], // via: createVia(), "max-forwards": 70 } }; if(sipParams.displayName) { req.headers.from.name = sipParams.displayName; } if(params.fromHeader) { req.headers.from = params.fromHeader; } if(params.toHeader) { req.headers.to = params.toHeader; } l.debug("req", JSON.stringify(req)); if (sipParams.headers) { if (sipParams.headers.route) { l.debug("sipParams.headers.route", sipParams.headers.route); req.headers.route = sipParams.headers.route; } } if (headers) { req.headers = Object.assign(req.headers, headers); } if (body) { if (!contentType) { throw "Content type is missing"; } req.content = body; req.headers["content-type"] = contentType; } else if (method == "INVITE" && !lateOffer) { req.content = getInviteBody(params); req.headers["content-type"] = "application/sdp"; } for (var key in headers) { req.headers[key] = headers[key]; } return req; } async function playIncomingReqMedia(rq) { let lp; if (!rq.content) return; var sdp = transform.parse(rq.content); if (sdp && !(sipParams.disableMedia)) { var id = rq.headers["call-id"]; l.verbose("media: playIncomingReqMedia for ", rq.method, rq.uri, id,sipParams.mediaFile); if(process.env.useMediatool) { lp = await createPipeline(id); } l.verbose("lp",lp); let mediaFile = prompt0; if(sipParams.mediaFile) { mediaFile = sipParams.mediaFile; } let rqUser; if(rq.uri) { rqUser = sip.parseUri(rq.uri).user } if(disabledMediaUsers.indexOf(rqUser)>=0) { l.verbose("Media disabled for user",rqUser,disabledMediaUsers); return; } if(sipParams.mediaFileConfig && sipParams.mediaFileConfig[rqUser]) { mediaFile = sipParams.mediaFileConfig[rqUser] l.verbose("Setting mediaFile from mediaFileConfig",mediaFile) } if (sdp.media[0].type == "audio") { if(sipParams.mediaDelay) { l.verbose("Will delay media playout for request") setTimeout(() => { playMedia(id, sdp.media[0], sdp.origin.address, mediaFile); }, sipParams.mediaDelay*1000); } else { playMedia(id, sdp.media[0], sdp.origin.address, mediaFile); } } if (sdp.media.length > 1) { if (sdp.media[1].type == "audio") { playMedia(id, sdp.media[1], sdp.origin.address, prompt1); } } return lp; } else { l.verbose("Media disabled"); } } function sendUpdateForRequest(req, seq) { var ipAddress; if (!sipParams.publicAddress) { ipAddress = ip.address(); } else { ipAddress = sipParams.publicAddress; } var to; var from; if (req.method) { to = req.headers.from; from = req.headers.to; } else { to = req.headers.to; from = req.headers.from; } let seqVal; if (seq) { seqVal = seq; } else { req.headers.cseq.seq++; seqVal = req.headers.cseq.seq; } var update = { method: "UPDATE", uri: req.headers.contact[0].uri, headers: { to: to, from: from, supported: "timer", "Session-Expires": "900;refresher=uac", "call-id": req.headers["call-id"], "Min-SE": 900, cseq: { method: "INVITE", seq: seqVal }, contact: [{ uri: "sip:" + sipParams.userid + "@" + ipAddress + ":" + (sipParams.tunnelPort || sipParams.port) + ";transport=" + sipParams.transport }], } }; if (req.headers["record-route"]) { update.headers["route"] = []; if (req.method) { for (let i = 0; i < req.headers["record-route"].length; i++) { l.debug("Push bye rr header", req.headers["record-route"][i]); update.headers["route"].push(req.headers["record-route"][i]); } } else { for (let i = req.headers["record-route"].length - 1; i >= 0; i--) { l.debug("Push bye rr header", req.headers["record-route"][i]); update.headers["route"].push(req.headers["record-route"][i]); } } } l.verbose("Send UPDATE request", JSON.stringify(update, null, 2)); //var id = [req.headers["call-id"]].join(":"); request = update; wrappedSipSend(update, (rs) => { l.verbose("Received UPDATE response", JSON.stringify(rs, null, 2)); }); return update; } function sendReinviteForRequest(req, seq, params, callback, ackCallback) { let ipAddress; if (!sipParams.publicAddress) { ipAddress = ip.address(); } else { ipAddress = sipParams.publicAddress; } let to; let from; if (req.method) { to = req.headers.from; from = req.headers.to; } else { to = req.headers.to; from = req.headers.from; } let seqVal; if (seq) { seqVal = seq; } else { req.headers.cseq.seq++; seqVal = req.headers.cseq.seq; } let contact = [{ uri: "sip:" + sipParams.userid + "@" + ipAddress + ":" + (sipParams.tunnelPort || sipParams.port) + ";transport=" + sipParams.transport }]; if(params.contact) { contact = params.contact; } const reinvite = { method: "INVITE", uri: req.headers.contact[0].uri, headers: { to: to, from: from, "call-id": req.headers["call-id"], cseq: { method: "INVITE", seq: seqVal }, contact: contact, } }; if ((params.body || params.codec || params.rtpAddress || params.rtpPort) && params.lateOffer != true) { reinvite.content = getInviteBody(params); if (params.contentType != null) { reinvite.headers["content-type"] = params.contentType; } else { reinvite.headers["content-type"] = "application/sdp"; } } if (req.headers["record-route"]) { reinvite.headers["route"] = []; if (req.method) { for (let i = 0; i < req.headers["record-route"].length; i++) { l.debug("Push bye rr header", req.headers["record-route"][i]); reinvite.headers["route"].push(req.headers["record-route"][i]); } } else { for (let i = req.headers["record-route"].length - 1; i >= 0; i--) { l.debug("Push bye rr header", req.headers["record-route"][i]); reinvite.headers["route"].push(req.headers["record-route"][i]); } } } l.verbose("Send reinvite request", JSON.stringify(reinvite, null, 2)); //var id = [req.headers["call-id"]].join(":"); request = reinvite; if(params.disableMedia) { l.verbose("Stopping media for reinvite"); const id = req.headers["call-id"]; stopMedia(id); } wrappedSipSend(reinvite, (rs) => { ackDelay = params.ackDelay || 0; l.verbose("Received reinvite response", JSON.stringify(rs, null, 2), "ackDelay", ackDelay); if (callback) { l.verbose("Call reInvite callback"); callback(rs); } let lateOfferSdp = params.lateOfferSdp; if (params.lateOfferSdp === true) { lateOfferSdp = getInviteBody(); } console.log("sip.send ack params", params); if ((params.body || params.codec || params.rtpAddress || params.rtpPort) && params.lateOffer == true) { lateOfferSdp = getInviteBody(params); console.log("lateOfferSdp", lateOfferSdp); } sendAck(rs, lateOfferSdp, ackCallback); }); return reinvite; } function sendBye(req, byecallback) { var to; var from; if (req.method) { to = req.headers.from; from = req.headers.to; } else { to = req.headers.to; from = req.headers.from; } req.headers.cseq.seq++; var bye = { method: "BYE", uri: req.headers.contact[0].uri, headers: { to: to, from: from, "call-id": req.headers["call-id"], cseq: { method: "BYE", seq: req.headers.cseq.seq } } }; //bye.headers["via"] = [req.headers.via[2]]; if (req.headers["record-route"]) { bye.headers["route"] = []; if (req.method) { for (let i = 0; i < req.headers["record-route"].length; i++) { l.debug("Push bye rr header", req.headers["record-route"][i]); bye.headers["route"].push(req.headers["record-route"][i]); } } else { for (let i = req.headers["record-route"].length - 1; i >= 0; i--) { l.debug("Push bye rr header", req.headers["record-route"][i]); bye.headers["route"].push(req.headers["record-route"][i]); } } } l.verbose("Send BYE request", JSON.stringify(bye, null, 2)); var id = req.headers["call-id"]; request = bye; stopMedia(id); l.debug("after stopmedia"); wrappedSipSend(bye, (rs) => { l.verbose("Received bye response", JSON.stringify(rs, null, 2)); if (byecallback) { byecallback(rs); l.verbose("Bye response callback called"); } }); return bye; } function sendInfoInDialog(req,body,contentType, infocallback) { var to; var from; if (req.method) { to = req.headers.from; from = req.headers.to; } else { to = req.headers.to; from = req.headers.from; } req.headers.cseq.seq++; var info = { method: "INFO", uri: req.headers.contact[0].uri, headers: { to: to, from: from, "call-id": req.headers["call-id"], cseq: { method: "INFO", seq: req.headers.cseq.seq }, "content-type":contentType } }; info.content = body; //info.headers["via"] = [req.headers.via[2]]; if (req.headers["record-route"]) { info.headers["route"] = []; if (req.method) { for (let i = 0; i < req.headers["record-route"].length; i++) { l.debug("Push info rr header", req.headers["record-route"][i]); info.headers["route"].push(req.headers["record-route"][i]); } } else { for (let i = req.headers["record-route"].length - 1; i >= 0; i--) { l.debug("Push info rr header", req.headers["record-route"][i]); info.headers["route"].push(req.headers["record-route"][i]); } } } l.verbose("Send INFO request", JSON.stringify(info, null, 2)); var id = req.headers["call-id"]; request = info; wrappedSipSend(info, (rs) => { l.verbose("Received info response", JSON.stringify(rs, null, 2)); if (infocallback) { infocallback(rs); l.verbose("INFO response callback called"); } }); return info; } function sendCancel(req, callback) { var cancel = { method: "CANCEL", uri: request.uri, headers: { to: request.headers.to, via: request.headers.via, from: request.headers.from, "call-id": request.headers["call-id"], cseq: { method: "CANCEL", seq: request.headers.cseq.seq } } }; if (request.headers["route"]) { cancel.headers["route"] = request.headers["route"]; } request = cancel; wrappedSipSend(cancel, function (rs) { l.verbose("Received CANCEL response", JSON.stringify(rs, null, 2)); if (callback) { callback(rs); } }); return cancel; } function sendAck(rs, sdp, callback) { l.verbose("Generate ACK reply for response", rs); if (dropAck) { l.verbose("Dropping ack, dropAck is true..."); return; } var headers = { to: rs.headers.to, from: rs.headers.from, "call-id": rs.headers["call-id"], cseq: { method: "ACK", seq: rs.headers.cseq.seq } }; if(sipParams && sipParams.headers) { headers = {...sipParams.headers,...headers}; } l.verbose("Headers", JSON.stringify(headers,null,2)); let body; if (sdp) { body = sdp; } else { body = getInviteBody(); } var ack; remoteUri = remoteUri || rs.headers.contact[0].uri; if (lateOffer || sdp) ack = makeRequest("ACK", remoteUri, headers, "application/sdp", body); else ack = makeRequest("ACK", remoteUri, headers, null, null); l.debug("ACK", ack); //ack.headers["via"] = rs.headers.via; /*if(ack.headers["via"][0].params) { delete ack.headers["via"][0].params.received; }*/ delete ack.headers["via"]; if (rs.headers["record-route"]) { ack.headers["route"] = []; for (var i = rs.headers["record-route"].length - 1; i >= 0; i--) { l.debug("Push ack header", rs.headers["record-route"][i]); ack.headers["route"].push(rs.headers["record-route"][i]); } } var ipAddress; if (!sipParams.publicAddress) { ipAddress = ip.address(); } else { ipAddress = sipParams.publicAddress; } if(headers && headers.contact) { let uri = sip.parseUri(headers.contact); l.verbose("parsed contact uri",uri); ack.headers.contact = headers.contact; } else { ack.headers.contact = [{ uri: "sip:" + sipParams.userid + "@" + ipAddress + ":" + (sipParams.tunnelPort || sipParams.port) + ";transport=" + sipParams.transport }]; } l.verbose("Send ACK reply", JSON.stringify(ack, null, 2)); if(ackDelay) { l.info("Using ackDelay",ackDelay); } setTimeout(() => { wrappedSipSend(ack); if(callback) { callback(); } },ackDelay * 1000); } function handle200(rs, disableMedia = false) { l.verbose("handle200",rs); // yes we can get multiple 2xx response with different tags if (rs.headers.cseq.method != "INVITE") { return; } l.debug("call " + rs.headers["call-id"] + " answered with tag " + rs.headers.to.params.tag); request.headers.to = rs.headers.to; request.uri = rs.headers.contact[0].uri; if (rs.headers["record-route"]) { request.headers["route"] = []; for (var i = rs.headers["record-route"].length - 1; i >= 0; i--) { l.debug("Push invite route header", rs.headers["record-route"][i]); request.headers["route"].push(rs.headers["record-route"][i]); } } remoteUri = rs.headers.contact[0].uri; // sending ACK sendAck(rs); l.debug("200 resp", JSON.stringify(rs, null, 2)); var id = rs.headers["call-id"]; l.verbose("200 response for ", id); if (rs.headers["content-type"] == "application/sdp") { var sdp = transform.parse(rs.content); l.verbose("Got SDP in 200 answer", sdp); l.verbose("Disablemedia:", disableMedia); if (!(sipParams.disableMedia || disableMedia)) { l.verbose("media: 200 response playMedia for ", id); if (sipParams.pcapFile) { let delay = 2000; if(sipParams.pcapDelay) { delay = delay + sipParams.pcapDelay * 1000; } setTimeout(() => { l.verbose("Starting playPcapFile",sipParams.pcapFile); playPcapFile(id, sdp.media[0], sdp.origin.address, sipParams.pcapFile); }, delay); return; } if(sipParams.callerPcap || sipParams.calleePcap) { if (sipParams.callerPcap) { setTimeout(() => { playPcapFile(id, sdp.media[0], sdp.origin.address, sipParams.callerPcap); }, 2000); } if (sipParams.calleePcap) { setTimeout(() => { playPcapFile(id, sdp.media[1], sdp.origin.address, sipParams.calleePcap); }, 2000); } return; } if (sipParams.mediaFile) { l.verbose("Starting mediaFile", sipParams.mediaFile); prompt0 = sipParams.mediaFile; } if (sdp.media[0].type == "audio") { if(sipParams.mediaDelay) { l.verbose("Will delay media playout with ",sipParams.mediaDelay,"seconds") setTimeout(() => { playMedia(id, sdp.media[0], sdp.origin.address, prompt0); }, sipParams.mediaDelay * 1000); } else { playMedia(id, sdp.media[0], sdp.origin.address, prompt0); } } if (sdp.media.length > 1) { if (sdp.media[1].type == "audio") { playMedia(id, sdp.media[1], sdp.origin.address, prompt1,1); } } } else { l.verbose("Media disabled"); } } // registring our 'dialog' which is just function to process in-dialog requests if (!dialogs[id]) { dialogs[id] = function (rq) { if (rq.method === "BYE") { l.verbose("call received bye"); delete dialogs[id]; delete playing[rs["call-id"]]; stopMedia(id); wrappedSipSend(sip.makeResponse(rq, 200, "Ok")); } else { wrappedSipSend(sip.makeResponse(rq, 405, "Method not allowed")); } }; } } function replyToDigest(request, response, callback, provisionalCallback) { l.verbose("replyToDigest", request.uri); if (sipParams.headers) { if (sipParams.headers.route) { l.debug("Update route header"); request.headers.route = sipParams.headers.route; } } delete request.headers.via; var session = { nonce: "" }; var creds; let realm; if(response.headers["www-authenticate"]) { realm = JSON.parse(response.headers["www-authenticate"][0].realm); } else if (response.headers["proxy-authenticate"]) { realm = JSON.parse(response.headers["proxy-authenticate"][0].realm); } l.debug("Response realm",realm); if (sipParams.authInfo) { let user = sip.parseUri(request.headers.from.uri).user; if (sipParams.authInfo[user]) { creds = { user: user, password: sipParams.authInfo[user], realm: realm, nonce: "", uri: "" }; } } else { creds = { user: sipParams.userid, password: sipParams.password, realm: realm, nonce: "", uri: "" }; } l.debug("creds",creds); digest.signRequest(session, request, response, creds); l.verbose("Sending request again with authorization header", JSON.stringify(request, null, 2)); wrappedSipSend(request, function (rs) { l.debug("Received after sending authorized request: " + rs.status); if (rs.status < 200) { if (provisionalCallback) { provisionalCallback(rs); } } if (rs.status == 200) { handle200(rs); gotFinalResponse(rs, callback); } else if (rs.status > 200) { stopMedia(rs.headers["call-id"]); gotFinalResponse(rs, callback); //sendAck(rs); } } ); } function startSessionRefresher(rq, callId, lastSeq) { l.verbose("startSessionRefresher"); if (!reInviteDisabled) { expirationTimers[callId] = setTimeout(() => { l.verbose("lastSeq", lastSeq); var nextSeq = lastSeq + 1; l.verbose("nextSeq", nextSeq); let rqCopy = sip.copyMessage(rq); delete rqCopy.headers.via; if (refreshUsingUpdate) { rqCopy.method = "UPDATE"; rqCopy.headers.cseq.method = "UPDATE"; if (!updateRefreshBody) { delete rqCopy.content; delete rqCopy.headers["content-type"]; delete rqCopy.headers["content-length"]; } } rqCopy.headers.cseq.seq = nextSeq; wrappedSipSend(rqCopy, (rs) => { if (rs.status >= 200 && onRefreshFinalResponse) { onRefreshFinalResponse(rs); } if (rs.status == 200) { startSessionRefresher(rqCopy, callId, nextSeq); if (rqCopy.method == "INVITE") { sendAck(rs); } } }); }, sessionExpires * 1000 / 2); } } function convertToTelUri(headerValue) { console.log("convertToTelUri",headerValue); let converted = headerValue; if(headerValue && headerValue.uri){ let parsed = sip.parseUri(headerValue.uri); console.log("parsed tel:",parsed); if(parsed.schema=="sip") { parsed.schema="tel"; delete parsed.host; parsed.params = headerValue.params; converted = sip.stringifyUri(parsed); } } return converted; } function sendRequest(rq, callback, provisionalCallback, disableMedia = false) { if (sessionExpires) { rq.headers["session-expires"] = sessionExpires; if (!refresherDisabled) { rq.headers["session-expires"] += ";refresher=uac"; } rq.headers.supported = "timer"; } if(useTelUri) { rq.headers.to = convertToTelUri(rq.headers.to); rq.headers.from = convertToTelUri(rq.headers.from); } l.verbose("Sending"); l.verbose(JSON.stringify(rq, null, 2), "\n\n"); wrappedSipSend(rq, function (rs) { l.verbose("Got response " + rs.status + " for callid " + rs.headers["call-id"]); if (rs.status < 200) { if (provisionalCallback) { l.debug("Calling provisionalCallback callback"); provisionalCallback(rs); } return; } if (rs.status == 401 || rs.status == 407) { l.verbose("Received auth response"); l.verbose(JSON.stringify(rs, null, 2)); replyToDigest(rq, rs, callback, provisionalCallback); return; } if (rs.status >= 300) { l.verbose("call failed with status " + rs.status); if (rq.method == "INVITE") { //sendAck(rs); } stopMedia(rs.headers["call-id"]); gotFinalResponse(rs, callback); return; } else if (rs.status < 200) { l.verbose("call progress status " + rs.status + " " + rs.reason); return; } else { l.verbose("Got final response"); if (sessionExpires) { let rqCopy = sip.copyMessage(rq); rqCopy.headers.cseq = rs.headers.cseq; rqCopy.headers.to = rs.headers.to; startSessionRefresher(rqCopy, rs.headers["call-id"], rs.headers.cseq.seq); } if(rs.status==200) { handle200(rs, disableMedia); } gotFinalResponse(rs, callback); } }); } l.debug("chai-sip params", params); if (!sipParams.publicAddress) { sipParams.publicAddress = ip.address(); } try { sip.start(sipParams, async function (rq) { let resend = false; var ts = Date.now(); l.debug("Received request", rq); handleTraceLogging(clone(rq)); if (rq.method == "BYE" || rq.method == "CANCEL") { let id = rq.headers["call-id"]; stopMedia(id); } if (rq.method == "BYE" && expirationTimers[rq.headers["call-id"]]) { l.verbose("Will clear session expiration timer."); clearTimeout(expirationTimers[rq.headers["call-id"]]); delete expirationTimers[rq.headers["call-id"]]; } if (rq.method == "INVITE" && rq.headers.to.params.tag) { l.verbose("*Got reinvite"); let id1 = rq.headers["call-id"]; if(rq.content) { stopMedia(id1); l.debug("after stopmedia"); } } if (requestCallback) { var resp; try { if (rq.method == "ACK") { if (ackCallback) { ackCallback(rq); } if (rq.content) { let id1 = rq.headers["call-id"]; stopMedia(id1); l.debug("after ack stopmedia"); //playIncomingReqMedia(rq); } } let localPort = sipParams.rtpPort if (rq.content && (rq.method == "INVITE" || rq.method == "ACK") && sipParams.disableMedia != true && !(sipParams.reInvitePcapFile && rq.headers.to.params.tag) && !(sipParams.pcapFile && !rq.headers.to.params.tag) ) { localPort = await playIncomingReqMedia(rq); l.verbose("response will use localPort",localPort) } resp = requestCallback(rq,localPort); if (resp && resp.resendResponse) { resp = resp.response; resend = true; } l.debug("requestCallback resp", resp); if (rq.method == "INVITE" && !rq.headers.to.params.tag) { rq.headers.to.params.tag = rstring(); } } catch (e) { l.error("Error", e); throw e; } if (resp == "sendNoResponse") { l.debug("sendNoResponse action"); return; } if (!resp) { var ipAddress; if (!sipParams.publicAddress) { ipAddress = ip.address(); } else { ipAddress = sipParams.publicAddress; } resp = sip.makeResponse(rq, 200, "OK"); if (!resp.content && rq.method == "INVITE") { resp.content = "v=0\r\n" + "o=- " + rstring() + " " + rstring() + " IN IP4 " + sipParams.rtpAddress + "\r\n" + "s=-\r\n" + "c=IN IP4 " + sipParams.rtpAddress + "\r\n" + "t=0 0\r\n" + "m=audio " + localPort + " RTP/AVP 0\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "a=ptime:20\r\n" + "a=sendrecv\r\n"; }