From c21851c2e099676ad0f413ffb7dcd4b105b91af1 Mon Sep 17 00:00:00 2001
From: Dave Horton <daveh@beachdognet.com>
Date: Thu, 12 Dec 2024 13:05:52 -0500
Subject: [PATCH 1/4] wip

---
 app.js              |  4 ----
 lib/call-session.js | 28 ++++++++++++++++++++--------
 2 files changed, 20 insertions(+), 12 deletions(-)

diff --git a/app.js b/app.js
index 6af4038..c653e63 100644
--- a/app.js
+++ b/app.js
@@ -244,10 +244,6 @@ srf.invite((req, res) => {
     }
     return session.replaces(req, res);
   }
-  if (req.locals.sdp === '') {
-    logger.info('no sdp in invite');
-    return res.send(488, {headers: {'X-Reason': '3pcc INVITEs without SDP are not currently supported'}});
-  }
   const session = new CallSession(logger, req, res);
   session.connect();
 });
diff --git a/lib/call-session.js b/lib/call-session.js
index 6ddd567..974dcc4 100644
--- a/lib/call-session.js
+++ b/lib/call-session.js
@@ -108,7 +108,8 @@ class CallSession extends Emitter {
 
   async connect() {
     const {sdp} = this.req.locals;
-    this.logger.info('inbound call accepted for routing');
+    const is3pcc = this.req.body?.length === 0;
+    this.logger.info(`inbound ${is3pcc ? '3pcc ' : ''}call accepted for routing`);
     const engine = this.getRtpEngine();
     if (!engine) {
       this.logger.info('No available rtpengines, rejecting call!');
@@ -183,13 +184,13 @@ class CallSession extends Emitter {
     else uri = `${scheme}:${host}`;
     this.logger.info(`uri will be: ${uri}, proxy ${proxy}`);
 
-    try {
+    const sendOfferToRtpEngine = async(remoteSdp) => {
       const opts = {
         ...this.rtpEngineOpts.common,
         ...this.rtpEngineOpts.uac.mediaOpts,
         'from-tag': this.rtpEngineOpts.uas.tag,
         direction:  [isPrivateVoipNetwork(this.req.source_address) ? 'private' : 'public', 'private'],
-        sdp
+        sdp: remoteSdp
       };
       const startAt = process.hrtime();
       const response = await this.offer(opts);
@@ -202,7 +203,11 @@ class CallSession extends Emitter {
         this.logger.error({}, `rtpengine offer failed with ${JSON.stringify(response)}`);
         throw new Error('rtpengine failed: answer');
       }
+      return response;
+    };
 
+    try {
+      const response = await sendOfferToRtpEngine(sdp);
       let headers = {
         'From': createBLegFromHeader(this.req),
         'To': this.req.get('To'),
@@ -212,9 +217,13 @@ class CallSession extends Emitter {
       };
       if (this.privateSipAddress) headers = {...headers, Contact: `<sip:${this.privateSipAddress}>`};
 
-      const spdOfferB = this.siprec && this.xml ?
-        createSiprecBody(headers, response.sdp, this.xml.type, this.xml.content) :
-        response.sdp;
+      let spdOfferB;
+      if (this.siprec && this.xml) {
+        spdOfferB = createSiprecBody(headers, response.sdp, this.xml.type, this.xml.content);
+      }
+      else if (!is3pcc) {
+        spdOfferB = response.sdp;
+      }
 
       if (this.req.locals.carrier) {
         Object.assign(headers, {
@@ -281,7 +290,10 @@ class CallSession extends Emitter {
           '-X-Authenticated-User'
         ],
         proxyResponseHeaders: ['all', '-X-Trace-ID'],
-        localSdpB: spdOfferB,
+        localSdpB: spdOfferB ? spdOfferB : async(ackBody) => {
+          const response = await sendOfferToRtpEngine(ackBody);
+          return response.sdp;
+        },
         localSdpA: async(sdp, res) => {
           this.rtpEngineOpts.uac.tag = res.getParsedHeader('To').params.tag;
           const opts = {
@@ -313,7 +325,7 @@ class CallSession extends Emitter {
           }
 
           return response.sdp;
-        }
+        },
       });
 
       // successfully connected

From e40550922009ffc6c7d4559aedef29ee2bfe21b5 Mon Sep 17 00:00:00 2001
From: Dave Horton <daveh@beachdognet.com>
Date: Thu, 12 Dec 2024 13:54:42 -0500
Subject: [PATCH 2/4] wip

---
 lib/call-session.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/call-session.js b/lib/call-session.js
index 974dcc4..474a79c 100644
--- a/lib/call-session.js
+++ b/lib/call-session.js
@@ -304,7 +304,7 @@ class CallSession extends Emitter {
             sdp
           };
           const startAt = process.hrtime();
-          const response = await this.answer(opts);
+          const response = await this.answer({direction: ['private', 'public'], ...opts});
           this.logger.debug({response, opts}, 'response from rtpengine to answer');
           const rtt = roundTripTime(startAt);
           this.stats.histogram('app.rtpengine.response_time', rtt, [

From 0d584a101b66a9d0616f0e113583d3fdbd52104f Mon Sep 17 00:00:00 2001
From: Dave Horton <daveh@beachdognet.com>
Date: Thu, 12 Dec 2024 14:07:01 -0500
Subject: [PATCH 3/4] wip

---
 lib/call-session.js | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/lib/call-session.js b/lib/call-session.js
index 474a79c..8f59818 100644
--- a/lib/call-session.js
+++ b/lib/call-session.js
@@ -304,8 +304,12 @@ class CallSession extends Emitter {
             sdp
           };
           const startAt = process.hrtime();
-          const response = await this.answer({direction: ['private', 'public'], ...opts});
-          this.logger.debug({response, opts}, 'response from rtpengine to answer');
+          const aOpts = {
+            ...opts,
+            ...(is3pcc && {direction: ['private', 'public']})
+          };
+          const response = await this.answer(aOpts);
+          this.logger.debug({response, opts: aOpts}, 'response from rtpengine to answer');
           const rtt = roundTripTime(startAt);
           this.stats.histogram('app.rtpengine.response_time', rtt, [
             'direction:inbound', 'command:answer', `rtpengine:${this.rtpengineIp}`]);

From 094a675cd734c2c99b210d4483c8afa69416e8c6 Mon Sep 17 00:00:00 2001
From: Dave Horton <daveh@beachdognet.com>
Date: Thu, 12 Dec 2024 14:20:24 -0500
Subject: [PATCH 4/4] add test for late media / 3pcc invite, which should now
 work

---
 test/scenarios/uac-late-media.xml | 77 +++++++++++++++++++++++++------
 1 file changed, 64 insertions(+), 13 deletions(-)

diff --git a/test/scenarios/uac-late-media.xml b/test/scenarios/uac-late-media.xml
index 8b96729..f5226cc 100644
--- a/test/scenarios/uac-late-media.xml
+++ b/test/scenarios/uac-late-media.xml
@@ -19,22 +19,21 @@
 <!--                 Sipp 'uac' scenario with pcap (rtp) play           -->
 <!--                                                                    -->
 
-<scenario name="UAC with late media">
+<scenario name="UAC with media">
   <!-- In client mode (sipp placing calls), the Call-ID MUST be         -->
   <!-- generated by sipp. To do so, use [call_id] keyword.                -->
   <send retrans="500">
     <![CDATA[
 
-      INVITE sip:16173333456@[remote_ip]:[remote_port] SIP/2.0
+      INVITE sip:+16173333456@jambonz.org SIP/2.0
       Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
-      From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[call_number]
-      To: sut <sip:[service]@[remote_ip]:[remote_port]>
+      From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
+      To: <sip:16173333456@jambonz.org>
       Call-ID: [call_id]
       CSeq: 1 INVITE
       Contact: sip:sipp@[local_ip]:[local_port]
       Max-Forwards: 70
-      Subject: uac-no-3pcc
-      Content-Type: application/sdp
+      Subject: uac-late-media
       Content-Length: 0
 
     ]]>
@@ -43,27 +42,79 @@
   <recv response="100" optional="true">
   </recv>
 
-  <recv response="488">
+  <recv response="180" optional="true">
+  </recv>
+
+  <!-- By adding rrs="true" (Record Route Sets), the route sets         -->
+  <!-- are saved and used for following messages sent. Useful to test   -->
+  <!-- against stateful SIP proxies/B2BUAs.                             -->
+  <recv response="200" rtd="true" crlf="true">
   </recv>
 
+  <!-- Packet lost can be simulated in any send/recv message by         -->
+  <!-- by adding the 'lost = "10"'. Value can be [1-100] percent.       -->
   <send>
     <![CDATA[
 
-      ACK sip:16173333456@[remote_ip]:[remote_port] SIP/2.0
-      [last_Via]
+      ACK sip:16173333456@jambonz.org SIP/2.0
+      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
       From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
-      To: [service] <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
+      To: <sip:16173333456@jambonz.org>[peer_tag_param]
       Call-ID: [call_id]
       CSeq: 1 ACK
-      Subject: uac-no-3pcc
+      Max-Forwards: 70
+      Subject: uac-late-media
+      Content-Type: application/sdp
+      Content-Length: [len]
+
+      v=0
+      o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
+      s=-
+      c=IN IP[local_ip_type] [local_ip]
+      t=0 0
+      m=audio [auto_media_port] RTP/AVP 8 101
+      a=rtpmap:8 PCMA/8000
+      a=rtpmap:101 telephone-event/8000
+      a=fmtp:101 0-11,16
+
+
+    ]]>
+  </send>
+
+  <!-- Play a pre-recorded PCAP file (RTP stream)                       -->
+  <nop>
+    <action>
+      <exec play_pcap_audio="pcap/g711a.pcap"/>
+    </action>
+  </nop>
+
+  <!-- Pause briefly      -->
+  <pause milliseconds="3000"/>
+
+  <!-- The 'crlf' option inserts a blank line in the statistics report. -->
+  <send retrans="500">
+    <![CDATA[
+
+      BYE sip:16173333456@jambonz.org SIP/2.0
+      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
+      From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
+      To: <sip:16173333456@jambonz.org>[peer_tag_param]
+      Call-ID: [call_id]
+      CSeq: 2 BYE
+      Subject: uac-late-media
       Content-Length: 0
 
     ]]>
   </send>
 
-    <!-- definition of the response time repartition table (unit is ms)   -->
+  <recv response="200" crlf="true">
+  </recv>
+
+  <!-- definition of the response time repartition table (unit is ms)   -->
   <ResponseTimeRepartition value="10, 20, 30, 40, 50, 100, 150, 200"/>
 
   <!-- definition of the call length repartition table (unit is ms)     -->
   <CallLengthRepartition value="10, 50, 100, 500, 1000, 5000, 10000"/>
-</scenario>
\ No newline at end of file
+
+</scenario>
+