Compare commits

...
90 Commits
Author SHA1 Message Date
Dave Horton 7099671e69 0.9.11 2026-09-15 12:39:32 -04:00
Dave Horton 11764e40b0 0.9.10 2026-09-15 12:39:12 -04:00
Hoan Luu HuuandClaude Opus 5 b9e9fe305a Fix/siprec survives fs transfer (#232)
* fix: keep siprec recording alive across a feature server transfer

A cross-feature-server move (enqueue/dequeue or conference) re-negotiates the
feature-server leg in _onFeatureServerTransfer, but nothing rebuilt the rtpengine
subscription the SIPREC recording forks from, so the recorder went silent from the
moment the call moved. The fresh destroy handler installed on the new leg also
dropped the _stopRecording() call the original handlers have, so the SIPREC dialog
was never BYEd and the recorder had to wait out its media timeout.

Rebuild the subscription after the transfer re-negotiates media, and stop the
recording when the transferred leg ends. The resubscribe call is guarded so this
is safe to deploy before @jambonz/siprec-client-utils is bumped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: drop the siprec re-subscribe, keep the missing teardown

The cross-feature-server move was tested on a real two-feature-server cluster
(inbound path): the rtpengine subscription survives the REFER on its own, so
resubscribe() was fixing a fault that does not exist. rtpengine keeps
non-offer-answer subscriptions across an answer.

What does fail, and what this branch still fixes, is the teardown: the destroy
handler installed on the transferred leg never stopped the recording, so the
recorder was left without a BYE. Verified on the inbound path; the outbound
transfer path has the identical handler and the same omission.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 18:42:47 +01:00
Dave Horton fec25d5d15 0.9.9 2026-08-20 09:16:55 -04:00
Dave Horton 5c470b9630 0.9.8 2026-07-20 08:43:34 -04:00
Hoan Luu HuuandClaude Opus 4.8 606792825e feat: honor X-Jambonz-SRTP header for forwarded sip URI calls (#230)
The feature-server sets X-Jambonz-SRTP (from the dial verb's
srtpEncryption option) to request encrypted media on a per-call basis.
Previously SRTP on a forwarded sip URI could only be enabled globally
via JAMBONES_SIPS_FORWARD_SRTP + a sips: scheme; now an application can
opt in per call. The env var remains as a global fallback. The internal
header is stripped before the INVITE is sent to the target.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 07:24:14 -04:00
Dave Horton ffec713d62 bump version 2026-07-09 07:31:05 -04:00
Hoan Luu Huu 6921c1cf31 update rtcp-mux to default for srtp (#229) 2026-07-09 07:10:13 -04:00
Sam Machin 37e4dea705 decrement count on abandoned call (#228)
fix typo so correct error message is logged
2026-07-03 11:11:46 -04:00
Hoan Luu Huu 4fb5acaf40 update realtimedb-helper (#227) 2026-06-15 20:43:07 -04:00
Sam MachinandDave Horton 865068f158 add new CODEC_TRANSCODE option (#225)
* add new CODEC_TRANSCODE option

allows RTP engine to add additional codecs to the outgoing offer that wern't in the invite from Freeswitch so RTP engine will transcode outbound calls.

* update README for new env var descriptions

---------

Co-authored-by: Dave Horton <daveh@beachdognet.com>
2026-05-01 08:43:13 -04:00
Hoan Luu Huu 4f65b4b585 support srtp for sips sipuri outbound call (#224)
* support srtp for sips sipuri outbound call

* wip

* wip

* add env variable for disable the srtp for sipURI
2026-04-22 08:00:50 -04:00
Hoan Luu Huu 8ccd02aa1f update drachtio srf 5.0.21 (#221) 2026-04-13 21:24:42 -04:00
Dave Horton 0911032146 bump version 2026-03-31 07:47:05 -04:00
Dave Horton 121ae75b39 update deps 2026-03-30 21:07:19 -04:00
Dave Horton 57bc073ea3 update to drachtio-srf latest (#218) 2026-03-13 09:02:05 -04:00
Dave Horton 84bce56766 update drachtio-srf 2026-02-09 10:25:47 -05:00
Sam Machin d053b94e71 remove ice and dtls off (#214)
* remove ice and dtls off if set in db

* lint

* more lint
2026-01-29 13:43:30 -05:00
Dave Horton a50ed56b36 update devDependencies (#215) 2026-01-22 11:47:02 -05:00
Hoan Luu Huu 8fdcc8cbc4 update drachtio srf version 5.0.17 (#213) 2026-01-22 08:12:50 -05:00
Sam Machin b882c0de2d Recording update (#212)
* if call hasRecording then add url to cdr

* handle missing hasRecording value in redis

* typo

* lint

* use nullish coalescing for null response

* typo
2026-01-02 10:29:51 -05:00
Dave Horton b3fee43c7f include codec-accept on answer to rtpengine during reinvites (#209)
* include codec-accept on answer to rtpengine during reinvites

* attempt to simplify

* fixes from testing
2025-12-02 07:37:18 -05:00
Dave Horton 5fab8a7515 when far end answers with only pcma, passthrough instead of transcoding to pcmu (#208) 2025-11-24 11:23:16 -06:00
Anton Voylenko 178105acd7 chore: bump node version (#205) 2025-11-04 18:02:31 -05:00
Sam Machin 70a09c10b3 Fix/200 (#202)
* Update call-session.js

* Update call-session.js
2025-10-28 16:58:54 -04:00
Dave Horton 49bc11bbb6 update pino and eslint (#201) 2025-10-21 07:52:29 -04:00
Sam Machin 2ebe4d3d78 bump db_helpers for cache change (#199) 2025-10-15 11:40:12 -04:00
Sam Machin cd0d360561 Redirect outbound user calls to private IP of other SBC (#197)
* redirect client calls to private address of other SBC

* remove unused util

* use address not port
2025-10-04 20:08:57 -04:00
Sam Machin 32d82ed67d Fix/193 (#195)
* pass sip failure reason back to FS

* Update call-session.js

* update drachtio-srf dep
2025-09-12 09:21:13 -04:00
Dave Horton c17f27ab2c fix prev commit (#192) 2025-09-04 07:57:38 -04:00
Dave Horton 2fc570f731 when sending to retell with user starting with call_ dont prepend plus (#191) 2025-09-03 23:35:56 -04:00
Sam Machin c87f831868 Fix/transport in contact (#190)
* use the req transport param in the From and Contact headers if set.

* Update call-session.js
2025-09-03 13:53:51 -04:00
Sam Machin 616228bf09 update isPrivateVoipNetwork function (#189) 2025-09-02 08:02:59 -04:00
Dave Horton 55fef10f0e revert change (for now) that caused audio issues when reinviting to partial media (#187) 2025-08-18 12:45:48 -04:00
Sam Machin 9357920f76 set strict source (#185)
* set strict source

RTPBleed

* change to env var for strict source

* Update srtp-transcoding.json

* lint

* lint

* reverse the logic

* and argghhh

* clarification

* change
2025-08-03 19:44:31 -04:00
Dave Horton 6771cbdefc bump version 2025-07-15 11:46:40 -04:00
Vinod Dharashive e422c2ed9c increase dtmf volume (#184)
https://github.com/jambonz/jambonz-feature-server/issues/1272
2025-07-09 08:22:53 -04:00
rammohan-y fd5e2f1a6c Remove video sdp incase of reinvite (#183)
https://github.com/jambonz/sbc-outbound/issues/182
2025-07-08 09:17:16 -04:00
sathish kumar pasham 5cc37265a9 Fix vulnerabilities by adding @babel/helpers@7.26.10 to dependencies (#181) 2025-06-27 08:04:03 -04:00
Dave Horton 1a9c438321 update version 2025-05-14 15:42:31 -04:00
Dave Horton fb62fd9bf4 update db-helpers 2025-05-13 10:36:08 -04:00
rammohan-y e02e0fa8c1 Updated drachtio version to 5.0.5 (#177)
https://github.com/drachtio/drachtio-server/issues/424
2025-04-22 11:05:45 -04:00
Hoan Luu Huu dd198105e3 update drachtio srf 5.0.4 (#176) 2025-04-18 07:50:21 -04:00
rammohan-y a34491a356 Updated db-helper to 0.9.11 (#175)
https://github.com/jambonz/jambonz-feature-server/issues/1151
2025-04-14 08:22:09 -04:00
Dave Horton 3a467921a6 return 482 loop detected if call to a sip uri would loop back to us (#173)
* return 482 loop detected if call to a sip uri would loop back to us

* this pr fixes #172

* typo

* wip

* wip
2025-03-20 11:25:08 -04:00
rammohan-y 9cb75a0b6b updated realtimedb-helper to 0.8.13 (#169) 2025-03-10 09:53:49 -04:00
Dave Horton 28dae50202 REFER should have sips contact if far end is using sips (#168) 2025-02-24 09:47:47 -05:00
Dave Horton 0b45e766b9 update drachtio-srf 2025-02-20 10:16:59 -05:00
Dave Horton 7380457b5a reject calls on hosted jambonz with no activ (#166)
e subscriptions
2025-02-19 13:04:44 -05:00
Hoan Luu Huu 9feb6f3c8f support voip carrier sip proxy (#165)
* support voiip carrier sip proxy

* wip

* wip

* wip
2025-02-17 09:47:59 -05:00
Hoan Luu Huu c449feeb9c support sip recording from siprec call (#164)
* support sip recording from siprec call

* update srsclient version
2025-02-12 09:24:33 -05:00
Dave Horton b2abe9891e refined the method for syncing call count updates and added debugging (#163)
* refined the method for syncing call count updates and added debugging

* wip

* include callId in debug key
2025-02-05 13:01:04 -05:00
Dave Horton a89b54b19e update deps 2025-01-14 10:50:08 -05:00
Dave Horton ab0f3ed2b5 update dractio-srf and mrf to main branch releases (#162) 2025-01-14 10:06:27 -05:00
Vinod Dharashive d8ae824559 Fresh/466 (#160)
* Hold and unhold does not resume transcript for outbound call to webrtc

* WIP

* jslint

* jslint

* handle null

* Added direction

* re-invite with opus codec need to change pcmu 

on reinvite on unhold opus codec was been sent from rtpegine to freeswitch due to which transcripts were not getting generated , hence it needs to be changed to pcmu

* jslint

jslint
2024-12-23 07:20:04 -05:00
Hoan Luu Huu 334db6f84f support referby display name (#161) 2024-12-11 10:34:27 -05:00
Hoan Luu Huu c762accce8 forward all refer to feature server (#156)
* forward all refer to feature server

* forwawrd extra custom header
2024-12-05 21:16:12 -05:00
Dave Horton c6c63e26da fix duplicate attempt to destroy dialog 2024-11-22 10:46:24 -05:00
Dave Horton 916d577b75 wip (#155) 2024-11-19 09:37:53 -05:00
Dave Horton b976a62a60 when forcing PCMU or PCMA we must also include telephone-event (#153) 2024-11-14 08:37:40 -05:00
Hoan Luu Huu 5e2369e5e8 support opus transcode (#150)
* support opus transcode

* dont resend reinvite when release media for srtp

* wip

* fix review comment
2024-11-13 07:41:43 -05:00
Dave Horton 41528db630 fix call count race condition #151 (#152)
* fix call count race condition #151

* logging fix
2024-11-12 14:57:04 -05:00
Hoan Luu Huu c22104fbe2 allow outbound cdr has sip_parent_callid (#149)
* allow outbound cdr has sip_parent_callid

* update time series version
2024-10-17 08:05:25 -04:00
Hoan Luu Huu 3701e20295 fix sbc crash while outbound calling to user (#148) 2024-10-15 07:28:40 -04:00
Hoan Luu Huu 821275cb67 support X-CID for feature server to detect sip callid (#147)
* support X-CID for feature server to detect sip callid

* fixed review comment

* fix review comment
2024-10-15 07:19:25 -04:00
Dave Horton f5ac51a8f7 minor logging 2024-10-13 10:12:55 -04:00
Hoan Luu Huu 0cf90f37b8 support change log level runtime (#146) 2024-10-07 09:52:22 -04:00
Dave Horton 62fb716236 bump version 2024-09-04 13:42:02 +01:00
Dave Horton b8fdf9f429 when sending out registered trunk use auth.username as userinfo in Contact header of INVITE 2024-09-04 13:40:59 +01:00
Dave Horton 503aa73bc6 Fix/invite use realm to registered trunk (#144)
* special header X-Preferred-From-Host was ignored

* when sending INVITE to registered trunk with a sip realm, use realm in the uri and send via proxy as defined in outbound gateway
2024-08-29 15:26:44 -04:00
Dave Horton a7406ddb8a use system_information.private_network_cidr (#143)
* use system_information.private_network_cidr

* make gh actions work

* fix: export logger
2024-08-18 12:50:19 -04:00
Dave HortonandMarkus Frindt bf1c670b40 Fix/tls transport (#141)
* fix scheme

* add missing initialization of scheme

* delete contact header explicitly

* wip

* wip

* wip

* fix bug with cseq

* wip

* deps

* wip

* wip

---------

Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2024-07-24 15:27:01 -04:00
Markus FrindtandMarkus Frindt b21eaad1ff Support contact header with sips or sip over tls (#139)
Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2024-07-23 08:53:38 -04:00
Dave Horton c671fbdbd0 after challenge register with creds should stay tls if it started that way (#137) 2024-06-24 15:08:43 -04:00
Hoan Luu Huu 73e3779eb1 only send Sips if it's enabled in outbound gateway configuration (#136)
* only send Sips if it's enabled in outbound gateway configuration

* update license
2024-06-15 09:12:02 -04:00
Dave Horton 9ad7dc76c7 fix for #133 (#134) 2024-05-17 07:21:19 -04:00
Hoan Luu Huu 1baf7fa824 Feat/refer support conplex uri for refer-to refered-by header (#131)
* respond to reinvite request incase error

* support complex refer-to refer-by uri

* wip
2024-05-07 08:28:08 -04:00
Hoan Luu Huu d2807c69ed Feat/stats version (#132)
* respond to reinvite request incase error

* update stats colector version
2024-05-06 20:06:23 -04:00
Dave Horton 3e6dc35e86 update deps (#130) 2024-04-07 18:33:39 -04:00
Hoan Luu Huu 16a4709b7d check if sip gateway is in blacklist before sending outbound call (#119)
* check if sip gateway is in blacklist before sending outbound call

* wip

* wip

* wip

* add testcase for blacklist

* wip
2024-03-30 11:10:13 -04:00
Hoan Luu Huu 01adb5cbf0 fix cannot clear siprec records (#127) 2024-03-27 07:50:50 -04:00
Dave Horton 49ba872f51 remove asymmetric flag on offer to B party reinvite as port learning should still happen (#128) 2024-03-26 12:13:01 -04:00
Hoan Luu Huu 591b8f2f8a respond to reinvite request incase error (#126) 2024-03-07 07:49:05 -05:00
Hoan Luu Huu 16ac56e50e start/stop/pause/resume recording success when one of siprec server success. (#125)
* start/stop/pause/resume recording success when one of siprec server success.

* wip

* allow send custom header on pause, resume recording

* allow timeout for siprec action

* wip

* wip

* update siprec-client-utils
2024-03-05 13:06:04 -05:00
Markus FrindtandMarkus Frindt 9412790366 add fix to lint script (#124)
Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2024-02-15 09:44:56 -05:00
Dave Horton 453b27d6e1 avoid sips scheme if best effort TLS is enabled via env (#122) 2024-01-30 20:09:58 -08:00
Dave Horton 4ba3d5b2b0 update db-helpers 2024-01-17 13:37:39 -05:00
Hoan Luu Huu bc3e2334ed fix hangup custom headers (#121) 2023-12-28 20:44:33 -05:00
Hoan Luu Huu d70a01578d INVITE from gw should have register_from_domain if it's available (#120) 2023-12-13 07:57:28 -05:00
Hoan Luu Huu eb0de5b0ee Feat support Outbound DTMF (#115)
* feat/support inband dtmf

* wip

* wip

* wip

* wip

* wip

* wip

* wip
2023-12-11 10:40:24 -05:00
22 changed files with 3006 additions and 5661 deletions
-1
View File
@@ -1 +0,0 @@
test/*
-126
View File
@@ -1,126 +0,0 @@
{
"env": {
"node": true,
"es6": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": false,
"modules": false
},
"ecmaVersion": 2020
},
"plugins": ["promise"],
"rules": {
"promise/always-return": "error",
"promise/no-return-wrap": "error",
"promise/param-names": "error",
"promise/catch-or-return": "error",
"promise/no-native": "off",
"promise/no-nesting": "warn",
"promise/no-promise-in-callback": "warn",
"promise/no-callback-in-promise": "warn",
"promise/no-return-in-finally": "warn",
// Possible Errors
// http://eslint.org/docs/rules/#possible-errors
"comma-dangle": [2, "only-multiline"],
"no-control-regex": 2,
"no-debugger": 2,
"no-dupe-args": 2,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-empty-character-class": 2,
"no-ex-assign": 2,
"no-extra-boolean-cast" : 2,
"no-extra-parens": [2, "functions"],
"no-extra-semi": 2,
"no-func-assign": 2,
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-negated-in-lhs": 2,
"no-obj-calls": 2,
"no-proto": 2,
"no-unexpected-multiline": 2,
"no-unreachable": 2,
"use-isnan": 2,
"valid-typeof": 2,
// Best Practices
// http://eslint.org/docs/rules/#best-practices
"no-fallthrough": 2,
"no-octal": 2,
"no-redeclare": 2,
"no-self-assign": 2,
"no-unused-labels": 2,
// Strict Mode
// http://eslint.org/docs/rules/#strict-mode
"strict": [2, "never"],
// Variables
// http://eslint.org/docs/rules/#variables
"no-delete-var": 2,
"no-undef": 2,
"no-unused-vars": [2, {"args": "none"}],
// Node.js and CommonJS
// http://eslint.org/docs/rules/#nodejs-and-commonjs
"no-mixed-requires": 2,
"no-new-require": 2,
"no-path-concat": 2,
"no-restricted-modules": [2, "sys", "_linklist"],
// Stylistic Issues
// http://eslint.org/docs/rules/#stylistic-issues
"comma-spacing": 2,
"eol-last": 2,
"indent": [2, 2, {"SwitchCase": 1}],
"keyword-spacing": 2,
"max-len": [2, 120, 2],
"new-parens": 2,
"no-mixed-spaces-and-tabs": 2,
"no-multiple-empty-lines": [2, {"max": 2}],
"no-trailing-spaces": [2, {"skipBlankLines": false }],
"quotes": [2, "single", "avoid-escape"],
"semi": 2,
"space-before-blocks": [2, "always"],
"space-before-function-paren": [2, "never"],
"space-in-parens": [2, "never"],
"space-infix-ops": 2,
"space-unary-ops": 2,
// ECMAScript 6
// http://eslint.org/docs/rules/#ecmascript-6
"arrow-parens": [2, "always"],
"arrow-spacing": [2, {"before": true, "after": true}],
"constructor-super": 2,
"no-class-assign": 2,
"no-confusing-arrow": 2,
"no-const-assign": 2,
"no-dupe-class-members": 2,
"no-new-symbol": 2,
"no-this-before-super": 2,
"prefer-const": 2
},
"globals": {
"DTRACE_HTTP_CLIENT_REQUEST" : false,
"LTTNG_HTTP_CLIENT_REQUEST" : false,
"COUNTER_HTTP_CLIENT_REQUEST" : false,
"DTRACE_HTTP_CLIENT_RESPONSE" : false,
"LTTNG_HTTP_CLIENT_RESPONSE" : false,
"COUNTER_HTTP_CLIENT_RESPONSE" : false,
"DTRACE_HTTP_SERVER_REQUEST" : false,
"LTTNG_HTTP_SERVER_REQUEST" : false,
"COUNTER_HTTP_SERVER_REQUEST" : false,
"DTRACE_HTTP_SERVER_RESPONSE" : false,
"LTTNG_HTTP_SERVER_RESPONSE" : false,
"COUNTER_HTTP_SERVER_RESPONSE" : false,
"DTRACE_NET_STREAM_END" : false,
"LTTNG_NET_STREAM_END" : false,
"COUNTER_NET_SERVER_CONNECTION_CLOSE" : false,
"DTRACE_NET_SERVER_CONNECTION" : false,
"LTTNG_NET_SERVER_CONNECTION" : false,
"COUNTER_NET_SERVER_CONNECTION" : false
}
}
+8 -3
View File
@@ -6,12 +6,17 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
node-version: 20.x
- run: npm ci
- run: npm run jslint
- name: Install Docker Compose
run: |
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
- run: npm test
+2
View File
@@ -34,5 +34,7 @@ build/Release
node_modules
.DS_Store
.vscode
examples/*
CLAUDE.md
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run jslint
npm run jslint
+3 -3
View File
@@ -1,10 +1,10 @@
FROM --platform=linux/amd64 node:18.15-alpine3.16 as base
FROM --platform=linux/amd64 node:24-alpine AS base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
WORKDIR /opt/app/
FROM base as build
FROM base AS build
COPY package.json package-lock.json ./
@@ -18,6 +18,6 @@ COPY --from=build /opt/app /opt/app/
ARG NODE_ENV
ENV NODE_ENV $NODE_ENV
ENV NODE_ENV=$NODE_ENV
CMD [ "node", "app.js" ]
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 Drachtio Communications Services, LLC
Copyright (c) 2018-2024 FirstFive8, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+10
View File
@@ -29,9 +29,19 @@ Configuration is provided via environment variables:
|JAMBONES_RTPENGINES| commans-separated list of ip:ng-port for rtpengines (e.g. '172.31.32.10:22222')|yes|
|JAMBONES_TIME_SERIES_HOST| influxdb host |yes|
|JAMBONES_RECORD_ALL_CALLS| enable auto record calls |no|
|JAMBONES_CODEC_OFFER_WITH_ORDER| comma-separated codec list to use as the outbound offer toward the carrier; the original codecs from the feature server are stripped first (e.g. `'opus,PCMU,PCMA,telephone-event'`) |no|
|JAMBONES_CODEC_TRANSCODE| comma-separated codec list that rtpengine should add to the outbound offer and transcode on the fly when the carrier selects them — used to support codecs the feature server does not speak (e.g. `'AMR-WB/16000'`). Requires an rtpengine build that includes the corresponding codec module. |no|
|JAMBONES_ACCEPT_AND_TRANSCODE| comma-separated codec list to accept on the outbound leg and transcode to PCMU/PCMA toward the feature server |no|
|JAMBONES_ACCEPT_G729| if set, accept G.729 on the outbound leg and transcode to PCMU/PCMA (shorthand for `JAMBONES_ACCEPT_AND_TRANSCODE=g729`) |no|
|K8S| service running as kubernetes service |no|
|K8S_RTPENGINE_SERVICE_NAME| rtpengine service name(required for K8S) |no|
#### Codec transcoding notes
`JAMBONES_CODEC_OFFER_WITH_ORDER` and `JAMBONES_CODEC_TRANSCODE` can be combined. When both are set, rtpengine first strips the original codecs from the SDP, then adds the codecs listed in `JAMBONES_CODEC_OFFER_WITH_ORDER` to the offer, and finally adds the codecs from `JAMBONES_CODEC_TRANSCODE` with transcoding enabled. When only `JAMBONES_CODEC_TRANSCODE` is set, the original codecs from the feature server are kept in the offer and the transcode codecs are appended.
Codecs that require licensed or optional rtpengine modules (AMR, AMR-WB, G.729, etc.) only work if rtpengine was compiled with support for them. Carrier-specific fmtp parameters (for example, `octet-align=1` for AMR-WB) can be appended to the codec name as needed (e.g. `AMR-WB/16000;octet-align=1`).
### running under pm2
Typically, this application runs under [pm2](https://pm2.io) using an [ecosystem.config.js](https://pm2.keymetrics.io/docs/usage/application-declaration/) file similar to this:
```js
+27 -5
View File
@@ -50,7 +50,8 @@ const {
lookupSipGatewaysByCarrier,
lookupCarrierBySid,
queryCallLimits,
lookupCarrierByAccountLcr
lookupCarrierByAccountLcr,
lookupSystemInformation
} = require('@jambonz/db-helpers')({
host: process.env.JAMBONES_MYSQL_HOST,
port: process.env.JAMBONES_MYSQL_PORT || 3306,
@@ -67,6 +68,9 @@ const {
decrKey,
retrieveSet,
isMemberOfSet,
addKey,
deleteKey,
retrieveKey
} = require('@jambonz/realtimedb-helpers')({}, logger);
const activeCallIds = new Map();
@@ -84,6 +88,7 @@ srf.locals = {...srf.locals,
queryCdrs,
activeCallIds,
idleEmitter,
privateNetworkCidr: process.env.PRIVATE_VOIP_NETWORK_CIDR || null,
dbHelpers: {
ping,
lookupOutboundCarrierForAccount,
@@ -94,9 +99,14 @@ srf.locals = {...srf.locals,
lookupSipGatewaysByCarrier,
lookupCarrierBySid,
queryCallLimits,
lookupCarrierByAccountLcr
lookupCarrierByAccountLcr,
lookupSystemInformation
},
realtimeDbHelpers: {
client: redisClient,
addKey,
deleteKey,
retrieveKey,
createHash,
retrieveHash,
incrKey,
@@ -183,10 +193,22 @@ if (process.env.K8S || process.env.HTTP_PORT) {
});
}
if ('test' !== process.env.NODE_ENV) {
/* update call stats periodically */
setInterval(() => {
/* update call stats periodically as well as definition of private network cidr */
setInterval(async() => {
stats.gauge('sbc.sip.calls.count', activeCallIds.size, ['direction:outbound',
`instance_id:${process.env.INSTANCE_ID || 0}`]);
const r = await lookupSystemInformation();
if (r) {
if (r.private_network_cidr !== srf.locals.privateNetworkCidr) {
logger.info(`updating private network cidr from ${srf.locals.privateNetworkCidr} to ${r.private_network_cidr}`);
srf.locals.privateNetworkCidr = r.private_network_cidr;
}
// Update system log level
if (r.log_level) {
logger.level = r.log_level;
}
}
}, 20000);
}
@@ -261,4 +283,4 @@ function handle(signal) {
}
}
module.exports = {srf};
module.exports = {srf, logger};
+1 -1
View File
@@ -3,6 +3,6 @@
"DTLS": "off",
"SDES": "off",
"ICE": "remove",
"flags": ["media handover", "port latching"],
"flags": ["port latching"],
"rtcp-mux": ["demux"]
}
+3 -3
View File
@@ -3,13 +3,13 @@
"transport-protocol": "UDP/TLS/RTP/SAVPF",
"ICE": "force",
"SDES": "off",
"flags": ["generate mid", "SDES-no", "media handover", "port latching"],
"rtcp-mux": ["require"]
"flags": ["generate mid", "SDES-no", "port latching"],
"rtcp-mux": ["offer"]
},
"teams": {
"transport-protocol": "RTP/SAVP",
"ICE": "force",
"flags": ["generate mid", "media handover",
"flags": ["generate mid",
"SDES-no-AES_CM_128_HMAC_SHA1_32",
"SDES-no-F8_128_HMAC_SHA1_80",
"SDES-no-F8_128_HMAC_SHA1_32",
+137
View File
@@ -0,0 +1,137 @@
const promisePlugin = require('eslint-plugin-promise');
module.exports = [
{
ignores: ['test/*']
},
{
files: ['**/*.js'],
languageOptions: {
ecmaVersion: 2020,
sourceType: 'commonjs',
globals: {
// Node.js globals
console: 'readonly',
process: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
module: 'readonly',
require: 'readonly',
exports: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly',
setImmediate: 'readonly',
clearImmediate: 'readonly',
// DTrace/LTTNG globals
DTRACE_HTTP_CLIENT_REQUEST: false,
LTTNG_HTTP_CLIENT_REQUEST: false,
COUNTER_HTTP_CLIENT_REQUEST: false,
DTRACE_HTTP_CLIENT_RESPONSE: false,
LTTNG_HTTP_CLIENT_RESPONSE: false,
COUNTER_HTTP_CLIENT_RESPONSE: false,
DTRACE_HTTP_SERVER_REQUEST: false,
LTTNG_HTTP_SERVER_REQUEST: false,
COUNTER_HTTP_SERVER_REQUEST: false,
DTRACE_HTTP_SERVER_RESPONSE: false,
LTTNG_HTTP_SERVER_RESPONSE: false,
COUNTER_HTTP_SERVER_RESPONSE: false,
DTRACE_NET_STREAM_END: false,
LTTNG_NET_STREAM_END: false,
COUNTER_NET_SERVER_CONNECTION_CLOSE: false,
DTRACE_NET_SERVER_CONNECTION: false,
LTTNG_NET_SERVER_CONNECTION: false,
COUNTER_NET_SERVER_CONNECTION: false
}
},
plugins: {
promise: promisePlugin
},
rules: {
// Promise plugin rules
'promise/always-return': 'error',
'promise/no-return-wrap': 'error',
'promise/param-names': 'error',
'promise/catch-or-return': 'error',
'promise/no-native': 'off',
'promise/no-nesting': 'warn',
'promise/no-promise-in-callback': 'warn',
'promise/no-callback-in-promise': 'warn',
'promise/no-return-in-finally': 'warn',
// Possible Errors
'comma-dangle': [2, 'only-multiline'],
'no-control-regex': 2,
'no-debugger': 2,
'no-dupe-args': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-ex-assign': 2,
'no-extra-boolean-cast': 2,
'no-extra-parens': [2, 'functions'],
'no-extra-semi': 2,
'no-func-assign': 2,
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-obj-calls': 2,
'no-proto': 2,
'no-unexpected-multiline': 2,
'no-unreachable': 2,
'use-isnan': 2,
'valid-typeof': 2,
// Best Practices
'no-fallthrough': 2,
'no-octal': 2,
'no-redeclare': 2,
'no-self-assign': 2,
'no-unused-labels': 2,
// Strict Mode
'strict': [2, 'never'],
// Variables
'no-delete-var': 2,
'no-undef': 2,
'no-unused-vars': [2, {args: 'none'}],
// Node.js and CommonJS
'no-mixed-requires': 2,
'no-new-require': 2,
'no-path-concat': 2,
// Stylistic Issues
'comma-spacing': 2,
'eol-last': 2,
'indent': [2, 2, {SwitchCase: 1}],
'keyword-spacing': 2,
'max-len': [2, 120, 2],
'new-parens': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multiple-empty-lines': [2, {max: 2}],
'no-trailing-spaces': [2, {skipBlankLines: false}],
'quotes': [2, 'single', 'avoid-escape'],
'semi': 2,
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': 2,
// ECMAScript 6
'arrow-parens': [2, 'always'],
'arrow-spacing': [2, {before: true, after: true}],
'constructor-super': 2,
'no-class-assign': 2,
'no-confusing-arrow': 2,
'no-const-assign': 2,
'no-dupe-class-members': 2,
'no-new-symbol': 2,
'no-this-before-super': 2,
'prefer-const': 2
}
}
];
+447 -129
View File
@@ -1,33 +1,62 @@
const Emitter = require('events');
const sdpTransform = require('sdp-transform');
const SrsClient = require('@jambonz/siprec-client-utils');
const {makeRtpEngineOpts, nudgeCallCounts, isPrivateVoipNetwork} = require('./utils');
const {
makeRtpEngineOpts,
nudgeCallCounts,
isPrivateVoipNetwork,
isBlackListedSipGateway,
makeFullMediaReleaseKey,
makePartnerFullMediaReleaseKey,
isValidDomainOrIP,
removeVideoSdp,
} = require('./utils');
const { MediaPath } = require('./constants.json');
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
const {SipError, stringifyUri, parseUri} = require('drachtio-srf');
const debug = require('debug')('jambonz:sbc-outbound');
const makeInviteInProgressKey = (callid) => `sbc-out-iip${callid}`;
/**
* this is to make sure the outgoing From has the number in the incoming From
* and not the incoming PAI
*/
const createBLegFromHeader = (req, teams) => {
const IMMUTABLE_HEADERS = ['via', 'from', 'to', 'call-id', 'cseq', 'max-forwards', 'content-length'];
const createBLegFromHeader = ({
logger,
req,
host,
fromUser,
register_from_domain,
transport,
teams = false,
scheme = 'sip'
}) => {
const from = req.getParsedHeader('From');
const uri = parseUri(from.uri);
let user = uri.user || 'anonymous';
let host = 'localhost';
if (teams) {
host = req.get('X-MS-Teams-Tenant-FQDN');
}
else if (req.has('X-Preferred-From-User') || req.has('X-Preferred-From-Host')) {
user = req.get('X-Preferred-From-User') || user;
host = req.get('X-Preferred-From-Host') || host;
const transportParam = transport ? `;transport=${transport}` : '';
logger.debug({from, fromUser, uri, host, scheme, transport, teams}, 'createBLegFromHeader');
/* user */
const user = fromUser || req.get('X-Preferred-From-User') || uri.user || 'anonymous';
/* host */
if (!host) {
if (teams) {
host = req.get('X-MS-Teams-Tenant-FQDN');
}
else if (req.has('X-Preferred-From-Host')) {
host = req.get('X-Preferred-From-Host');
} else if (register_from_domain) {
host = register_from_domain;
}
else {
host = 'localhost';
}
}
if (from.name) {
return `${from.name} <sip:${user}@${host}>`;
return `${from.name} <${scheme}:${user}@${host}${transportParam}>`;
}
return `sip:${user}@${host}`;
return `<${scheme}:${user}@${host}${transportParam}>`;
};
const createBLegToHeader = (req, teams) => {
const to = req.getParsedHeader('To');
const host = teams ? req.get('X-MS-Teams-Tenant-FQDN') : 'localhost';
@@ -51,6 +80,7 @@ const initCdr = (req, invite) => {
account_sid: req.get('X-Account-Sid'),
call_sid: req.get('X-Call-Sid'),
sip_callid: invite.get('Call-ID'),
...(req.has('X-CID') && {sip_parent_callid: req.get('X-CID')}),
...(applicationSid && {application_sid: applicationSid}),
from,
to,
@@ -64,15 +94,6 @@ const initCdr = (req, invite) => {
};
};
const updateRtpEngineFlags = (sdp, opts) => {
try {
const parsed = sdpTransform.parse(sdp);
const codec = parsed.media[0].rtp[0].codec;
if (['PCMU', 'PCMA'].includes(codec)) opts.flags.push(`codec-accept-${codec}`);
} catch (err) {}
return opts;
};
class CallSession extends Emitter {
constructor(logger, req, res) {
super();
@@ -87,6 +108,9 @@ class CallSession extends Emitter {
this.writeCdrs = this.srf.locals.writeCdrs;
this.decrKey = req.srf.locals.realtimeDbHelpers.decrKey;
this.addKey = req.srf.locals.realtimeDbHelpers.addKey;
this.retrieveKey = req.srf.locals.realtimeDbHelpers.retrieveKey;
this.retrieveHash = req.srf.locals.realtimeDbHelpers.retrieveHash;
const {
lookupOutboundCarrierForAccount,
@@ -99,7 +123,9 @@ class CallSession extends Emitter {
this.lookupSipGatewaysByCarrier = lookupSipGatewaysByCarrier;
this.lookupCarrierByAccountLcr = lookupCarrierByAccountLcr;
this._mediaReleased = false;
this._mediaPath = MediaPath.FullMedia;
this.recordingNoAnswerTimeout = (process.env.JAMBONES_RECORDING_NO_ANSWER_TIMEOUT || 2) * 1000;
}
get service_provider_sid() {
@@ -119,12 +145,7 @@ class CallSession extends Emitter {
}
get isMediaReleased() {
return this._mediaReleased;
}
get calleeIsUsingSrtp() {
const tp = this.rtpEngineOpts?.uac?.mediaOpts['transport-protocol'];
return tp && -1 !== tp.indexOf('SAVP');
return this._mediaPath !== MediaPath.FullMedia;
}
subscribeForDTMF(dlg) {
@@ -164,7 +185,7 @@ class CallSession extends Emitter {
subscribeAnswer,
unsubscribe
} = engine;
const {createHash, retrieveHash} = this.srf.locals.realtimeDbHelpers;
const {client, createHash, retrieveHash} = this.srf.locals.realtimeDbHelpers;
this.offer = offer;
this.answer = answer;
this.del = del;
@@ -182,15 +203,14 @@ class CallSession extends Emitter {
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, this.useWss || teams, false, teams);
this.rtpEngineResource = {destroy: this.del.bind(null, this.rtpEngineOpts.common)};
let proxy, uris;
let proxy, uris = [];
const mapGateways = new Map();
let encryptedMedia = false;
try {
// determine where to send the call
debug(`connecting call: ${JSON.stringify(this.req.locals)}`);
let headers = {
'From': createBLegFromHeader(this.req, teams),
'Contact': createBLegFromHeader(this.req, teams),
const headers = {
'To': createBLegToHeader(this.req, teams),
Allow: 'INVITE, ACK, OPTIONS, CANCEL, BYE, NOTIFY, UPDATE, PRACK',
'X-Account-Sid': this.account_sid
@@ -214,8 +234,27 @@ class CallSession extends Emitter {
if (!contact.includes('transport=ws')) {
proxy = this.req.locals.registration.proxy;
}
this.logger.info(`sending call to registered user ${destUri}`);
}
else if (this.req.locals.target === 'forward') {
/* the feature-server sets X-Jambonz-SRTP (from the dial verb's srtpEncryption
option) to request encrypted media on a per-call basis */
const srtpMode = this.req.get('X-Jambonz-SRTP');
if (srtpMode) {
/* SDES (RTP/SAVP): pass teams=true to select the SDES srtp profile, matching
the proven carrier tls/srtp path above. This offers a=crypto to the target
(what SIP endpoints such as LiveKit expect), not DTLS-SRTP. */
this.logger.info({uri: this.req.uri, srtpMode},
'using SRTP (SDES) for forwarded call per X-Jambonz-SRTP');
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, true, false, true);
encryptedMedia = true;
}
else if (process.env.JAMBONES_SIPS_FORWARD_SRTP && this.req.uri.startsWith('sips:')) {
/* legacy global opt-in for sips: forwards (uses the DTLS srtp profile) */
this.logger.info({uri: this.req.uri}, 'using SRTP/TLS for forwarded sips: call');
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, true, false, false);
encryptedMedia = true;
}
uris = [{
private_network: await isPrivateVoipNetwork(this.req.uri),
uri: this.req.uri
@@ -238,10 +277,6 @@ class CallSession extends Emitter {
private_network: false,
uri: `sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com`
}];
headers = {
...headers,
Contact: `sip:${this.req.calledNumber}@${this.req.get('X-MS-Teams-Tenant-FQDN')}:5061;transport=tls`
};
}
else {
try {
@@ -276,7 +311,13 @@ class CallSession extends Emitter {
}
const vc = await this.lookupCarrierBySid(voip_carrier_sid);
const gateways = await this.lookupSipGatewaysByCarrier(voip_carrier_sid);
const gws = (gateways || [])
const goodGateways = [];
for (const g of gateways) {
if (!await isBlackListedSipGateway(client, this.logger, g.sip_gateway_sid)) {
goodGateways.push(g);
}
}
const gws = (goodGateways || [])
.filter((gw) => gw.outbound);
if (gws.length) {
uris = [];
@@ -285,17 +326,19 @@ class CallSession extends Emitter {
this.req.calledNumber.slice(1) :
this.req.calledNumber;
const prefix = vc.tech_prefix || '';
const protocol = o.protocol?.startsWith('tls') ? 'tls' : (o.protocol || 'udp');
const transport = o.protocol?.startsWith('tls') ? 'tls' : (o.protocol || 'udp');
const hostport = !o.port || 5060 === o.port ? o.ipv4 : `${o.ipv4}:${o.port}`;
const prependPlus = vc.e164_leading_plus && !this.req.calledNumber.startsWith('0') ? '+' : '';
const transport = `transport=${protocol}`;
const scheme = protocol === 'tls' ? 'sips' : 'sip';
const u = `${scheme}:${prefix}${prependPlus}${calledNumber}@${hostport};${transport}`;
const prependPlus = vc.e164_leading_plus && /^[1-9]/.test(calledNumber) ? '+' : '';
const scheme = transport === 'tls' && !process.env.JAMBONES_USE_BEST_EFFORT_TLS && o.use_sips_scheme ?
'sips' : 'sip';
let u = `${scheme}:${prefix}${prependPlus}${calledNumber}@${hostport};transport=${transport}`;
const obj = {
name: vc.name,
diversion: vc.diversion,
hostport,
protocol: o.protocol
transport,
scheme,
register_from_domain: vc.register_from_domain
};
if (vc.register_username && vc.register_password) {
obj.auth = {
@@ -303,8 +346,22 @@ class CallSession extends Emitter {
password: vc.register_password
};
}
if (vc.requires_register && vc.register_sip_realm?.length > 0) {
proxy = u;
u = `${scheme}:${prefix}${prependPlus}${calledNumber}@${vc.register_sip_realm};transport=${transport}`;
this.logger.debug({uri: u}, `using outbound proxy for this registered trunk: ${proxy}`);
} else if (vc.outbound_sip_proxy && vc.outbound_sip_proxy.length > 0) {
if (vc.outbound_sip_proxy.includes('sip:') || vc.outbound_sip_proxy.includes('sips:')) {
proxy = vc.outbound_sip_proxy;
} else if (isValidDomainOrIP(vc.outbound_sip_proxy)) {
proxy =
`${scheme}:${prefix}${prependPlus}${calledNumber}@${vc.outbound_sip_proxy};transport=${transport}`;
this.logger.debug({uri: u}, `using outbound proxy for this trunk: ${proxy}`);
}
}
mapGateways.set(u, obj);
uris.push(u);
this.logger.debug({gateway: o}, `pushed uri ${u}`);
if (o.protocol === 'tls/srtp') {
/** TODO: this is a bit of a hack in the sense that we are not
* supporting a scenario where you have a carrier with several outbound
@@ -313,10 +370,12 @@ class CallSession extends Emitter {
* like a rare use case -- encryption is usually an all or nothing requirement.
*/
this.logger.info({u}, `using SRTP for outbound call, pad crypto: ${o.pad_crypto ? 'yes' : 'no'}`);
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, true, o.pad_crypto, true);
// eslint-disable-next-line max-len
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, true, o.pad_crypto, true, o.remove_ice, o.dtls_off);
encryptedMedia = true;
}
});
// Check private network for each gw
/* Check private network for each gw */
uris = await Promise.all(uris.map(async(u) => {
return {
private_network: await isPrivateVoipNetwork(u),
@@ -327,6 +386,7 @@ class CallSession extends Emitter {
}
else {
this.logger.info({voip_carrier_sid}, 'no outbound gateways found for requested carrier');
this.res.send(603);
}
} catch (err) {
debug(err);
@@ -337,23 +397,20 @@ class CallSession extends Emitter {
debug(`sending call to PSTN ${uris}`);
}
// private_network should be called at last
/* private_network should be called at last - try public first */
uris = uris.sort((a, b) => a.private_network - b.private_network);
const toPrivate = uris.some((u) => u.private_network === true);
const toPublic = uris.some((u) => u.private_network === false);
let isOfferUpdatedToPrivate = toPrivate && !toPublic;
// rtpengine 'offer'
const opts = updateRtpEngineFlags(this.req.body, {
const opts = {
...this.rtpEngineOpts.common,
...this.rtpEngineOpts.uac.mediaOpts,
'from-tag': this.rtpEngineOpts.uas.tag,
direction: ['private', toPublic ? 'public' : 'private'],
sdp: this.req.body
});
};
let response = await this.offer(opts);
debug(`response from rtpengine to offer ${JSON.stringify(response)}`);
this.logger.debug({offer: opts, response}, 'initial offer to rtpengine');
if ('ok' !== response.result) {
this.logger.error(`rtpengine offer failed with ${JSON.stringify(response)}`);
@@ -366,10 +423,12 @@ class CallSession extends Emitter {
// crank through the list of gateways until connected, exhausted or caller hangs up
let earlyMedia = false;
while (uris.length) {
let hdrs = { ...headers};
let hdrs = { ...headers };
const {private_network, uri} = uris.shift();
/* if we've exhausted attempts to public endpoints and are switching to trying private, we need new rtp */
if (private_network && !isOfferUpdatedToPrivate) {
// Cannot make call to all public Uris, now come to talk with private network Uris
this.logger.info('switching to attempt to deliver call via private network now..');
this.rtpEngineResource.destroy()
.catch((err) => this.logger.info({err}, 'Error destroying rtpe to re-connect to private network'));
response = await this.offer({
@@ -378,8 +437,8 @@ class CallSession extends Emitter {
});
isOfferUpdatedToPrivate = true;
}
const gw = mapGateways.get(uri);
const passFailure = 0 === uris.length; // only a single target
/* on the second and subsequent attempts, use the same Call-ID and CSeq from the first attempt */
if (0 === uris.length) {
try {
const key = makeInviteInProgressKey(this.req.get('Call-ID'));
@@ -395,26 +454,79 @@ class CallSession extends Emitter {
this.logger.info({err}, 'Error retrieving iip key');
}
}
// INVITE request line and To header should be the same.
hdrs = {...hdrs, 'To': uri};
if (gw) {
this.logger.info({gw}, `sending INVITE to ${uri} via carrier ${gw.name}`);
if (gw.diversion) {
let div = gw.diversion;
if (div.startsWith('+')) {
div = `<sip:${div}@${gw.hostport}>;reason=unknown;counter=1;privacy=off`;
}
else div = `<sip:+${div}@${gw.hostport}>;reason=unknown;counter=1;privacy=off`;
hdrs = {
...hdrs,
'Diversion': div
};
}
}
else this.logger.info(`sending INVITE to ${uri} via proxy ${proxy})`);
try {
const responseHeaders = this.privateSipAddress ? {Contact: `<sip:${this.privateSipAddress}>`} : {};
/* INVITE request line and To header should be the same. */
hdrs = {...hdrs, 'To': uri};
/* only now can we set Contact & From header since they depend on transport and scheme of gw */
const gw = mapGateways.get(uri);
if (gw) {
const {scheme, transport} = gw;
this.logger.info({gw}, `sending INVITE to ${uri} via carrier ${gw.name}`);
hdrs = {
...hdrs,
From: gw.register_from_domain ?
createBLegFromHeader({
logger: this.logger,
req: this.req,
register_from_domain: gw.register_from_domain,
scheme,
transport,
...(private_network && {host: this.privateSipAddress})
}) :
createBLegFromHeader({
logger: this.logger,
req: this.req,
scheme,
transport,
...(private_network && {host: this.privateSipAddress})
}),
Contact: createBLegFromHeader({
logger: this.logger,
req: this.req,
...(gw.auth?.username && {fromUser: gw.auth?.username}),
scheme,
transport,
...(private_network && {host: this.privateSipAddress})
}),
...(gw.diversion && {
Diversion: gw.diversion.startsWith('+') ?
`<sip:${gw.diversion}@${gw.hostport}>;reason=unknown;counter=1;privacy=off` :
`<sip:+${gw.diversion}@${gw.hostport}>;reason=unknown;counter=1;privacy=off`
})
};
}
else if (teams) {
hdrs = {
...hdrs,
'From': createBLegFromHeader({logger: this.logger, req: this.req, teams: true, transport: 'tls'}),
'Contact': `sip:${this.req.calledNumber}@${this.req.get('X-MS-Teams-Tenant-FQDN')}:5061;transport=tls`
};
}
else {
const uri = parseUri(this.req.uri);
hdrs = {
...hdrs,
'From': createBLegFromHeader({
logger: this.logger,
req: this.req,
transport: uri.params.transport,
...(private_network && {host: this.privateSipAddress})
}),
'Contact': createBLegFromHeader({
logger: this.logger,
req: this.req,
transport: uri.params.transport,
...(private_network && {host: this.privateSipAddress})
})
};
const p = proxy ? ` via ${proxy}` : '';
this.logger.info({uri, p}, `sending INVITE${p}`);
}
/* now launch an outbound call attempt */
const passFailure = 0 === uris.length; // only propagate failure on last attempt
try {
const {uas, uac} = await this.srf.createB2BUA(this.req, this.res, uri, {
proxy,
passFailure,
@@ -432,6 +544,7 @@ class CallSession extends Emitter {
'-X-Preferred-From-Host',
'-X-Jambonz-FS-UUID',
'-X-Voip-Carrier-Sid',
'-X-Jambonz-SRTP',
'-X-SIP-Proxy'
],
proxyResponseHeaders: [
@@ -439,18 +552,27 @@ class CallSession extends Emitter {
'-Allow',
'-Session-Expires'
],
// Add X-CID header to feature server response.
// to allow calling/status hooks contains sbc_callid.
responseHeaders: (uacRes, headers) => {
headers['X-CID'] = uacRes.get('Call-ID');
},
headers: hdrs,
responseHeaders,
auth: gw ? gw.auth : undefined,
localSdpB: response.sdp,
localSdpA: async(sdp, res) => {
this.rtpEngineOpts.uac.tag = res.getParsedHeader('To').params.tag;
const opts = {
...this.rtpEngineOpts.common,
...this.rtpEngineOpts.uas.mediaOpts,
'from-tag': this.rtpEngineOpts.uas.tag,
'to-tag': this.rtpEngineOpts.uac.tag,
flags: ['single codec'],
flags: [
'single codec',
'inject DTMF',
'reuse codecs',
],
sdp
};
const response = await this.answer(opts);
@@ -493,6 +615,9 @@ class CallSession extends Emitter {
} catch (err) {
this.logger.error({err}, 'Error saving Call-ID/CSeq');
}
this.contactHeader = inv.get('Contact');
this.logger.info(`outbound call attempt to ${uri} has contact header ${this.contactHeader}`);
},
cbProvisional: (response) => {
if (!earlyMedia && [180, 183].includes(response.status) && response.body) earlyMedia = true;
@@ -512,14 +637,25 @@ class CallSession extends Emitter {
// these are all final failure scenarios
if (uris.length === 0 || // exhausted all targets
earlyMedia || // failure after early media
encryptedMedia || // cant crank back when using encrypted media as keys have been exchanged
!(err instanceof SipError) || // unexpected error
err.status === 487) { // caller hung up
const abandoned = err.message && err.message.includes('rtpengine failed: Unknown call-id');
const status = err.status || (abandoned ? 487 : 500);
if (err instanceof SipError) this.logger.info(`final call failure ${status}`);
else if (!abandoned) this.logger.error(err, 'unexpected call failure');
debug(`got final outdial error: ${err}`);
else if (!abandoned) {
const {writeCallCount, writeCallCountSP, writeCallCountApp} = this.req.srf.locals;
this.logger.error({err}, 'unexpected call failure');
nudgeCallCounts(this.req, 'failure', {
service_provider_sid: this.service_provider_sid,
account_sid: this.account_sid,
application_sid: this.application_sid,
callId: this.req.locals.callId
}, this.decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
.catch((err) => this.logger.error(err, 'Error decrementing call counts'));
}
this.logger.debug(`got final outdial error: ${err}`);
if (!passFailure) this.res.send(status);
this.emit('failed');
this.rtpEngineResource.destroy()
@@ -543,7 +679,21 @@ class CallSession extends Emitter {
}
}
} catch (err) {
if ('abandonded' !== err.message) this.logger.error(err, `Error setting up outbound call to: ${uris}`);
if ('abandoned' !== err.message) this.logger.error({err}, `Error setting up outbound call to: ${uris}`);
/* the call count was incremented on 'init' but we bailed out before connecting (e.g. caller
hung up before the B leg answered), so we must decrement it here - no failure response is
sent on this path, so the res.once('end') safety net in middleware would not fire.
nudgeCallCounts is a no-op if the count was already decremented elsewhere. */
const {writeCallCount, writeCallCountSP, writeCallCountApp} = this.req.srf.locals;
nudgeCallCounts(this.req, 'failure', {
service_provider_sid: this.service_provider_sid,
account_sid: this.account_sid,
application_sid: this.application_sid,
callId: this.req.locals.callId
}, this.decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
.catch((err) => this.logger.error(err, 'Error decrementing call counts'));
this.emit('failed');
this.srf.endSession(this.req);
this.rtpEngineResource.destroy();
@@ -562,18 +712,31 @@ class CallSession extends Emitter {
answered_at: callStart
};
}
/* save far end SDP for later use if we do a full media release */
if (process.env.JAMBONES_ENABLE_FULL_MEDIA_RELEASE) {
const key = makeFullMediaReleaseKey(this.req.get('X-CID'));
const sdp = uac.remote.sdp;
this.logger.info({key, sdp}, 'saving far end sdp for full media release feature');
this.addKey(key, sdp, 3600).catch((err) => this.logger.error(err, 'Error saving far end sdp'));
}
this.uas = uas;
this.uac = uac;
[uas, uac].forEach((dlg) => {
dlg.on('destroy', async() => {
dlg.on('destroy', async(bye) => {
const other = dlg.other;
this.rtpEngineResource.destroy();
this.activeCallIds.delete(this.req.get('Call-ID'));
this.unsubscribeForDTMF();
//this.unsubscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uac.tag);
try {
await other.destroy();
} catch (err) {}
const headers = {};
Object.keys(bye.headers).forEach((h) => {
if (!IMMUTABLE_HEADERS.includes(h)) headers[h] = bye.headers[h];
});
await other.destroy({headers});
} catch {}
const trackingOn = process.env.JAMBONES_TRACK_ACCOUNT_CALLS ||
process.env.JAMBONES_TRACK_SP_CALLS ||
@@ -581,10 +744,12 @@ class CallSession extends Emitter {
if (process.env.JAMBONES_HOSTING || trackingOn) {
const {writeCallCount, writeCallCountSP, writeCallCountApp} = this.req.srf.locals;
await nudgeCallCounts(this.logger, {
this.logger.debug('decrementing call count at end of call');
await nudgeCallCounts(this.req, 'complete', {
service_provider_sid: this.service_provider_sid,
account_sid: this.account_sid,
application_sid: this.application_sid
application_sid: this.application_sid,
callId: this.req.locals.callId
}, this.decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
.catch((err) => this.logger.error(err, 'Error decrementing call counts'));
}
@@ -593,10 +758,13 @@ class CallSession extends Emitter {
if (this.req.locals.cdr) {
const now = Date.now();
const day = new Date();
const recordAllCalls = this.req.locals.record_all_calls;
// eslint-disable-next-line max-len
const {hasRecording = false} = await this.retrieveHash(`call:${this.account_sid}:${this.req.locals.cdr.call_sid}`) ?? {};
const recordAllCalls = this.req.locals.record_all_calls || hasRecording;
const record_format = this.req.locals.account.record_format || 'mp3';
let recording_url = `/Accounts/${this.account_sid}/RecentCalls/${this.req.locals.cdr.call_sid}/record`;
recording_url += `/${day.getFullYear()}/${(day.getMonth() + 1).toString().padStart(2, '0')}`;
recording_url += `/${day.getDate().toString().padStart(2, '0')}/${recordAllCalls}`;
recording_url += `/${day.getDate().toString().padStart(2, '0')}/${record_format}`;
this.writeCdrs({...this.req.locals.cdr,
terminated_at: now,
termination_reason: dlg.type === 'uas' ? 'caller hungup' : 'called party hungup',
@@ -637,6 +805,22 @@ class CallSession extends Emitter {
forwardInDialogRequests(uac, ['notify', 'options', 'message']);
}
_startRecordingNoAnswerTimer(res) {
this._clearRecordingNoAnswerTimer();
this.recordingNoAnswerTimer = setTimeout(() => {
this.logger.info('No response from SipRec server, return error to feature server');
this.isRecordingNoAnswerResponded = true;
res.send(400);
}, this.recordingNoAnswerTimeout);
}
_clearRecordingNoAnswerTimer() {
if (this.recordingNoAnswerTimer) {
clearTimeout(this.recordingNoAnswerTimer);
this.recordingNoAnswerTimer = null;
}
}
_stopRecording() {
if (this.srsClients.length) {
this.srsClients.forEach((c) => c.stop());
@@ -647,12 +831,19 @@ class CallSession extends Emitter {
async _onRefer(dlg, req, res) {
/* REFER coming in from a sip device, forward to feature server */
try {
const customHeaders = Object.keys(req.headers)
.filter((h) => h.toLowerCase().startsWith('x-'))
.reduce((acc, h) => {
acc[h] = req.get(h);
return acc;
}, {});
const response = await dlg.other.request({
method: 'REFER',
headers: {
'Refer-To': req.get('Refer-To'),
'Referred-By': req.get('Referred-By'),
'User-Agent': req.get('User-Agent')
'User-Agent': req.get('User-Agent'),
...customHeaders
}
});
res.send(response.status, response.reason);
@@ -693,16 +884,50 @@ Duration=${payload.duration} `
try {
const reason = req.get('X-Reason');
const isReleasingMedia = reason && dlg.type === 'uas' && ['release-media', 'anchor-media'].includes(reason);
const isFullMediaRelease = reason === 'release-media-entirely' && process.env.JAMBONES_ENABLE_FULL_MEDIA_RELEASE;
const fromTag = dlg.type === 'uas' ? this.rtpEngineOpts.uas.tag : this.rtpEngineOpts.uac.tag;
const toTag = dlg.type === 'uas' ? this.rtpEngineOpts.uac.tag : this.rtpEngineOpts.uas.tag;
const offerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uac.mediaOpts : this.rtpEngineOpts.uas.mediaOpts;
const answerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uas.mediaOpts : this.rtpEngineOpts.uac.mediaOpts;
let offerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uac.mediaOpts : this.rtpEngineOpts.uas.mediaOpts;
let answerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uas.mediaOpts : this.rtpEngineOpts.uac.mediaOpts;
const direction = dlg.type === 'uas' ? ['private', 'public'] : ['public', 'private'];
if (isFullMediaRelease) {
const a_sdp = await this.retrieveKey(makePartnerFullMediaReleaseKey(this.req.get('X-CID')));
this.logger.info({a_sdp}, 'reinvite ourselves out of the media path with this reinvite offer');
const answerSdp = await dlg.other.modify(a_sdp);
this.logger.info({answerSdp}, 'far end response to full media release');
res.send(200, {
body: dlg.local.sdp,
headers: {
'Contact': this.contactHeader
}
});
/* no media going through us now we can destroy the rtpengine resource */
this.rtpEngineResource.destroy().catch((err) => {
this.logger.info({err}, 'Error destroying rtpengine resource after full media release');
});
this._mediaPath = MediaPath.NoMedia;
return;
}
if (isReleasingMedia) {
if (!offerMedia.flags.includes('port latching')) offerMedia.flags.push('port latching');
if (!offerMedia.flags.includes('asymmetric')) offerMedia.flags.push('asymmetric');
offerMedia.flags = offerMedia.flags.filter((f) => f !== 'media handover');
}
const acceptCodecs = process.env.JAMBONES_ACCEPT_AND_TRANSCODE ?
process.env.JAMBONES_ACCEPT_AND_TRANSCODE :
process.env.JAMBONES_ACCEPT_G729 ? 'g729' : '';
offerMedia = { ...offerMedia, ...(acceptCodecs && dlg.type === 'uac' &&
{ codec: { mask: acceptCodecs, transcode: 'pcmu,pcma' } })};
answerMedia = { ...answerMedia, ...((process.env.JAMBONES_CODEC_OFFER_WITH_ORDER ||
process.env.JAMBONES_CODEC_TRANSCODE) && dlg.type === 'uac' &&
{ codec: {
...(process.env.JAMBONES_CODEC_OFFER_WITH_ORDER &&
{ offer: process.env.JAMBONES_CODEC_OFFER_WITH_ORDER.split(','), strip: 'all' }),
...(process.env.JAMBONES_CODEC_TRANSCODE &&
{ transcode: process.env.JAMBONES_CODEC_TRANSCODE.split(',') })
}})};
let opts = {
...this.rtpEngineOpts.common,
...offerMedia,
@@ -723,17 +948,20 @@ Duration=${payload.duration} `
/* if this is a re-invite from the FS to change media anchoring, avoid sending the reinvite out */
let sdp;
if (isReleasingMedia && !this.calleeIsUsingSrtp) {
//HL 2024-11-13: previously forwarded re-invites to webrtc clients but further testing has shown to be unnecessary
//if (isReleasingMedia && !this.calleeIsUsingSrtp) {
//DH 2024-11- 18: if we are going from no-media to either partial or full media, we need reinvite the far end
if (isReleasingMedia && this._mediaPath !== MediaPath.NoMedia) {
this.logger.info(`got a reinvite from FS to ${reason}`);
sdp = dlg.other.remote.sdp;
if (!answerMedia.flags.includes('port latching')) answerMedia.flags.push('port latching');
if (!answerMedia.flags.includes('asymmetric')) answerMedia.flags.push('asymmetric');
answerMedia.flags = answerMedia.flags.filter((f) => f !== 'media handover');
this._mediaReleased = 'release-media' === reason;
this._mediaPath = 'release-media' === reason ? MediaPath.PartialMedia : MediaPath.FullMedia;
}
else {
sdp = await dlg.other.modify(response.sdp);
this.logger.info({sdp}, 'CallSession:_onReinvite: got sdp from 200 OK to invite we sent');
}
opts = {
...this.rtpEngineOpts.common,
@@ -743,13 +971,25 @@ Duration=${payload.duration} `
sdp
};
response = await this.answer(opts);
/* now remove asymmetric as B party (looking at you Genesys ring group) may need port re-learning on invites */
answerMedia.flags = answerMedia.flags.filter((f) => f !== 'asymmetric');
if ('ok' !== response.result) {
res.send(488);
throw new Error(`_onReinvite: rtpengine failed: ${JSON.stringify(response)}`);
}
this.logger.debug({opts, sdp: response.sdp}, 'CallSession:_onReinvite: (answer) sending back upstream');
res.send(200, {body: response.sdp});
let responseSdp = response.sdp;
if (process.env.JAMBONES_VIDEO_CALLS_ENABLED_IN_FS) {
responseSdp = removeVideoSdp(responseSdp);
}
res.send(200, {
body: responseSdp,
headers: {
'Contact': this.contactHeader
}
});
} catch (err) {
res.send(err.status || 500);
this.logger.error(err, 'Error handling reinvite');
}
}
@@ -777,12 +1017,12 @@ Duration=${payload.duration} `
}
else if (reason.includes('CallRecording')) {
let succeeded = false;
const headers = contentType === 'application/json' && req.body ? JSON.parse(req.body) : {};
if (reason === 'startCallRecording') {
const from = this.req.getParsedHeader('From');
const to = this.req.getParsedHeader('To');
const aorFrom = from.uri;
const aorTo = to.uri;
const headers = contentType === 'application/json' && req.body ? JSON.parse(req.body) : {};
this.logger.info({to, from}, 'startCallRecording request for an outbound call');
const srsUrl = req.get('X-Srs-Url');
@@ -822,52 +1062,102 @@ Duration=${payload.duration} `
blockMedia: this.blockMedia,
unblockMedia: this.unblockMedia,
unsubscribe: this.unsubscribe,
headers
headers,
isSipRecCall: Array.isArray(this.req.payload) && this.req.payload.length > 1
}));
try {
succeeded = (await Promise.all(
this.srsClients.map((c) => c.start())
)).every((r) => r);
this._startRecordingNoAnswerTimer(res);
await Promise.any(this.srsClients.map((c) => c.start()));
succeeded = true;
} catch (err) {
this.logger.error({err}, 'Error starting SipRec call recording');
succeeded = false;
}
}
else if (reason === 'stopCallRecording') {
if (!this.srsClients.length) {
if (!this.srsClients.length || !this.srsClients.some((c) => c.activated)) {
res.send(400);
this.logger.info('discarding stopCallRecording request because we are not recording');
return;
}
try {
succeeded = (await Promise.all(
this.srsClients.map((c) => c.stop())
)).every((r) => r);
this._startRecordingNoAnswerTimer(res);
await Promise.any(this.srsClients.map((c) => {
if (c.activated) {
c.stop();
}
}));
succeeded = true;
} catch (err) {
this.logger.error({err}, 'Error stopping SipRec call recording');
succeeded = false;
}
this.srsClients = [];
}
else if (reason === 'pauseCallRecording') {
if (!this.srsClients.length || this.srsClients.every((c) => c.paused)) {
if (!this.srsClients.length || !this.srsClients.some((c) => c.activated && !c.paused)) {
this.logger.info('discarding invalid pauseCallRecording request');
res.send(400);
return;
}
succeeded = (await Promise.all(
this.srsClients.map((c) => c.pause())
)).every((r) => r);
try {
this._startRecordingNoAnswerTimer(res);
await Promise.any(this.srsClients.map((c) => {
if (c.activated && !c.paused) {
c.pause({headers});
}
}));
succeeded = true;
} catch (err) {
this.logger.error({err}, 'Error pausing SipRec call recording');
succeeded = false;
}
}
else if (reason === 'resumeCallRecording') {
if (!this.srsClients.length || !this.srsClients.every((c) => c.paused)) {
if (!this.srsClients.length || !this.srsClients.some((c) => c.activated && c.paused)) {
res.send(400);
this.logger.info('discarding invalid resumeCallRecording request');
return;
}
succeeded = (await Promise.all(
this.srsClients.map((c) => c.resume())
)).every((r) => r);
try {
this._startRecordingNoAnswerTimer(res);
await Promise.any(this.srsClients.map((c) => {
if (c.activated && c.paused) {
c.resume({headers});
}
}));
succeeded = true;
} catch (err) {
this.logger.error({err}, 'Error resuming SipRec call recording');
succeeded = false;
}
}
if (!this.isRecordingNoAnswerResponded) {
this._clearRecordingNoAnswerTimer();
res.send(succeeded ? 200 : 503);
}
} else if (reason.includes('Dtmf')) {
const arr = /Signal=\s*([0-9#*])/.exec(req.body);
if (!arr) {
this.logger.info({body: req.body}, '_onInfo: invalid INFO Dtmf');
throw new Error(`_onInfo: no dtmf in body for ${contentType}`);
}
const code = arr[1];
const arr2 = /Duration=\s*(\d+)/.exec(req.body);
const duration = arr2 ? arr2[1] : 250;
const volume = 13;
const dtmfOpts = {
...this.rtpEngineOpts.common,
'from-tag': this.rtpEngineOpts.uas.tag,
code,
duration,
volume
};
const response = await this.playDTMF(dtmfOpts);
if ('ok' !== response.result) {
this.logger.info({response}, `rtpengine play Dtmf failed with ${JSON.stringify(response)}`);
throw new Error('rtpengine failed: answer');
}
res.send(succeeded ? 200 : 503);
}
}
else if (dlg.type === 'uac' && ['application/dtmf-relay', 'application/dtmf'].includes(contentType)) {
@@ -905,10 +1195,9 @@ Duration=${payload.duration} `
}
}
else {
const immutableHdrs = ['via', 'from', 'to', 'call-id', 'cseq', 'max-forwards', 'content-length'];
const headers = {};
Object.keys(req.headers).forEach((h) => {
if (!immutableHdrs.includes(h)) headers[h] = req.headers[h];
if (!IMMUTABLE_HEADERS.includes(h)) headers[h] = req.headers[h];
});
const response = await dlg.other.request({method: 'INFO', headers, body: req.body});
const responseHeaders = {};
@@ -924,24 +1213,34 @@ Duration=${payload.duration} `
async _onFeatureServerTransfer(dlg, req, res) {
try {
// the following properties are ignored in the REFER headers
// eslint-disable-next-line no-unused-vars
const { via, from, to, 'call-id': callid, cseq, 'max-forwards': maxforwards,
// eslint-disable-next-line no-unused-vars
'content-length': _contentlength, 'refer-to': _referto, 'referred-by': referredby,
// eslint-disable-next-line no-unused-vars
'X-Refer-To-Leave-Untouched': _leave, contact: _contact,
...customHeaders
} = req.headers;
const referTo = req.getParsedHeader('Refer-To');
const uri = parseUri(referTo.uri);
this.logger.info({uri, referTo}, 'received REFER from feature server');
const arr = /context-(.*)/.exec(uri.user);
if (!arr) {
/* call transfer requested */
const referredBy = req.getParsedHeader('Referred-By');
if (!referredBy) return res.send(400);
const u = parseUri(referredBy.uri);
const farEnd = parseUri(this.connectedUri);
uri.host = farEnd.host;
uri.port = farEnd.port;
if (!req.has('Referred-By')) return res.send(400);
/* delete contact if it was there from feature server */
delete customHeaders['contact'];
const response = await this.uac.request({
method: 'REFER',
headers: {
'Refer-To': stringifyUri(uri),
'Referred-By': stringifyUri(u)
'Refer-To': `<${stringifyUri(uri)}>`,
'Referred-By': referredby,
'Contact': this.contactHeader,
...customHeaders
}
});
return res.send(response.status);
@@ -953,7 +1252,6 @@ Duration=${payload.duration} `
...(req.has('X-Retain-Call-Sid') && {'X-Retain-Call-Sid': req.get('X-Retain-Call-Sid')}),
...(req.has('X-Account-Sid') && {'X-Account-Sid': req.get('X-Account-Sid')})
};
const uac = await this.srf.createUAC(referTo.uri, {localSdp: dlg.local.sdp, headers});
this.uas = uac;
uac.type = 'uas';
@@ -962,12 +1260,32 @@ Duration=${payload.duration} `
uac.on('info', this._onInfo.bind(this, uac));
uac.on('modify', this._onReinvite.bind(this, uac));
uac.on('refer', this._onFeatureServerTransfer.bind(this, uac));
uac.on('destroy', () => {
uac.on('destroy', async() => {
this.logger.info('call ended with normal termination');
this.rtpEngineResource.destroy();
this.activeCallIds.delete(this.req.get('Call-ID'));
if (this.activeCallIds.size === 0) this.idleEmitter.emit('idle');
uac.other.destroy();
this._stopRecording();
if (this.req.locals.cdr) {
const now = Date.now();
const day = new Date();
// eslint-disable-next-line max-len
const {hasRecording = false} = await this.retrieveHash(`call:${this.account_sid}:${this.req.locals.cdr.call_sid}`) ?? {} ;
const recordAllCalls = this.req.locals.record_all_calls || hasRecording;
const record_format = this.req.locals.account.record_format || 'mp3';
let recording_url = `/Accounts/${this.account_sid}/RecentCalls/${this.req.locals.cdr.call_sid}/record`;
recording_url += `/${day.getFullYear()}/${(day.getMonth() + 1).toString().padStart(2, '0')}`;
recording_url += `/${day.getDate().toString().padStart(2, '0')}/${record_format}`;
this.writeCdrs({...this.req.locals.cdr,
terminated_at: now,
termination_reason: 'caller hungup',
sip_status: 200,
answered: true,
duration: Math.floor((now - this.req.locals.cdr.answered_at) / 1000),
...(recordAllCalls && {recording_url})
}).catch((err) => this.logger.error({err}, 'Error writing cdr for completed call'));
}
this.srf.endSession(this.req);
});
+7
View File
@@ -0,0 +1,7 @@
{
"MediaPath": {
"NoMedia": "no-media",
"PartialMedia": "partial-media",
"FullMedia": "full-media"
}
}
+41 -12
View File
@@ -1,7 +1,7 @@
const debug = require('debug')('jambonz:sbc-outbound');
const parseUri = require('drachtio-srf').parseUri;
const Registrar = require('@jambonz/mw-registrar');
const {selectHostPort, nudgeCallCounts} = require('./utils');
const { nudgeCallCounts} = require('./utils');
const FS_UUID_SET_NAME = 'fsUUIDs';
module.exports = (srf, logger, redisClient) => {
@@ -17,6 +17,8 @@ module.exports = (srf, logger, redisClient) => {
const initLocals = async(req, res, next) => {
req.locals = req.locals || {};
const callId = req.get('Call-ID');
req.locals.nudge = 0;
req.locals.callId = callId;
req.locals.account_sid = req.get('X-Account-Sid');
req.locals.application_sid = req.get('X-Application-Sid');
req.locals.record_all_calls = req.get('X-Record-All-Calls');
@@ -60,7 +62,7 @@ module.exports = (srf, logger, redisClient) => {
});
return req.srf.endSession(req);
}
} catch (err) {
} catch {
res.send(500);
return req.srf.endSession(req);
}
@@ -77,7 +79,11 @@ module.exports = (srf, logger, redisClient) => {
});
try {
req.locals.account = await lookupAccountBySid(req.locals.account_sid);
const account = await lookupAccountBySid(req.locals.account_sid);
req.locals.account = account;
if (account.enable_debug_log) {
req.locals.logger.level = 'debug';
}
req.locals.service_provider_sid = req.locals.account.service_provider_sid;
} catch (err) {
req.locals.logger.error({err}, `Error looking up account sid ${req.locals.account_sid}`);
@@ -103,10 +109,11 @@ module.exports = (srf, logger, redisClient) => {
/* decrement count if INVITE is later rejected */
res.once('end', async({status}) => {
if (status > 200) {
nudgeCallCounts(logger, {
nudgeCallCounts(req, 'failure', {
service_provider_sid,
account_sid,
application_sid
application_sid,
callId: req.locals.callId
}, decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
.catch((err) => logger.error(err, 'Error decrementing call counts'));
const tags = ['accepted:no', `sipStatus:${status}`];
@@ -119,10 +126,11 @@ module.exports = (srf, logger, redisClient) => {
});
/* increment the call count */
const {callsSP, calls} = await nudgeCallCounts(logger, {
const {callsSP, calls} = await nudgeCallCounts(req, 'init', {
service_provider_sid,
account_sid,
application_sid
application_sid,
callId: req.locals.callId
}, incrKey, {writeCallCountSP, writeCallCount, writeCallCountApp});
/* compare to account's limit, though avoid db hit when call count is low */
@@ -166,6 +174,11 @@ module.exports = (srf, logger, redisClient) => {
});
return req.srf.endSession(req);
}
if (!account_limit && !sp_limit && process.env.JAMBONES_HOSTING) {
logger.info(`checkLimits: no active subscription found for account ${account_sid}, rejecting call`);
res.send(503, 'No Active Subscription');
return req.srf.endSession(req);
}
if (process.env.JAMBONES_TRACK_SP_CALLS && sp_limit > 0 && callsSP > sp_limit) {
logger.info({callsSP, sp_limit}, 'checkLimits: service provider limits exceeded');
writeAlerts({
@@ -221,9 +234,12 @@ module.exports = (srf, logger, redisClient) => {
logger.info({details: reg}, `sending call to registered user ${aor}`);
if (req.server.hostport !== reg.sbcAddress) {
/* redirect to the correct SBC where this user is connected */
const proxyAddress = selectHostPort(reg.sbcAddress, 'tcp');
const redirectUri = `<sip:${proxyAddress[1]}>`;
logger.info(`redirecting call to SBC at ${redirectUri}`);
const proxyAddress = reg.privateSbcAddress.split(':');
const redirectUri = `<sip:${proxyAddress[0]}>`;
logger.info({
myHostPort: req.server.hostport,
registeredHostPort: reg.sbcAddress,
}, `redirecting call to SBC at ${redirectUri}`);
return res.send(302, {headers: {Contact: redirectUri}});
}
req.locals.registration = reg;
@@ -244,9 +260,22 @@ module.exports = (srf, logger, redisClient) => {
else if ('sip' === desiredRouting) {
// call that needs to be forwarded to a sip endpoint
logger.info(`forwarding call to sip endpoint ${req.uri}`);
// check if the domain is one of ours
if (process.env.JAMBONES_LOCAL_SIP_DOMAINS) {
const allowedDomains = process.env.JAMBONES_LOCAL_SIP_DOMAINS.split(',');
const domain = uri.host;
const isLoop = allowedDomains.some((allowed) =>
domain === allowed.trim() || domain.endsWith(`.${allowed.trim()}`)
);
if (isLoop) {
logger.info({ host: domain }, `returning 482 Loop Detected for attempt to send to: ${req.uri}`);
res.send(482, 'Loop Detected');
return req.srf.endSession(req);
}
}
req.locals.target = 'forward';
}
else if ('phone' === desiredRouting) {
} else if ('phone' === desiredRouting) {
debug('sending call to LCR');
req.locals.target = 'lcr';
}
+187 -19
View File
@@ -3,11 +3,10 @@ const srtpCharacteristics = require('../data/srtp-transcoding');
const debug = require('debug')('jambonz:sbc-outbound');
const CIDRMatcher = require('cidr-matcher');
const dns = require('dns');
const sdpTransform = require('sdp-transform');
const cidrMatcher = process.env.PRIVATE_VOIP_NETWORK_CIDR ?
new CIDRMatcher(process.env.PRIVATE_VOIP_NETWORK_CIDR.split(',')) : null;
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams) {
// eslint-disable-next-line max-len
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams, remove_ice = false, dtls_off = false) {
const from = req.getParsedHeader('from');
const rtpCopy = JSON.parse(JSON.stringify(rtpCharacteristics));
const srtpCopy = JSON.parse(JSON.stringify(srtpCharacteristics));
@@ -18,19 +17,53 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams
}
const srtpOpts = teams ? srtpCopy['teams'] : srtpCopy['default'];
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCopy;
const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCopy;
/* webrtc clients (e.g. sipjs) send DMTF via SIP INFO */
if ((srcIsUsingSrtp || dstIsUsingSrtp) && !teams) {
dstOpts.flags.push('inject DTMF');
srcOpts.flags.push('inject DTMF');
if (remove_ice) {
srtpOpts.ICE = 'remove';
}
if (dtls_off) {
srtpOpts.DTLS = 'off';
}
const dstOpts = JSON.parse(JSON.stringify(dstIsUsingSrtp ? srtpOpts : rtpCopy));
const srcOpts = JSON.parse(JSON.stringify(srcIsUsingSrtp ? srtpOpts : rtpCopy));
/** Allow feature server to send DTMF to the call excepts call from/to teams */
if (!teams) {
if (!dstOpts.flags.includes('inject DTMF')) {
dstOpts.flags.push('inject DTMF');
}
if (!srcOpts.flags.includes('inject DTMF')) {
srcOpts.flags.push('inject DTMF');
}
}
/** By default, and for backwards compatibility, use media handover
* set env var to true to use strict source instead (needed for rtpbleed vulnerability)
*/
const enableStrictSource = !!process.env.RTPENGINE_ENABLE_STRICT_SOURCE;
dstOpts.flags.push(enableStrictSource ? 'strict source' : 'media handover');
srcOpts.flags.push(enableStrictSource ? 'strict source' : 'media handover');
const common = {
'call-id': req.get('Call-ID'),
'replace': ['origin', 'session-connection'],
'record call': process.env.JAMBONES_RECORD_ALL_CALLS ? 'yes' : 'no'
};
const codec = {
accept: ['PCMU', 'PCMA', 'telephone-event'],
...(process.env.JAMBONES_CODEC_OFFER_WITH_ORDER &&
{
offer: process.env.JAMBONES_CODEC_OFFER_WITH_ORDER.split(','),
strip: 'all'
}),
...(process.env.JAMBONES_CODEC_TRANSCODE &&
{
transcode: process.env.JAMBONES_CODEC_TRANSCODE.split(',')
}),
};
return {
common,
uas: {
@@ -39,7 +72,10 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams
},
uac: {
tag: null,
mediaOpts: dstOpts
mediaOpts: {
...dstOpts,
codec,
}
}
};
}
@@ -126,13 +162,96 @@ const createHealthCheckApp = (port, logger) => {
});
};
const nudgeCallCounts = async(logger, sids, nudgeOperator, writers) => {
const {service_provider_sid, account_sid, application_sid} = sids;
/**
* nudgeCallCounts - increment or decrement call counts in redis
*
* current nudge value
* -----------------------------------------
* why | -1 | 0 | 1 |
* -----------------------------------------
* init | no-op | +1 | N/A |
* failure | N/A | -1 | -1 |
* complete| N/A | N/A | -1 |
*
*
*/
const shouldNudge = (why, req) => {
const {nudge, logger} = req.locals;
let modifyCount = false;
const originalNudge = nudge;
switch (why) {
case 'init':
if (nudge === 0) {
// normal case: new call, increment call count
req.locals.nudge = 1;
modifyCount = true;
}
else if (nudge === -1) {
// extremely quick cancel, don't increment call count
req.locals.nudge = 0;
}
else {
logger.info(`shouldNudge: unexpected nudge value ${nudge} for ${why}`);
}
break;
case 'failure':
if (nudge === 1) {
// normal case of call failed for any reason, decrement call count
req.locals.nudge = 0;
modifyCount = true;
}
else if (nudge === 0) {
// very quick failure dont decrement call count
req.locals.nudge = -1;
}
else {
logger.info(`shouldNudge: unexpected nudge value ${nudge} for ${why}`);
}
break;
case 'complete':
if (nudge === 1) {
// normal case of call completed, decrement call count
req.locals.nudge = 0;
modifyCount = true;
}
else {
logger.info(`shouldNudge: unexpected nudge value ${nudge} for ${why}`);
}
break;
default:
logger.info(`shouldNudge: unexpected why value ${why}`);
break;
}
logger.info(`shouldNudge: '${why}': updating count: ${modifyCount}, nudge: ${originalNudge} -> ${req.locals.nudge}`);
return modifyCount;
};
const nudgeCallCounts = async(req, why, sids, nudgeOperator, writers) => {
const {logger} = req.locals;
const {service_provider_sid, account_sid, application_sid, callId} = sids;
const {writeCallCount, writeCallCountSP, writeCallCountApp} = writers;
const nudges = [];
const writes = [];
logger.debug(sids, 'nudgeCallCounts');
if (!shouldNudge(why, req)) {
return {callsSP: null, calls: null, callsApp: null};
}
if (process.env.JAMBONES_DEBUG_CALL_COUNTS) {
const {srf} = require('..');
const {addKey, deleteKey} = srf.locals.realtimeDbHelpers;
if (why === 'init') {
// save for 3 days
await addKey(`debug:outcalls:${account_sid}:${callId}`, new Date().toISOString(), 259200);
}
else {
await deleteKey(`debug:outcalls:${account_sid}:${callId}`);
}
}
if (process.env.JAMBONES_TRACK_SP_CALLS) {
const key = makeSPCallCountKey(service_provider_sid);
@@ -187,9 +306,13 @@ const nudgeCallCounts = async(logger, sids, nudgeOperator, writers) => {
};
const isPrivateVoipNetwork = async(uri) => {
if (cidrMatcher) {
const {srf, logger} = require('..');
const {privateNetworkCidr} = srf.locals;
if (privateNetworkCidr) {
try {
const arr = /sips?:.*@(.*?)(:\d+)?(;.*)$/.exec(uri);
const matcher = new CIDRMatcher(privateNetworkCidr.split(','));
const arr = /sips?:.*@(.*?)(:\d+)?(;.*)?$/.exec(uri);
if (arr) {
const input = arr[1];
let addresses;
@@ -199,16 +322,56 @@ const isPrivateVoipNetwork = async(uri) => {
addresses = await dns.resolve4(input);
}
for (const ip of addresses) {
if (cidrMatcher.contains(ip)) {
if (matcher.contains(ip)) {
return true;
}
}
}
} catch (err) {}
} catch (err) {
logger.info({err, privateNetworkCidr},
'Error checking private network CIDR, probably misconfigured must be a comma separated list of CIDRs');
}
}
return false;
};
function makeBlacklistGatewayKey(key) {
return `blacklist-sip-gateway:${key}`;
}
async function isBlackListedSipGateway(client, logger, sip_gateway_sid) {
try {
return await client.exists(makeBlacklistGatewayKey(sip_gateway_sid)) === 1;
} catch (err) {
logger.error({err}, `isBlackListedSipGateway: error while checking blacklist for ${sip_gateway_sid}`);
}
}
const makeFullMediaReleaseKey = (callId) => {
return `b_sdp:${callId}`;
};
const makePartnerFullMediaReleaseKey = (callId) => {
return `a_sdp:${callId}`;
};
function isValidDomainOrIP(input) {
const domainRegex = /^(?!:\/\/)([a-zA-Z0-9.-]+)(:\d+)?$/;
// eslint-disable-next-line max-len
const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(:\d+)?$/;
if (domainRegex.test(input) || ipRegex.test(input)) {
return true;
}
return false; // Invalid input
}
const removeVideoSdp = (sdp) => {
const parsedSdp = sdpTransform.parse(sdp);
// Filter out video media sections, keeping only non-video media
parsedSdp.media = parsedSdp.media.filter((media) => media.type !== 'video');
return sdpTransform.write(parsedSdp);
};
module.exports = {
makeRtpEngineOpts,
selectHostPort,
@@ -219,5 +382,10 @@ module.exports = {
systemHealth,
createHealthCheckApp,
nudgeCallCounts,
isPrivateVoipNetwork
isPrivateVoipNetwork,
isBlackListedSipGateway,
makeFullMediaReleaseKey,
makePartnerFullMediaReleaseKey,
isValidDomainOrIP,
removeVideoSdp,
};
+1976 -5329
View File
File diff suppressed because it is too large Load Diff
+21 -20
View File
@@ -1,9 +1,9 @@
{
"name": "sbc-outbound",
"version": "0.8.5",
"version": "0.9.11",
"main": "app.js",
"engines": {
"node": ">= 12.0.0"
"node": ">= 20.0.0"
},
"keywords": [
"sip",
@@ -24,30 +24,31 @@
"start": "node app",
"test": "NODE_ENV=test HTTP_PORT=3050 JAMBONES_HOSTING=1 JAMBONES_NETWORK_CIDR=127.0.0.1/32 JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=error DRACHTIO_SECRET=cymru DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 JAMBONES_RTPENGINES=127.0.0.1:12222 node test/ ",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
"jslint": "eslint app.js lib"
"jslint": "eslint app.js lib --fix"
},
"dependencies": {
"@jambonz/db-helpers": "^0.9.0",
"@jambonz/realtimedb-helpers": "^0.8.7",
"@babel/helpers": "^7.26.10",
"@jambonz/db-helpers": "^0.9.18",
"@jambonz/http-health-check": "^0.0.1",
"@jambonz/mw-registrar": "0.2.4",
"@jambonz/rtpengine-utils": "^0.4.3",
"@jambonz/siprec-client-utils": "^0.2.6",
"@jambonz/stats-collector": "^0.1.9",
"@jambonz/time-series": "^0.2.5",
"@jambonz/mw-registrar": "0.2.7",
"@jambonz/realtimedb-helpers": "^0.8.21",
"@jambonz/rtpengine-utils": "^0.4.4",
"@jambonz/siprec-client-utils": "^0.2.10",
"@jambonz/stats-collector": "^0.1.10",
"@jambonz/time-series": "^0.2.10",
"cidr-matcher": "^2.1.1",
"debug": "^4.3.4",
"drachtio-fn-b2b-sugar": "^0.0.12",
"drachtio-srf": "^4.5.31",
"express": "^4.18.1",
"pino": "^7.11.0",
"sdp-transform": "^2.14.1"
"debug": "^4.4.3",
"drachtio-fn-b2b-sugar": "^0.2.1",
"drachtio-srf": "^5.0.21",
"express": "^4.21.2",
"pino": "^10.1.0",
"sdp-transform": "^2.15.0"
},
"devDependencies": {
"bent": "^7.3.12",
"eslint": "^7.32.0",
"eslint-plugin-promise": "^5.2.0",
"nyc": "^15.1.0",
"tape": "^5.5.3"
"eslint": "^9.17.0",
"eslint-plugin-promise": "^7.2.1",
"nyc": "^17.1.0",
"tape": "^5.9.0"
}
}
+62 -6
View File
@@ -1,4 +1,5 @@
/* SQLEditor (MySQL (2))*/
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS account_static_ips;
@@ -13,6 +14,8 @@ DROP TABLE IF EXISTS beta_invite_codes;
DROP TABLE IF EXISTS call_routes;
DROP TABLE IF EXISTS clients;
DROP TABLE IF EXISTS dns_records;
DROP TABLE IF EXISTS lcr;
@@ -51,6 +54,8 @@ DROP TABLE IF EXISTS signup_history;
DROP TABLE IF EXISTS smpp_addresses;
DROP TABLE IF EXISTS google_custom_voices;
DROP TABLE IF EXISTS speech_credentials;
DROP TABLE IF EXISTS system_information;
@@ -127,6 +132,19 @@ application_sid CHAR(36) NOT NULL,
PRIMARY KEY (call_route_sid)
) COMMENT='a regex-based pattern match for call routing';
CREATE TABLE clients
(
client_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
username VARCHAR(64),
password VARCHAR(1024),
allow_direct_app_calling BOOLEAN NOT NULL DEFAULT 1,
allow_direct_queue_calling BOOLEAN NOT NULL DEFAULT 1,
allow_direct_user_calling BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY (client_sid)
);
CREATE TABLE dns_records
(
dns_record_sid CHAR(36) NOT NULL UNIQUE ,
@@ -322,14 +340,27 @@ last_tested DATETIME,
tts_tested_ok BOOLEAN,
stt_tested_ok BOOLEAN,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
label VARCHAR(64),
PRIMARY KEY (speech_credential_sid)
);
CREATE TABLE google_custom_voices
(
google_custom_voice_sid CHAR(36) NOT NULL UNIQUE ,
speech_credential_sid CHAR(36) NOT NULL,
model VARCHAR(512) NOT NULL,
reported_usage ENUM('REPORTED_USAGE_UNSPECIFIED','REALTIME','OFFLINE') DEFAULT 'REALTIME',
name VARCHAR(64) NOT NULL,
PRIMARY KEY (google_custom_voice_sid)
);
CREATE TABLE system_information
(
domain_name VARCHAR(255),
sip_domain_name VARCHAR(255),
monitoring_domain_name VARCHAR(255)
monitoring_domain_name VARCHAR(255),
private_network_cidr VARCHAR(8192),
log_level ENUM('info', 'debug') NOT NULL DEFAULT 'info'
);
CREATE TABLE users
@@ -411,7 +442,7 @@ PRIMARY KEY (smpp_gateway_sid)
CREATE TABLE phone_numbers
(
phone_number_sid CHAR(36) UNIQUE ,
number VARCHAR(132) NOT NULL UNIQUE ,
number VARCHAR(132) NOT NULL,
voip_carrier_sid CHAR(36),
account_sid CHAR(36),
application_sid CHAR(36),
@@ -424,11 +455,14 @@ CREATE TABLE sip_gateways
sip_gateway_sid CHAR(36),
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.',
netmask INTEGER NOT NULL DEFAULT 32,
port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port',
port INTEGER COMMENT 'sip signaling port',
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
voip_carrier_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
send_options_ping BOOLEAN NOT NULL DEFAULT 0,
use_sips_scheme BOOLEAN NOT NULL DEFAULT 0,
pad_crypto BOOLEAN NOT NULL DEFAULT 0,
protocol ENUM('udp','tcp','tls', 'tls/srtp') DEFAULT 'udp' COMMENT 'Outbound call protocol',
PRIMARY KEY (sip_gateway_sid)
) COMMENT='A whitelisted sip gateway used for origination/termination';
@@ -465,10 +499,21 @@ messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
app_json TEXT,
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
speech_synthesis_voice VARCHAR(64),
speech_synthesis_voice VARCHAR(256),
speech_synthesis_label VARCHAR(64),
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
speech_recognizer_label VARCHAR(64),
use_for_fallback_speech BOOLEAN DEFAULT false,
fallback_speech_synthesis_vendor VARCHAR(64),
fallback_speech_synthesis_language VARCHAR(12),
fallback_speech_synthesis_voice VARCHAR(256),
fallback_speech_synthesis_label VARCHAR(64),
fallback_speech_recognizer_vendor VARCHAR(64),
fallback_speech_recognizer_language VARCHAR(64),
fallback_speech_recognizer_label VARCHAR(64),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
record_all_calls BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (application_sid)
) COMMENT='A defined set of behaviors to be applied to phone calls ';
@@ -506,6 +551,10 @@ subspace_client_secret VARCHAR(255),
subspace_sip_teleport_id VARCHAR(255),
subspace_sip_teleport_destinations VARCHAR(255),
siprec_hook_sid CHAR(36),
record_all_calls BOOLEAN NOT NULL DEFAULT false,
record_format VARCHAR(16) NOT NULL DEFAULT 'mp3',
bucket_credential VARCHAR(8192) COMMENT 'credential used to authenticate with storage service',
enable_debug_log BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (account_sid)
) COMMENT='An enterprise that uses the platform for comm services';
@@ -526,6 +575,9 @@ ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERE
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
CREATE INDEX client_sid_idx ON clients (client_sid);
ALTER TABLE clients ADD CONSTRAINT account_sid_idxfk_13 FOREIGN KEY account_sid_idxfk_13 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid);
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
@@ -590,8 +642,6 @@ CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid);
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid);
CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid);
CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
@@ -599,6 +649,10 @@ ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (ser
CREATE INDEX account_sid_idx ON speech_credentials (account_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX google_custom_voice_sid_idx ON google_custom_voices (google_custom_voice_sid);
CREATE INDEX speech_credential_sid_idx ON google_custom_voices (speech_credential_sid);
ALTER TABLE google_custom_voices ADD FOREIGN KEY speech_credential_sid_idxfk (speech_credential_sid) REFERENCES speech_credentials (speech_credential_sid) ON DELETE CASCADE;
CREATE INDEX user_sid_idx ON users (user_sid);
CREATE INDEX email_idx ON users (email);
CREATE INDEX phone_idx ON users (phone);
@@ -628,6 +682,8 @@ CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid);
CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid);
ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
CREATE UNIQUE INDEX phone_numbers_unique_idx_voip_carrier_number ON phone_numbers (number,voip_carrier_sid);
CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid);
CREATE INDEX number_idx ON phone_numbers (number);
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
+1 -1
View File
@@ -24,7 +24,7 @@ services:
ipv4_address: 172.39.0.2
sbc:
image: drachtio/drachtio-server:latest
image: drachtio/drachtio-server:0.8.26
command: drachtio --contact "sip:*;transport=udp" --loglevel debug --sofia-loglevel 9
ports:
- "9060:9022/tcp"
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="UAC with media">
<send retrans="500">
<![CDATA[
INVITE sip:16173333456@127.0.0.1 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@127.0.0.1>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: ed649e33-e771-403a-8c99-1780eabbc803
X-Call-Sid: ff49e33-e771-403a-8c99-1780eabbc803
X-Jambonz-Routing: phone
Subject: uac-pcap-carrier-fail-limits
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>
<recv response="100" optional="true">
</recv>
<recv response="603" rtd="true" crlf="true">
</recv>
<send>
<![CDATA[
ACK sip:sip:+16173333456@127.0.0.1 SIP/2.0
[last_Via]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
To: <sip:sip:+16173333456@127.0.0.1>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Subject: uac-pcap-carrier-fail-blacklist
Content-Length: 0
]]>
</send>
</scenario>
+12 -1
View File
@@ -26,6 +26,7 @@ function connect(connectable) {
test('sbc-outbound tests', async(t) => {
const {srf} = require('../app');
const { queryCdrs } = srf.locals;
const redisClient = srf.locals.realtimeDbHelpers.client;
try {
await connect(srf);
@@ -95,7 +96,17 @@ test('sbc-outbound tests', async(t) => {
/* fails when session limit exceeded */
await sippUac('uac-pcap-carrier-fail-limits.xml');
t.pass('fails when max calls in progress');
// re-rack test data
execSync(`mysql -h 127.0.0.1 -u root --protocol=tcp -D jambones_test < ${__dirname}/db/jambones-sql.sql`);
execSync(`mysql -h 127.0.0.1 -u root --protocol=tcp -D jambones_test < ${__dirname}/db/populate-test-data.sql`);
// Black list good carrier for some seconds
await redisClient.setex('blacklist-sip-gateway:124a5339-c62c-4075-9e19-f4de70a96597', 3, '');
await sippUac('uac-pcap-carrier-fail-blacklist.xml');
t.pass('fails when carrier is blacklisted');
await redisClient.del('blacklist-sip-gateway:124a5339-c62c-4075-9e19-f4de70a96597');
await waitFor(25);
const res = await queryCdrs({account_sid: 'ed649e33-e771-403a-8c99-1780eabbc803'});