Compare commits

...
164 Commits
Author SHA1 Message Date
Dave Horton 8ce531ec81 revert change (for now) that caused audio issues when reinviting to partial media (#212) 2025-08-18 12:46:13 -04:00
Sam Machin 7bc2a1a6f4 Check for loops (#211)
* check for matching x-application-sid header

and tests

* lint

* update test scenario

* rename file

* now with valid test data!
2025-08-18 07:24:45 -04:00
Sam Machin 83461f99e8 Set strict source on rtpengine (#210)
* Set strict source on rtpengine

RTPBleed

* use env var for strict source

* lint

* Update utils.js

* arghhhh

* clarification

* change
2025-08-03 19:44:47 -04:00
Dave Horton 45684aa337 bump version 2025-07-15 11:46:09 -04:00
Vinod Dharashive 9d0c829ae8 increase dtmf volume (#208)
https://github.com/jambonz/jambonz-feature-server/issues/1272
2025-07-09 08:23:17 -04:00
sathish kumar pasham 7a6551c141 Fix security vulnerabilities by upgrading to @babel/helpers version 7.26.10. (#207) 2025-06-27 08:14:26 -04:00
rammohan-y 5f267906b1 Fixed issue when wild cards or regex is used in phone number for multiple carriers (#202)
* Fixed issue when wild cards or regex is used in phone number for multiple carriers
https://github.com/jambonz/sbc-inbound/issues/201

* Implemanted the regex/pattern based logic when url is a sip_realm

* Refactored getApplicationForDidAndCarrier as it is failing to parse other regex if first regex is invalid
In gateway is defined at "all accounts" using the phone number's account_sid

* Performance improvements

* Exported getApplicationForDidAndCarriers method

* Added additional unit test cases
2025-06-18 21:11:05 +02:00
Dave Horton 680d32e9a2 update version 2025-05-14 15:42:00 -04:00
Dave Horton 71c437ee07 update db-helpers 2025-05-13 10:35:46 -04:00
rammohan-y 35c38685a9 Updated drachtio-srf version (#199)
https://github.com/drachtio/drachtio-server/issues/424
2025-04-22 11:05:30 -04:00
Hoan Luu Huu 8d15098b77 update drachtio srf 5.0.4 (#198) 2025-04-18 07:45:18 -04:00
Hoan Luu Huu 656ad3b6cb update digest-utils version (#197) 2025-04-17 19:45:39 -04:00
rammohan-y 7eccb6816d Updated db-helper to 0.9.11 (#196)
https://github.com/jambonz/jambonz-feature-server/issues/1151
2025-04-14 08:22:17 -04:00
rammohan-y f58cf8eecf updated realtimedb-helper to 0.8.13 (#195) 2025-03-10 09:54:03 -04:00
Dave Horton 638c003dfc fix aws sns topic subscribe (#193) 2025-02-24 12:57:23 -05:00
Dave Horton 23c748046d REFER should have sips contact if far end is using sips (#192) 2025-02-24 09:37:18 -05:00
Dave Horton 086adfbeef update drachtio-srf 2025-02-20 10:15:25 -05:00
Dave Horton d9b375738b reject calls on hosted jambonz with no active subscriptions (#191) 2025-02-19 13:05:56 -05:00
Hoan Luu Huu ec3af43a41 support sip recording from a siprec call (#190)
* support sip recording from a siprec call

* update srsClient version
2025-02-12 09:24:12 -05:00
Dave Horton 0ba5cb53bb refined the method for syncing call count updates and added debugging (#189)
* refined the method for syncing call count updates and added debugging

* include callId in debug key

* fix debug key name
2025-02-05 13:00:13 -05:00
Dave Horton 25415c0be9 logging 2025-01-17 09:11:08 -05:00
Dave Horton c0e42bcdaa update deps 2025-01-14 10:49:31 -05:00
Dave Horton 8f3ea260ca fix race condition with call counts when quick cancel is received (#188)
* fix race condition with call counts when quick cancel is received

* wip

* update dractio-srf and mrf to main branch releases
2025-01-14 10:04:03 -05:00
Dave Horton c807fdb93d Feat/3pcc invite (#187)
* wip

* wip

* wip

* add test for late media / 3pcc invite, which should now work
2024-12-12 18:39:01 -05:00
Hoan Luu Huu a7d6cf5a31 support referby with display name (#186)
* support referby with display name

* update siprec client utils
2024-12-11 10:34:54 -05:00
Dave Horton 1cd526c684 wip (#182) 2024-11-19 09:38:19 -05:00
Dave Horton 50b249544e fix #180 (#181) 2024-11-14 17:52:34 -05:00
Hoan Luu Huu 4add329102 support force opus pcmu transcode (#175)
* support force opus pcmu transcode

* fix review comment

* wip
2024-11-14 12:22:45 -05:00
André HeberandAndre Heber 06b3122571 reject late media call with 488 and X-Reason (#179)
* reject late media call with 488 and X-Reason

* test added

* changed X-Reason, clarified test

---------

Co-authored-by: Andre Heber <a.heber@cognigy.com>
2024-11-14 09:13:39 -05:00
Hoan Luu Huu bcfbe9a0fc fixed incase srtp, dont need to send reinvite when release media (#176)
* fixed, srtp dont need to send reinvite when release media

* fix review comment
2024-11-13 07:42:42 -05:00
Dave Horton bb9e5e98f5 minor logging 2024-11-12 09:27:12 -05:00
Dave Horton a714190aa4 fix race condition #177 (#178) 2024-11-12 09:15:19 -05:00
Hoan Luu Huu e6d41dec71 update time series version (#171) 2024-10-17 08:07:10 -04:00
Hoan Luu Huu a6b1e58b87 support change log level run time (#169)
* support change log level run time

* support change log level run time
2024-10-07 09:52:01 -04:00
Hoan Luu Huu 524a556a7c fixed pad_crypto does not work if not a incoming call to a sip realm (#167)
* fixed pad_crypto does not work if not a incoming call to a sip realm

* fixed review comment
2024-09-19 10:26:22 -04:00
Markus FrindtandMarkus Frindt b6d856c626 fix count of alert limits (#151)
Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2024-09-19 07:12:55 -04:00
Dave Horton 3454232314 bump version 2024-09-18 20:46:49 -04:00
Hoan Luu Huu a97cadc4b9 allow inbound call SRTP pad crypto (#161) 2024-09-11 09:37:28 +01:00
Dave Horton 20755f456d use system_information.private_network_cidr (#158)
* use system_information.private_network_cidr

* lint
2024-08-18 12:50:01 -04:00
Dave Horton fec4c42008 fix/gh-actions 2024-08-07 10:03:22 -04:00
javibookline 34cfe8ca9d Handle cases where gateway query at account level returns more than one (#135)
* Handle cases where gateway query at account level returns more than one

* Change constant variable as it needs to be reassigned

* Fix mistake for handling case where sql query returned no match

* grammar

* Fix linting errors

* Avoid checking for duplicates in case there is only one match to avoid stringifying+parsing for nothing
2024-08-07 10:02:24 -04:00
Dave Horton 55bcba05f6 update gh actions (#156) 2024-08-01 08:48:40 -04:00
Markus FrindtandMarkus Frindt c270c0df43 Feat/improve parse host ports (#149)
* Move parseHostPorts to utils and cover with test suite

* fix tabs

* comment pre commit hook

* add test

* wip

* remove only

* fix 2 critical, 12 high vulnerabilities via npm audit fix

---------

Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2024-08-01 08:41:49 -04:00
Dave Horton b848ab99e1 fix for #153 (#154) 2024-07-24 12:27:35 -04:00
Dave Horton 4153a141d7 sip scheme in contact header of re-invite 200 OK should be same as initial 200 OK (#150) 2024-07-10 15:56:08 -06:00
Dave Horton e999b4e5e8 Contact in 200 should set sip(s) appropriately and have port and prot… (#148)
* Contact in 200 should set sip(s) appropriately and have port and protocol

* fix for k8s

* lint

* wip
2024-07-03 10:56:23 -06:00
Vinod Dharashive 0e2285c4b8 Contact header to have private IP on Private cidr (#147)
* Contact header to have private IP on Private cidr

fix for #146

* Update call-session.js

* Update call-session.js

* Update call-session.js
2024-06-25 14:34:07 -04:00
Dave Horton f9ae466709 #142 add option to transcode incoming g729 calls to pcmu (#144) 2024-05-22 12:42:06 -04:00
Dave Horton 831005f7a1 discover local ip address based on authentication response from drachtio-server (#143) 2024-05-16 10:50:07 -04:00
Hoan Luu Huu a46ad934a8 support complex refer-to and refered-by uri (#140)
* support complex refer-to and refered-by uri

* wip
2024-05-07 08:27:32 -04:00
Hoan Luu Huu d200799997 update stats version (#141) 2024-05-06 20:07:05 -04:00
Dave Horton 2fef169e92 update deps (#138) 2024-04-07 18:33:28 -04:00
Hoan Luu Huu 5829ecdf4c fix cannot clear siprec sessions (#136) 2024-03-27 07:25:07 -04:00
Hoan Luu Huu 03d3a9695d forward reinvite error response to other destination (#133)
* forward reinvite error response to other destination

* update comment review
2024-03-07 07:48:33 -05:00
Hoan Luu Huu f5be95006c start/stop/pause/resume recording success when one of siprec server success. (#131)
* start/stop/pause/resume recording success when one of siprec server success.

* wip

* wip

* update customer header for pause and resume

* allow timeout for siprec action

* wip

* wip

* wip

* update siprec client utils
2024-03-05 13:05:42 -05:00
Dave Horton 2747bc15d6 update deps, comment out questionable code 2024-01-17 13:37:07 -05:00
Dave Horton cdfbf384fe update db-helpers 2024-01-17 13:26:37 -05:00
Hoan Luu Huu 96a83619c7 fix sbc to forward BYE header (#129)
* fix sbc to forward BYE header

* fix sbc to forward BYE header
2023-12-28 20:45:38 -05:00
Hoan Luu Huu b8a67019a1 support sending DTMF to caller (#125)
* support sending DTMF to caller

* wip
2023-12-13 09:14:27 -05:00
Hoan Luu Huu 0e1627ba82 phone number's application should override voice carrier default application (#128)
* phone number's application should override voice carrier default application

* fix review comments

* fix review comments
2023-12-13 08:25:26 -05:00
Hoan Luu Huu bb857a72ff let realtimedb-utils build configuration from env vars (#122) 2023-11-14 09:13:08 -05:00
Hoan Luu Huu aea3033ae0 support PRIVATE_VOIP_NETWORK_CIDR for SDP negotiation (#124)
* support PRIVATE_VOIP_NETWORK_CIDR for SDP negotiation

* support multi PRIVATE_VOIP_NETWORK_CIDR
2023-11-13 15:24:41 -05:00
Dave Horton 9b9eaa37fb 0.8.5 2023-11-09 12:39:24 -05:00
Dave Horton 56e0e3c0f2 update drachtio-srf 2023-11-09 10:02:46 -05:00
Hoan Luu Huu 6a6b4dd26f upgrade drachtio srf (#121)
* upgrade drachtio srf

* fix review comments
2023-10-24 08:42:53 -04:00
Hoan Luu Huu a3799576a2 add sentinelPassword option (#119) 2023-10-04 19:41:21 -04:00
Antony Jukesandajukes 63e72fbfce feat/MS Teams IP check improvement (#117)
* added ip in cidr utilities

* middleware add ms teams cidr check

* use cidr-matcher lib

* removed redundant require

* moved cidr-matcher require to top of file

* moved express require to top of file

---------

Co-authored-by: ajukes <ajukes@callable.io>
2023-09-18 10:35:04 -04:00
Dave Horton e5dce35bab more efficient checking of device calls (#118)
* more efficient checking of device calls

* when searching for matching gateways choose largest enclosing netmask
2023-09-15 10:53:22 -04:00
two56andMatt Preskett b6675de2bd Fix: Feature server REFER (#114)
* Update _onFeatureServerTransfer to fix REFER case

* Add INFO listener

---------

Co-authored-by: Matt Preskett <matt.preskett@netcall.com>
2023-08-20 08:47:01 -04:00
Dave Horton 66e73d8353 logging 2023-08-14 14:36:07 -04:00
Hoan Luu Huu a25aa08e17 update stats colector version (#111) 2023-08-07 21:22:47 -04:00
Hoan Luu Huu b1296c8e89 correct pare callRecording headers from sip info (#110) 2023-07-20 09:02:04 -04:00
Hoan Luu Huu 17df3a4ca8 Feat/siprec custom headers (#109)
* siprec custom headers

* siprec custom headers

* update siprec client util
2023-07-20 08:21:09 -04:00
Hoan Luu Huu f2e820915d Multi srs (#106)
* multi srs

* multi srs

* multi srs

* fix review comment
2023-07-04 16:41:46 +01:00
Dave Horton a1ec0fd5da 0.8.4 2023-06-28 09:33:01 +01:00
Hoan Luu Huu d7654de526 fix: update aws sdk v3 (#108)
* fix: update aws sdk v3

* fix: update aws sdk v3

* fix jslint issue

* fix jslint issue

* fix parse aws response

* fix parse aws response
2023-06-28 09:20:37 +01:00
Hoan Luu Huu 6e47e37cc2 fix client encrypted password (#105)
* fix client encrypted password

* fix client encrypted password

* fix failing testcase
2023-06-15 20:47:31 -04:00
Hoan Luu Huu c37d417a69 Feat: jambonz clients (#104)
* authenticate user

* authenticate user

* update db helper

* update db helper

* fix jslint

* use digest-utils
2023-06-15 07:36:33 -04:00
Hoan Luu Huu f7b3815ee3 feat: record all calls (#98)
* feat: record all calls

* wip: add cdr

* wip: add cdr

* fix

* fix jslint

* fix: record format ext
2023-06-09 14:57:45 -04:00
Hoan Luu Huu f385a33297 redis sentinel configuration (#103)
* redis sentinel configuration

* redis sentinel configuration

* update redis version

* update redis version
2023-06-07 10:00:44 -04:00
Hoan Luu Huu a01febdc22 update dbhelper and redis (#101) 2023-06-01 08:32:32 -04:00
Dave Horton e641c590b2 Fix/cidr error handling (#102)
* fix docker build

* catch error from CIDR which can happen with invalid sip gateway data
2023-05-31 09:11:53 -04:00
Dave Horton 8448e003f6 update siprec-client-utils 2023-05-29 09:23:37 -04:00
Dave Horton f6e071f31e standardize on passing .query args as array (#99) 2023-05-22 09:56:48 -04:00
Dave Horton 54d9044937 0.8.3 2023-05-11 09:25:24 -04:00
Dave Horton bcc089058f update deps 2023-05-08 13:12:50 -04:00
Snyk bot 1a09a13cfa fix: package.json & package-lock.json to reduce vulnerabilities (#97)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-XML2JS-5414874
2023-04-25 07:47:16 -04:00
Hoan Luu Huu b311d4af48 fix: ice issue (#95)
* fix: ice issue

* fix: review comment
2023-04-13 08:43:56 -04:00
Anton Voylenko 0fe8843729 README and env variables validation (#91)
* validate env at startup

* README updated with environment variables
2023-04-10 15:36:36 -04:00
Dave Horton 0b0e37020c push to docker 2023-04-10 09:43:00 -04:00
Antony Jukes 01cde07117 if else made authorization check unreachable (#90) 2023-04-06 09:34:37 -04:00
Hoan Luu Huu 31b4534878 fix: update stat collector version (#89) 2023-04-05 12:03:36 -04:00
Hoan Luu Huu 0c6365a8df fix: update stat collector version (#89) 2023-04-05 12:03:17 -04:00
Dave Hortonandsnyk-bot def2cb2be2 fix: Dockerfile to reduce vulnerabilities (#88)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3368756
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3368756
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-5291792
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-5291792

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-03-30 13:31:30 -04:00
Dave Horton baaee6f063 bump version 2023-03-28 14:16:07 -04:00
Hoan Luu HuuandQuan HL d2f32229a2 feat: add instance_id to gause calls metric (#87)
Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
2023-03-21 07:54:52 -04:00
Dave Horton 38f2d27246 refactor of speech-utils (#85) 2023-03-14 10:01:19 -04:00
Dave Horton d94977169e pass on displayName of From header if we get it (#82)
* pass on displayName of From header if we get it

* bump version
2023-03-03 18:55:56 -05:00
Dave Horton 0c9df878b8 update dockerfile 2023-02-23 08:23:17 -05:00
Dave Horton 93930fff18 fix for #79 (#80) 2023-02-22 08:51:35 -05:00
Dave Horton 2ce24659ca initial attempt (#77)
* initial attempt

* fix: special handling on uas dialog
2023-02-21 14:00:36 -05:00
EgleH 7c10e98a66 Upgrade node to node:18.14.0-alpine3.16 (#78) 2023-02-21 07:54:55 -05:00
Dave Horton f3fe46319d update siprec client with fix for stop recording 2023-02-17 12:10:14 -05:00
Dave Horton 43bfeb439a update siprec-client with fix for smart tap xml 2023-02-14 14:15:11 -05:00
Dave Horton 31f34fbffa update to latest siprec client 2023-02-13 16:50:07 -05:00
Dave Horton 52d0670b88 bump version 2023-02-13 09:15:05 -05:00
Snyk bot f42128765d fix: Dockerfile to reduce vulnerabilities (#75)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314623
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314624
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314624
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314641
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314643
2023-02-12 12:45:19 -05:00
Dave Horton eac1d7a9e9 K8s scale in (#73)
* changes for proper scale-in for K8S

* fix check for call count
2023-02-07 19:58:01 -05:00
Dave Horton fae1732ddf update to latest db-helpers 2023-02-07 13:31:48 -05:00
Dave Horton c858624815 Feature/tcp to fs (#72)
* add env var K8S_FEATURE_SERVER_TRANSPORT

* add Contact with private address when using tcp to FS on K8S

* further refinement
2023-01-26 13:33:24 -05:00
Dave Horton 2f137f2bfc bugfix: handle calls with no user in To header 2023-01-22 18:03:12 -05:00
Dave Horton ef0dcfc021 update to rtpengine-utils with fix for not discarding dtmf as dups 2023-01-12 13:09:15 -05:00
Dave Horton fb8baa34af always enable dtmf from rtpengine, since dtmfCapture on FS now uses this rather than freeswitch dtmf events (#70) 2023-01-12 08:45:39 -05:00
Dave Horton 7efce18a03 gh: run tests on PR 2023-01-09 10:13:25 -05:00
Dave Horton 84e5202607 bugfix: in EC2 deployment restrict udp localport for rtpengine 2022-12-30 08:54:33 -05:00
Dave Horton 8f856a30a6 update to drachtio-srf@4.5.21 with some perf fixes 2022-12-29 09:51:14 -05:00
Dave Horton d9881632c2 faster uuid 2022-12-28 10:06:31 -06:00
Dave Horton 6d2b1a94e9 bugfix: proper detection of reinvite from FS to release media 2022-12-27 13:40:56 -06:00
Dave Horton ded83ac39a bump version 2022-12-24 12:06:57 -06:00
Dave Horton 91a29d2b2c bump siprec-utils 2022-12-21 11:44:37 -05:00
Dave Horton 27686a80e8 update to latest rtpengine-utils and drachtio-server 2022-12-21 11:20:47 -05:00
Dave Horton a9e79b23bd bugfix: account returned when carrier default routing is used was wrong 2022-12-19 21:09:14 -05:00
Dave Horton faafaa8549 add env JAMBONES_RECORD_ALL_CALLS to enable global call recording 2022-12-02 13:53:15 -05:00
Dave Horton b24ecf84d3 minor logging 2022-12-01 12:39:51 -05:00
Dave Horton 3360ddb3e6 bugfix: proper removal of X-Refer-To-Leave-Untouched on outgoing REFER 2022-12-01 12:36:49 -05:00
Dave Horton b2d48c8954 Sdes testing (#67)
* sdes testing

* log rtpengine answer

* test with sdes off (again)
2022-12-01 11:41:30 -05:00
Dave Horton 1f1a4d2330 allow SDES in sip/tls scenarios 2022-12-01 10:13:52 -05:00
Dave Horton c34da3cfa8 support for gateways sending TLS with SRTP but not dtls (#66) 2022-12-01 09:49:25 -05:00
Dave Horton 5b88065925 bugfix: allow users to fully specify a Refer-To uri without us overiding it 2022-11-30 12:55:15 -05:00
Dave Horton 438924ca36 Bugfix/customer assigned name in uri (#65)
* dont force ice on srtp calls if not offered

* support customers creating their own dns name for our SBC IPs and putting that in the request-uri of INVITEs
2022-11-29 10:58:26 -05:00
Dave Horton c4d4d7bc0a if REFER from FS has a hostname in the Refer-To preserve it on the outbound REFER 2022-11-28 12:29:58 -05:00
Dave HortonandGuilherme Rauen ebcd0ce4a2 proper selection of account-level carrier with default routing (#63) (#64)
* update node image to the latest and most secure (#61)

Co-authored-by: Guilherme Rauen <g.rauen@cognigy.com>

* #62: dont modify refer-to host unless we have a dns name as outbound gateway

* added some tests and cleanup

* update drachtio-srf to latest

* gh action use node lts version

* revert and use node 18.x
2022-11-27 16:40:04 -05:00
Guilherme RauenandGuilherme Rauen cf239ca7fa update node image to the latest and most secure (#61)
Co-authored-by: Guilherme Rauen <g.rauen@cognigy.com>
2022-11-11 11:26:10 -05:00
Dave Horton b072524585 update deps, only subscribe for DTMF when client is possibly webrtc 2022-11-06 10:12:18 -05:00
Dave Horton 7fb9966d20 update to latest db-helpers 2022-11-05 10:42:14 -04:00
Dave Horton 42b3317206 update to db-helpers with caching fix 2022-11-01 20:40:53 -04:00
Dave Horton db66d98d0a write stat for db lookup 2022-11-01 18:57:31 -04:00
Dave Horton 26169517dc fix for dtmf handler when running multiple instances on same EC2 2022-11-01 15:40:37 -04:00
Dave Horton c78ec892b9 bugfix: running multiple instances under EC2 2022-10-31 14:45:20 -04:00
Dave Horton 6d237d43b3 update to db-helpers@0.7.0 with caching option 2022-10-31 11:44:53 -04:00
Dave Horton 44d298a4f3 Issue/58 (#59)
* feature: provide a range of health check ports, HTTP_PORT to HTTP_PORT_MAX, bind to first available

* feature: provide a range of aws sns ports, AWS_SNS_PORT to AWS_SNS_PORT_MAX, bind to first available

* AWS SNS port range 3010-3019
2022-10-30 13:08:07 -04:00
Dave Horton 030059a596 bugfix: transferring conference legs to different FS 2022-10-27 10:27:09 -04:00
Dave Horton 78b60525e2 Feature/support carrier domain in invite (#57)
* add support for incoming calls from carriers we register with, who then put their domain in the host part of incoming INVITE

* fix query to lookup registration carriers
2022-10-25 13:44:43 -04:00
Dave Horton dc9103cfa1 update deps 2022-10-23 15:27:18 -04:00
Dave Horton 4dd4247dc2 update package-lock.json 2022-10-23 12:20:28 -04:00
Dave Horton 055350903a update rtpengine-utils again 2022-10-23 11:25:06 -04:00
Dave Horton 7e83791ef0 update to latest rtpengine-utils 2022-10-22 22:17:02 -04:00
Dave Horton dbedd7419e block media going back to Five9 voicestream 2022-10-20 23:05:34 -04:00
Markus FrindtandMarkus Frindt c4d07b517e [snyk] fix vulnerability (#56)
Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2022-10-20 21:35:55 -04:00
Dave Horton 2693077871 include X-Account-Sid header when moving call between FS 2022-10-15 10:58:27 -04:00
Dave Horton ad0912d302 bugfix for release media (#55) 2022-10-14 12:48:22 -04:00
Dave Horton e715433534 bump version 2022-10-13 16:00:41 -04:00
Dave Horton f315b1b41f update time-series 2022-10-10 09:18:35 +01:00
Dave Horton bd09703732 bugfix: inject DMTF flag was inserted over and over 2022-10-08 18:36:40 +01:00
JoanandJoan Salvatella 363eb676a3 added quotes around in the sql query (#54)
Co-authored-by: Joan Salvatella <joan@bookline.io>
2022-10-08 11:33:27 +01:00
Dave Horton 7b085e5763 update to latest rtpengine-utils 2022-09-27 15:51:56 +01:00
Dave Horton 04554fd60d add support for connecting to rtpengine via ws 2022-09-27 09:38:59 +01:00
Dave Horton 9f1109c738 bump rtpengine-utils 2022-09-26 22:48:18 +01:00
Dave Horton d27847747d Feature/app call count tracking (#51)
* add call count tracking at the app level (optional)

* add histogram for rtt to rtpengine commands

* update time-series

* update rtpengine-utils
2022-09-22 23:41:00 +02:00
Dave Horton 88bdeffe4b only track service provider calls if JAMBONES_TRACK_SP_CALLS is set 2022-09-20 16:43:30 +02:00
Dave Horton 3245fae069 include application_sid in cdr 2022-09-20 13:56:09 +02:00
Dave Horton 213e84f59c Feature/sp limits (#49)
* add account and service provider call limits

* add custom headers when rejecting calls due to max calls limit
2022-09-20 13:12:49 +02:00
Dave Horton ca0c9c157c Feature/recent calls enhancement with sp (#47)
* write cdrs and alerts with service_provider_sid

* update to latest @jambonz/http-authenticator which sends sip headers

* add call_sid to cdr when call is connected

* write call counts by SP as well as account, bugfix to allow digit '0' in dtmf-relay
2022-09-16 11:49:42 +02:00
Paulo Tellesandp.souza 996519404e update node image (#46)
Co-authored-by: p.souza <p.souza@cognigy.com>
2022-09-07 13:19:47 +02:00
30 changed files with 8266 additions and 3992 deletions
+31 -28
View File
@@ -2,16 +2,10 @@ name: Docker
on:
push:
# Publish `master` as Docker `latest` image.
branches:
- main
# Publish `v1.2.3` tags as releases.
tags:
- v*
env:
IMAGE_NAME: sbc-inbound
- '*'
jobs:
push:
@@ -20,32 +14,41 @@ jobs:
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v2
- name: Checkout code
uses: actions/checkout@v3
- name: Build image
run: docker build . --file Dockerfile --tag $IMAGE_NAME
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push image
- name: prepare tag
id: prepare_tag
run: |
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
IMAGE_ID=jambonz/sbc-inbound
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
docker push $IMAGE_ID:$VERSION
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.prepare_tag.outputs.image_id }}:${{ steps.prepare_tag.outputs.version }}
build-args: |
GITHUB_REPOSITORY=$GITHUB_REPOSITORY
GITHUB_REF=$GITHUB_REF
+11 -10
View File
@@ -1,19 +1,20 @@
name: CI
on:
push:
workflow_dispatch:
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 14.x
- run: npm ci
node-version: 20.x
- run: npm install
- run: npm run jslint
- run: npm test
- 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 -1
View File
@@ -1,4 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run jslint
# npm audit fix || true
# npm run jslint
+1 -1
View File
@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 node:18.6.0-alpine as base
FROM --platform=linux/amd64 node:20.13.0-alpine3.18 as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
+33 -2
View File
@@ -1,10 +1,41 @@
# sbc-inbound ![Build Status](https://github.com/jambonz/sbc-inbound/workflows/CI/badge.svg)
This application provides a part of the SBC (Session Border Controller) functionality of jambonz. It handles incoming INVITE requests from carrier sip trunks or from sip devices and webrtc applications. SIP INVITEs from known carriers are allowed in, while INVITEs from sip devices are challenged to authenticate. SIP traffic that is allowed in is sent on to a jambonz application server in a private subnet.
This application provides a part of the SBC (Session Border Controller) functionality of jambonz platform. It handles incoming INVITE requests from carrier sip trunks or from sip devices and webrtc applications. SIP INVITEs from known carriers are allowed in, while INVITEs from sip devices are challenged to authenticate. SIP traffic that is allowed in is sent on to a jambonz application server in a private subnet.
## Configuration
Configuration is provided via the [npmjs config](https://www.npmjs.com/package/config) package. The following elements make up the configuration for the application:
Configuration is provided via environment variables:
| variable | meaning | required?|
|----------|----------|---------|
|DRACHTIO_HOST| ip address of drachtio server (typically '127.0.0.1')|yes|
|DRACHTIO_PORT| listening port of drachtio server for control connections (typically 9022)|yes|
|DRACHTIO_SECRET| shared secret|yes|
|HTTP_PORT| http listen port |no|
|JAMBONES_LOGLEVEL| log level for application, 'info' or 'debug'|no|
|JAMBONES_MYSQL_HOST| mysql host|yes|
|JAMBONES_MYSQL_PORT| mysql port |no|
|JAMBONES_MYSQL_USER| mysql username|yes|
|JAMBONES_MYSQL_PASSWORD| mysql password|yes|
|JAMBONES_MYSQL_DATABASE| mysql data|yes|
|JAMBONES_MYSQL_CONNECTION_LIMIT| mysql connection limit |no|
|DTMF_LISTEN_PORT| DTMF listening port |no|
|JAMBONES_NG_PROTOCOL| rtpengine NG protocol |no|
|RTPENGINE_PORT| rtpengine port |no|
|JAMBONES_CLUSTER_ID| cluster id |no|
|JAMBONES_NETWORK_CIDR| CIDR of private network that feature server is running in (e.g. '172.31.0.0/16')|yes|
|JAMBONES_REDIS_HOST| redis host|yes|
|JAMBONES_REDIS_PORT|redis port|no|
|JAMBONES_RTPENGINES| commas-separated list of ip:ng-port for rtpengines (e.g. '172.31.32.10:22222')|no|
|JAMBONES_TIME_SERIES_HOST| influxdb host |yes|
|JAMBONES_TIME_SERIES_PORT| influxdb port |no|
|JAMBONES_RECORD_ALL_CALLS| enable auto record calls, 'yes' or 'no' |no|
|K8S| service running as kubernetes service |no|
|K8S_RTPENGINE_SERVICE_NAME| rtpengine service name(required for K8S) |no|
|K8S_FEATURE_SERVER_SERVICE_NAME| feature server service name(required for K8S) |no|
|JWT_SECRET| secret for signing JWT token |yes|
|ENCRYPTION_SECRET| secret for credential encryption(JWT_SECRET is deprecated) |yes|
##### drachtio server location
```
{
+114 -29
View File
@@ -3,10 +3,17 @@ assert.ok(process.env.JAMBONES_MYSQL_HOST &&
process.env.JAMBONES_MYSQL_USER &&
process.env.JAMBONES_MYSQL_PASSWORD &&
process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars');
assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACHTIO_PORT env var');
if (process.env.JAMBONES_REDIS_SENTINELS) {
assert.ok(process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
'missing JAMBONES_REDIS_SENTINEL_MASTER_NAME env var, JAMBONES_REDIS_SENTINEL_PASSWORD env var is optional');
} else {
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
}
assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACHTIO_PORT env vars');
assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
assert.ok(process.env.JAMBONES_TIME_SERIES_HOST, 'missing JAMBONES_TIME_SERIES_HOST env var');
assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_NETWORK_CIDR env var');
const Srf = require('drachtio-srf');
const srf = new Srf('sbc-inbound');
const opts = Object.assign({
@@ -15,6 +22,8 @@ const opts = Object.assign({
const logger = require('pino')(opts);
const {
writeCallCount,
writeCallCountSP,
writeCallCountApp,
queryCdrs,
writeCdrs,
writeAlerts,
@@ -26,8 +35,9 @@ const {
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
});
const StatsCollector = require('@jambonz/stats-collector');
const CIDRMatcher = require('cidr-matcher');
const stats = new StatsCollector(logger);
const {equalsIgnoreOrder, createHealthCheckApp, systemHealth} = require('./lib/utils');
const {equalsIgnoreOrder, createHealthCheckApp, systemHealth, parseHostPorts} = require('./lib/utils');
const {LifeCycleEvents} = require('./lib/constants');
const setNameRtp = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-rtp`;
const rtpServers = [];
@@ -43,9 +53,13 @@ const {
lookupAppByTeamsTenant,
lookupAccountBySipRealm,
lookupAccountBySid,
lookupAccountCapacitiesBySid
lookupAccountCapacitiesBySid,
queryCallLimits,
lookupClientByAccountAndUsername,
lookupSystemInformation
} = require('@jambonz/db-helpers')({
host: process.env.JAMBONES_MYSQL_HOST,
port: process.env.JAMBONES_MYSQL_PORT || 3306,
user: process.env.JAMBONES_MYSQL_USER,
password: process.env.JAMBONES_MYSQL_PASSWORD,
database: process.env.JAMBONES_MYSQL_DATABASE,
@@ -53,30 +67,35 @@ const {
}, logger);
const {
client: redisClient,
addKey,
deleteKey,
retrieveKey,
createSet,
retrieveSet,
addToSet,
removeFromSet,
incrKey,
decrKey} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
decrKey} = require('@jambonz/realtimedb-helpers')({}, logger);
const ngProtocol = process.env.JAMBONES_NG_PROTOCOL || 'udp';
const ngPort = process.env.RTPENGINE_PORT || ('udp' === ngProtocol ? 22222 : 8080);
const {getRtpEngine, setRtpEngines} = require('@jambonz/rtpengine-utils')([], logger, {
emitter: stats,
//emitter: stats,
dtmfListenPort: process.env.DTMF_LISTEN_PORT || 22224,
protocol: 'udp'
protocol: ngProtocol
});
srf.locals = {...srf.locals,
stats,
writeCallCount,
writeCallCountSP,
writeCallCountApp,
queryCdrs,
writeCdrs,
writeAlerts,
AlertType,
activeCallIds: new Map(),
getRtpEngine,
privateNetworkCidr: process.env.PRIVATE_VOIP_NETWORK_CIDR || null,
dbHelpers: {
pool,
ping,
@@ -86,9 +105,15 @@ srf.locals = {...srf.locals,
lookupAppByTeamsTenant,
lookupAccountBySid,
lookupAccountBySipRealm,
lookupAccountCapacitiesBySid
lookupAccountCapacitiesBySid,
queryCallLimits,
lookupClientByAccountAndUsername,
lookupSystemInformation
},
realtimeDbHelpers: {
addKey,
deleteKey,
retrieveKey,
createSet,
incrKey,
decrKey,
@@ -96,16 +121,20 @@ srf.locals = {...srf.locals,
}
};
const {
wasOriginatedFromCarrier,
getApplicationForDidAndCarrier,
getOutboundGatewayForRefer
} = require('./lib/db-utils')(srf, logger);
srf.locals = {
...srf.locals,
getSPForAccount,
wasOriginatedFromCarrier,
getApplicationForDidAndCarrier,
getOutboundGatewayForRefer,
getFeatureServer: require('./lib/fs-tracking')(srf, logger)
getApplicationBySid
} = require('./lib/db-utils')(srf, logger);
srf.locals = {
...srf.locals,
getSPForAccount,
wasOriginatedFromCarrier,
getApplicationForDidAndCarrier,
getOutboundGatewayForRefer,
getFeatureServer: require('./lib/fs-tracking')(srf, logger),
getApplicationBySid
};
const activeCallIds = srf.locals.activeCallIds;
@@ -119,18 +148,34 @@ const {
const CallSession = require('./lib/call-session');
if (process.env.DRACHTIO_HOST && !process.env.K8S) {
const CIDRMatcher = require('cidr-matcher');
const cidrs = process.env.JAMBONES_NETWORK_CIDR
.split(',')
.map((s) => s.trim());
const matcher = new CIDRMatcher(cidrs);
srf.connect({host: process.env.DRACHTIO_HOST, port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET });
srf.on('connect', (err, hp) => {
srf.on('connect', (err, hp, version, localHostports) => {
if (err) return this.logger.error({err}, 'Error connecting to drachtio server');
logger.info(`connected to drachtio listening on ${hp}`);
let addedPrivateIp = false;
logger.info(`connected to drachtio ${version} listening on ${hp}, local hostports: ${localHostports}`);
const hostports = hp.split(',');
if (localHostports) {
const locals = localHostports.split(',');
for (const hp of locals) {
const arr = /^(.*)\/(.*):(\d+)$/.exec(hp);
if (arr && 'tcp' === arr[1] && matcher.contains(arr[2])) {
const hostport = `${arr[2]}:${arr[3]}`;
logger.info(`adding sbc private address to redis: ${hostport}`);
srf.locals.privateSipAddress = hostport;
srf.locals.addToRedis = () => addToSet(setName, hostport);
srf.locals.removeFromRedis = () => removeFromSet(setName, hostport);
srf.locals.addToRedis();
addedPrivateIp = true;
}
}
}
for (const hp of hostports) {
const arr = /^(.*)\/(.*):(\d+)$/.exec(hp);
if (arr && 'udp' === arr[1] && !matcher.contains(arr[2])) {
@@ -138,7 +183,7 @@ if (process.env.DRACHTIO_HOST && !process.env.K8S) {
srf.locals.sipAddress = arr[2];
if (!process.env.SBC_ACCOUNT_SID) addSbcAddress(arr[2]);
}
else if (arr && 'tcp' === arr[1] && matcher.contains(arr[2])) {
else if (!addedPrivateIp && arr && 'tcp' === arr[1] && matcher.contains(arr[2])) {
const hostport = `${arr[2]}:${arr[3]}`;
logger.info(`adding sbc private address to redis: ${hostport}`);
srf.locals.privateSipAddress = hostport;
@@ -147,6 +192,7 @@ if (process.env.DRACHTIO_HOST && !process.env.K8S) {
srf.locals.addToRedis();
}
}
srf.locals.sbcPublicIpAddress = parseHostPorts(logger, hostports, srf);
});
}
else {
@@ -154,6 +200,24 @@ else {
logger.info(`listening in outbound mode on port ${process.env.DRACHTIO_PORT}`);
});
srf.listen({port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET});
srf.on('connect', (err, hp, version, localHostports) => {
if (err) return this.logger.error({err}, 'Error connecting to drachtio server');
logger.info(`connected to drachtio ${version} listening on ${hp}, local hostports: ${localHostports}`);
if (process.env.K8S_FEATURE_SERVER_TRANSPORT === 'tcp') {
const matcher = new CIDRMatcher(['192.168.0.0/24', '172.16.0.0/16', '10.0.0.0/8']);
const hostports = localHostports ? localHostports.split(',') : hp.split(',');
for (const hp of hostports) {
const arr = /^(.*)\/(.*):(\d+)$/.exec(hp);
if (arr && matcher.contains(arr[2])) {
const hostport = `${arr[2]}:${arr[3]}`;
logger.info(`using sbc private address when sending to feature-server: ${hostport}`);
srf.locals.privateSipAddress = hostport;
}
}
}
srf.locals.sbcPublicIpAddress = parseHostPorts(logger, hp, srf);
});
}
if (process.env.NODE_ENV === 'test') {
srf.on('error', (err) => {
@@ -218,14 +282,25 @@ if (process.env.K8S || process.env.HTTP_PORT) {
});
}
if ('test' !== process.env.NODE_ENV) {
/* update call stats periodically */
setInterval(() => {
stats.gauge('sbc.sip.calls.count', activeCallIds.size, ['direction:inbound']);
/* update call stats periodically as well as definition of private network cidr */
setInterval(async() => {
stats.gauge('sbc.sip.calls.count', activeCallIds.size,
['direction:inbound', `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;
}
if (r.log_level) {
logger.level = r.log_level;
}
}
}, 20000);
}
const lookupRtpServiceEndpoints = (lookup, serviceName) => {
logger.debug(`dns lookup for ${serviceName}..`);
lookup(serviceName, {family: 4, all: true}, (err, addresses) => {
if (err) {
logger.error({err}, `Error looking up ${serviceName}`);
@@ -237,7 +312,7 @@ const lookupRtpServiceEndpoints = (lookup, serviceName) => {
rtpServers.length = 0;
Array.prototype.push.apply(rtpServers, addrs);
logger.info({rtpServers}, 'rtpserver endpoints have been updated');
setRtpEngines(rtpServers.map((a) => `${a}:${process.env.RTPENGINE_PORT || 22222}`));
setRtpEngines(rtpServers.map((a) => `${a}:${ngPort}`));
}
});
};
@@ -264,7 +339,7 @@ else {
logger.debug({newArray, rtpServers}, 'getActiveRtpServers');
if (!equalsIgnoreOrder(newArray, rtpServers)) {
logger.info({newArray}, 'resetting active rtpengines');
setRtpEngines(newArray.map((a) => `${a}:${process.env.RTPENGINE_PORT || 22222}`));
setRtpEngines(newArray.map((a) => `${a}:${ngPort}`));
rtpServers.length = 0;
Array.prototype.push.apply(rtpServers, newArray);
}
@@ -295,8 +370,18 @@ process.on('SIGUSR2', handle.bind(null, removeFromSet, setName));
process.on('SIGTERM', handle.bind(null, removeFromSet, setName));
function handle(removeFromSet, setName, signal) {
logger.info(`got signal ${signal}, removing ${srf.locals.privateSipAddress} from set ${setName}`);
removeFromSet(setName, srf.locals.privateSipAddress);
logger.info(`got signal ${signal}`);
if (srf.locals.privateSipAddress && setName) {
logger.info(`removing ${srf.locals.privateSipAddress} from set ${setName}`);
removeFromSet(setName, srf.locals.privateSipAddress);
}
if (process.env.K8S) {
lifecycleEmitter.operationalState = LifeCycleEvents.ScaleIn;
if (0 === activeCallIds.size) {
logger.info('exiting immediately since we have no calls in progress');
process.exit(0);
}
}
}
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": ["accept"]
}
+4 -4
View File
@@ -1,16 +1,16 @@
{
"default": {
"transport-protocol": "UDP/TLS/RTP/SAVPF",
"ICE": "force",
"ICE": "default",
"SDES": "off",
"flags": ["generate mid", "SDES-no", "media handover", "port latching"],
"flags": ["generate mid", "SDES-no", "port latching"],
"rtcp-mux": ["require"]
},
"teams": {
"transport-protocol": "RTP/SAVP",
"ICE": "force",
"ICE": "default",
"SDES": "off",
"flags": ["generate mid", "SDES-no", "media handover", "port latching"],
"flags": ["generate mid", "SDES-no", "port latching"],
"rtcp-mux": ["accept"]
}
}
+4 -1
View File
@@ -8,7 +8,7 @@ module.exports = (logger) => {
// listen for SNS lifecycle changes
let lifecycleEmitter = new Emitter();
lifecycleEmitter.dryUpCalls = false;
if (process.env.AWS_SNS_TOPIC_ARM) {
if (process.env.AWS_SNS_TOPIC_ARN) {
(async function() {
try {
@@ -57,6 +57,9 @@ module.exports = (logger) => {
}
})();
}
else if (process.env.K8S) {
lifecycleEmitter.scaleIn = () => process.exit(0);
}
return {lifecycleEmitter};
};
+76 -34
View File
@@ -1,26 +1,51 @@
const Emitter = require('events');
const bent = require('bent');
const assert = require('assert');
const PORT = process.env.AWS_SNS_PORT || 3001;
const PORT = process.env.AWS_SNS_PORT || 3010;
const {LifeCycleEvents} = require('./constants');
const express = require('express');
const app = express();
const getString = bent('string');
const AWS = require('aws-sdk');
const sns = new AWS.SNS({apiVersion: '2010-03-31'});
const autoscaling = new AWS.AutoScaling({apiVersion: '2011-01-01'});
const {
SNSClient,
SubscribeCommand,
UnsubscribeCommand } = require('@aws-sdk/client-sns');
const snsClient = new SNSClient({ region: process.env.AWS_REGION, apiVersion: '2010-03-31' });
const {
AutoScalingClient,
DescribeAutoScalingGroupsCommand,
CompleteLifecycleActionCommand } = require('@aws-sdk/client-auto-scaling');
const autoScalingClient = new AutoScalingClient({ region: process.env.AWS_REGION, apiVersion: '2011-01-01' });
const {Parser} = require('xml2js');
const parser = new Parser();
const {validatePayload} = require('verify-aws-sns-signature');
AWS.config.update({region: process.env.AWS_REGION});
class SnsNotifier extends Emitter {
constructor(logger) {
super();
this.logger = logger;
}
_doListen(logger, app, port, resolve) {
return app.listen(port, () => {
this.snsEndpoint = `http://${this.publicIp}:${port}`;
logger.info(`SNS lifecycle server listening on http://localhost:${port}`);
resolve(app);
});
}
_handleErrors(logger, app, resolve, reject, e) {
if (e.code === 'EADDRINUSE' &&
process.env.AWS_SNS_PORT_MAX &&
e.port < process.env.AWS_SNS_PORT_MAX) {
logger.info(`SNS lifecycle server failed to bind port on ${e.port}, will try next port`);
const server = this._doListen(logger, app, ++e.port, resolve);
server.on('error', this._handleErrors.bind(this, logger, app, resolve, reject));
return;
}
reject(e);
}
async _handlePost(req, res) {
try {
@@ -44,7 +69,18 @@ class SnsNotifier extends Emitter {
subscriptionRequestId: this.subscriptionRequestId
}, 'response from SNS SubscribeURL');
const data = await this.describeInstance();
this.lifecycleState = data.AutoScalingInstances[0].LifecycleState;
const group = data.AutoScalingGroups.find((group) =>
group.Instances && group.Instances.some((instance) => instance.InstanceId === this.instanceId)
);
if (!group) {
this.logger.error('Current instance not found in any Auto Scaling group', data);
} else {
const instance = group.Instances.find((instance) => instance.InstanceId === this.instanceId);
this.lifecycleState = instance.LifecycleState;
}
this.emit('SubscriptionConfirmation', {publicIp: this.publicIp});
break;
case 'Notification':
@@ -80,14 +116,12 @@ class SnsNotifier extends Emitter {
async init() {
try {
this.logger.info('SnsNotifier: retrieving instance data');
this.logger.debug('SnsNotifier: retrieving instance data');
this.instanceId = await getString('http://169.254.169.254/latest/meta-data/instance-id');
this.publicIp = await getString('http://169.254.169.254/latest/meta-data/public-ipv4');
this.snsEndpoint = `http://${this.publicIp}:${PORT}`;
this.logger.info({
instanceId: this.instanceId,
publicIp: this.publicIp,
snsEndpoint: this.snsEndpoint
publicIp: this.publicIp
}, 'retrieved AWS instance data');
// start listening
@@ -99,7 +133,10 @@ class SnsNotifier extends Emitter {
this.logger.error(err, 'burped error');
res.status(err.status || 500).json({msg: err.message});
});
app.listen(PORT);
return new Promise((resolve, reject) => {
const server = this._doListen(this.logger, app, PORT, resolve);
server.on('error', this._handleErrors.bind(this, this.logger, app, resolve, reject));
});
} catch (err) {
this.logger.error({err}, 'Error retrieving AWS instance metadata');
@@ -108,51 +145,56 @@ class SnsNotifier extends Emitter {
async subscribe() {
try {
const response = await sns.subscribe({
const params = {
Protocol: 'http',
TopicArn: process.env.AWS_SNS_TOPIC_ARM,
TopicArn: process.env.AWS_SNS_TOPIC_ARN,
Endpoint: this.snsEndpoint
}).promise();
this.logger.info({response}, `response to SNS subscribe to ${process.env.AWS_SNS_TOPIC_ARM}`);
};
const response = await snsClient.send(new SubscribeCommand(params));
this.logger.info({response}, `response to SNS subscribe to ${process.env.AWS_SNS_TOPIC_ARN}`);
} catch (err) {
this.logger.error({err}, `Error subscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARM}`);
this.logger.error({err}, `Error subscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARN}`);
}
}
async unsubscribe() {
if (!this.subscriptionArn) throw new Error('SnsNotifier#unsubscribe called without an active subscription');
try {
const response = await sns.unsubscribe({
const params = {
SubscriptionArn: this.subscriptionArn
}).promise();
this.logger.info({response}, `response to SNS unsubscribe to ${process.env.AWS_SNS_TOPIC_ARM}`);
};
const response = await snsClient.send(new UnsubscribeCommand(params));
this.logger.info({response}, `response to SNS unsubscribe to ${process.env.AWS_SNS_TOPIC_ARN}`);
} catch (err) {
this.logger.error({err}, `Error unsubscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARM}`);
this.logger.error({err}, `Error unsubscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARN}`);
}
}
completeScaleIn() {
assert(this.scaleInParams);
autoscaling.completeLifecycleAction(this.scaleInParams, (err, response) => {
if (err) return this.logger.error({err}, 'Error completing scale-in');
this.logger.info({response}, 'Successfully completed scale-in action');
});
autoScalingClient.send(new CompleteLifecycleActionCommand(this.scaleInParams))
.then((data) => {
return this.logger.info({data}, 'Successfully completed scale-in action');
})
.catch((err) => {
this.logger.error({err}, 'Error completing scale-in');
});
}
describeInstance() {
return new Promise((resolve, reject) => {
if (!this.instanceId) return reject('instance-id unknown');
autoscaling.describeAutoScalingInstances({
autoScalingClient.send(new DescribeAutoScalingGroupsCommand({
InstanceIds: [this.instanceId]
}, (err, data) => {
if (err) {
}))
.then((data) => {
this.logger.info({data}, 'SnsNotifier: describeInstance');
return resolve(data);
})
.catch((err) => {
this.logger.error({err}, 'Error describing instances');
reject(err);
} else {
this.logger.info({data}, 'SnsNotifier: describeInstance');
resolve(data);
}
});
});
});
}
@@ -166,7 +208,7 @@ module.exports = async function(logger) {
process.on('SIGHUP', async() => {
try {
const data = await notifier.describeInstance();
const state = data.AutoScalingInstances[0].LifecycleState;
const state = data.AutoScalingGroups[0].Instances[0].LifecycleState;
if (state !== notifier.lifecycleState) {
notifier.lifecycleState = state;
switch (state) {
+472 -131
View File
@@ -1,11 +1,24 @@
const Emitter = require('events');
const SrsClient = require('@jambonz/siprec-client-utils');
const {makeRtpEngineOpts, SdpWantsSrtp, makeCallCountKey} = require('./utils');
const {
makeRtpEngineOpts,
SdpWantsSrtp,
SdpWantsSDES,
nudgeCallCounts,
roundTripTime,
parseConnectionIp,
isPrivateVoipNetwork,
makeFullMediaReleaseKey,
makePartnerFullMediaReleaseKey
} = require('./utils');
const { MediaPath } = require('./constants.json');
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
const {parseUri, stringifyUri, SipError} = require('drachtio-srf');
const debug = require('debug')('jambonz:sbc-inbound');
const MS_TEAMS_USER_AGENT = 'Microsoft.PSTNHub.SIPProxy';
const MS_TEAMS_SIP_ENDPOINT = 'sip.pstnhub.microsoft.com';
const IMMUTABLE_HEADERS = ['via', 'from', 'to', 'call-id', 'cseq', 'max-forwards', 'content-length'];
/**
* this is to make sure the outgoing From has the number in the incoming From
@@ -14,8 +27,10 @@ const MS_TEAMS_SIP_ENDPOINT = 'sip.pstnhub.microsoft.com';
const createBLegFromHeader = (req) => {
const from = req.getParsedHeader('From');
const uri = parseUri(from.uri);
if (uri && uri.user) return `<sip:${uri.user}@localhost>`;
return '<sip:anonymous@localhost>';
const name = from.name;
const displayName = name ? `${name} ` : '';
if (uri && uri.user) return `${displayName}<sip:${uri.user}@localhost>`;
else return `${displayName}<sip:anonymous@localhost>`;
};
const createSiprecBody = (headers, sdp, type, content) => {
@@ -49,8 +64,16 @@ class CallSession extends Emitter {
this.activeCallIds = this.srf.locals.activeCallIds;
this.decrKey = req.srf.locals.realtimeDbHelpers.decrKey;
this.callCountKey = makeCallCountKey(req.locals.account_sid);
this._mediaReleased = false;
this.addKey = req.srf.locals.realtimeDbHelpers.addKey;
this.retrieveKey = req.srf.locals.realtimeDbHelpers.retrieveKey;
this._mediaPath = MediaPath.FullMedia;
this.application_sid = req.locals.application_sid;
this.account_sid = req.locals.account_sid;
this.service_provider_sid = req.locals.service_provider_sid;
this.srsClients = [];
this.recordingNoAnswerTimeout = (process.env.JAMBONES_RECORDING_NO_ANSWER_TIMEOUT || 2) * 1000;
}
get isFromMSTeams() {
@@ -62,17 +85,31 @@ class CallSession extends Emitter {
}
get isMediaReleased() {
return this._mediaReleased;
return this._mediaPath !== MediaPath.FullMedia;
}
get callerIsUsingSrtp() {
const tp = this.rtpEngineOpts?.uas?.mediaOpts['transport-protocol'];
return tp && -1 !== tp.indexOf('SAVP');
get isFive9VoiceStream() {
return this.req.has('X-Five9-StreamingPairId');
}
subscribeForDTMF(dlg) {
if (!this._subscribedForDTMF) {
this._subscribedForDTMF = true;
this.subscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uas.tag,
this._onDTMF.bind(this, dlg));
}
}
unsubscribeForDTMF() {
if (this._subscribedForDTMF) {
this._subscribedForDTMF = false;
this.unsubscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uas.tag);
}
}
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!');
@@ -85,6 +122,7 @@ class CallSession extends Emitter {
offer,
answer,
del,
query,
blockMedia,
unblockMedia,
blockDTMF,
@@ -99,6 +137,7 @@ class CallSession extends Emitter {
this.offer = offer;
this.answer = answer;
this.del = del;
this.query = query;
this.blockMedia = blockMedia;
this.unblockMedia = unblockMedia;
this.blockDTMF = blockDTMF;
@@ -119,11 +158,18 @@ class CallSession extends Emitter {
}
this.logger.debug(`using feature server ${featureServer}`);
this.rtpEngineOpts = makeRtpEngineOpts(this.req, SdpWantsSrtp(sdp), false, this.isFromMSTeams);
this.rtpEngineResource = {destroy: this.del.bind(null, this.rtpEngineOpts.common)};
const wantsSrtp = this.req.locals.possibleWebRtcClient = SdpWantsSrtp(sdp);
const wantsSDES = SdpWantsSDES(sdp);
this.rtpEngineOpts = makeRtpEngineOpts(this.req, wantsSrtp, false, this.isFromMSTeams || wantsSDES);
this.rtpEngineResource = {
destroy: this.del.bind(null, this.rtpEngineOpts.common),
query: this.query.bind(null, this.rtpEngineOpts.common),
};
const obj = parseUri(this.req.uri);
let proxy, host, uri;
const scheme = obj.scheme;
// replace host part of uri if its an ipv4 address, leave it otherwise
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(obj.host)) {
debug(`replacing host: was ${obj.host} is ${featureServer}`);
@@ -134,25 +180,34 @@ class CallSession extends Emitter {
host = obj.host;
proxy = `sip:${featureServer}`;
}
if (obj.user) uri = `${obj.scheme}:${obj.user}@${host}`;
else uri = `${obj.scheme}:${host}`;
if (obj.user) uri = `sip:${obj.user}@${host}`;
else uri = `sip:${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: ['public', 'private'],
sdp
direction: [isPrivateVoipNetwork(this.req.source_address) ? 'private' : 'public', 'private'],
sdp: remoteSdp
};
const startAt = process.hrtime();
const response = await this.offer(opts);
this.logger.debug({opts, response}, 'response from rtpengine to offer');
this.rtpengineIp = opts.sdp ? parseConnectionIp(opts.sdp) : 'undefined';
const rtt = roundTripTime(startAt);
this.stats.histogram('app.rtpengine.response_time', rtt, [
'direction:inbound', 'command:offer', `rtpengine:${this.rtpengineIp}`]);
this.logger.debug({opts, response, rtt, rtpengine: this.rtpengineIp}, 'response from rtpengine to offer');
if ('ok' !== response.result) {
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'),
@@ -162,30 +217,52 @@ 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;
}
const responseHeaders = {};
if (this.req.locals.carrier) {
Object.assign(headers, {
'X-Originating-Carrier': this.req.locals.carrier,
'X-Voip-Carrier-Sid': this.req.locals.voip_carrier_sid
});
}
if (this.req.locals.msTeamsTenantFqdn) {
// set Contact header based on scenario, and transport protocol
let responseHeaders = {};
if (isPrivateVoipNetwork(this.req.source_address)) {
this.contactHeader = `<${scheme}:${this.privateSipAddress};transport=${this.req.protocol}>`;
responseHeaders = {
...responseHeaders,
'Contact': this.contactHeader
};
} else if (this.req.locals.msTeamsTenantFqdn) {
Object.assign(headers, {'X-MS-Teams-Tenant-FQDN': this.req.locals.msTeamsTenantFqdn});
// for Microsoft Teams the Contact header must include the tenant FQDN
Object.assign(responseHeaders, {
this.contactHeader = `sip:${this.req.locals.msTeamsTenantFqdn}`;
responseHeaders = {
...responseHeaders,
Allow: 'INVITE, ACK, OPTIONS, CANCEL, BYE, NOTIFY, UPDATE, PRACK',
Contact: `sip:${this.req.locals.msTeamsTenantFqdn}`
});
Contact: this.contactHeader
};
}
else {
const hostport = this.srf.locals.sbcPublicIpAddress[this.req.protocol];
this.contactHeader = `<${scheme}:${hostport};transport=${this.req.protocol}>`;
responseHeaders = {
...responseHeaders,
'Contact': this.contactHeader
};
}
if (this.req.locals.application_sid) {
Object.assign(headers, {'X-Application-Sid': this.req.locals.application_sid});
}
else if (this.req.authorization) {
if (this.req.authorization) {
if (this.req.authorization.grant && this.req.authorization.grant.application_sid) {
Object.assign(headers, {'X-Application-Sid': this.req.authorization.grant.application_sid});
}
@@ -209,10 +286,14 @@ class CallSession extends Emitter {
'-Max-Forwards',
'-Record-Route',
'-Session-Expires',
'-X-Subspace-Forwarded-For'
'-X-Application-Sid',
'-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 = {
@@ -222,13 +303,33 @@ class CallSession extends Emitter {
'to-tag': this.rtpEngineOpts.uac.tag,
sdp
};
const response = await this.answer(opts);
const startAt = process.hrtime();
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}`]);
if ('ok' !== response.result) {
this.logger.error(`rtpengine answer failed with ${JSON.stringify(response)}`);
throw new Error('rtpengine failed: answer');
}
/* special case: Five9 Voicestream calls do not advertise a:sendonly, though they should */
if (this.isFive9VoiceStream) {
const opts = {
...this.rtpEngineOpts.common,
'from-tag':this.rtpEngineOpts.uac.tag
};
this.logger.info('Voicestream call from Five9, blocking audio in the reverse direction');
const response = await Promise.all([this.blockMedia(opts), this.blockDTMF(opts)]);
this.logger.debug({response}, 'response to blockMedia/blockDTMF');
}
return response.sdp;
}
},
});
// successfully connected
@@ -239,7 +340,6 @@ class CallSession extends Emitter {
} catch (err) {
this.rtpEngineResource.destroy().catch((err) => this.logger.info({err}, 'Error destroying rtpe after failure'));
this.activeCallIds.delete(this.req.get('Call-ID'));
this.stats.gauge('sbc.sip.calls.count', this.activeCallIds.size);
if (err instanceof SipError) {
const tags = ['accepted:no', `sipStatus:${err.status}`, `originator:${this.req.locals.originator}`];
this.stats.increment('sbc.terminations', tags);
@@ -247,7 +347,16 @@ class CallSession extends Emitter {
this.emit('failed');
}
else if (err.message !== 'call canceled') {
const {writeCallCount, writeCallCountSP, writeCallCountApp} = this.req.srf.locals;
this.logger.error(err, 'unexpected error routing inbound call');
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.srf.endSession(this.req);
}
@@ -256,8 +365,7 @@ class CallSession extends Emitter {
_setDlgHandlers(dlg) {
const {callId} = dlg.sip;
this.activeCallIds.set(callId, this);
this.subscribeDTMF(this.logger, callId, this.rtpEngineOpts.uas.tag,
this._onDTMF.bind(this));
this.subscribeForDTMF(this.uac);
dlg.on('destroy', () => {
debug('call ended with normal termination');
this.logger.info('call ended with normal termination');
@@ -265,10 +373,7 @@ class CallSession extends Emitter {
this.activeCallIds.delete(callId);
if (dlg.other && dlg.other.connected) dlg.other.destroy().catch((e) => {});
if (this.srsClient) {
this.srsClient.stop();
this.srsClient = null;
}
this._stopRecording();
this.srf.endSession(this.req);
});
@@ -277,9 +382,34 @@ class CallSession extends Emitter {
dlg.on('modify', this._onReinvite.bind(this, dlg));
}
_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());
this.srsClients = [];
}
}
_setHandlers({uas, uac}) {
this.emit('connected');
const callStart = Date.now();
const call_sid = uac.res?.get('X-Call-Sid');
const application_sid = this.application_sid || uac.res?.get('X-Application-Sid');
const tags = ['accepted:yes', 'sipStatus:200', `originator:${this.req.locals.originator}`];
this.stats.increment('sbc.terminations', tags);
this.activeCallIds.set(this.req.get('Call-ID'), this);
@@ -288,30 +418,61 @@ class CallSession extends Emitter {
...this.req.locals.cdr,
answered: true,
answered_at: callStart,
...(call_sid && {call_sid}),
...(application_sid && {application_sid}),
trace_id: uac.res?.get('X-Trace-ID') || '00000000000000000000000000000000'
};
}
/* 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('Call-ID'));
const sdp = this.req.body;
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().catch((err) => {});
/* DH: need a better understanding of why query before delete is a good idea
this.rtpEngineResource.query()
.then((results) => {
this.logger.info({results}, 'rtpengine query results');
return this.rtpEngineResource.destroy();
})
.catch((err) => {});
*/
this.activeCallIds.delete(this.req.get('Call-ID'));
try {
await other.destroy();
const headers = {};
Object.keys(bye.headers).forEach((h) => {
if (!IMMUTABLE_HEADERS.includes(h)) headers[h] = bye.headers[h];
});
await other.destroy({headers});
} catch (err) {}
this.unsubscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uas.tag);
if (process.env.JAMBONES_HOSTING || process.env.JAMBONES_TRACK_ACCOUNT_CALLS) {
const {account_sid} = this.req.locals;
this.decrKey(this.callCountKey)
.then((count) => {
this.logger.info(
{key: this.callCountKey},
`after hangup there are ${count} active calls for this account`);
return this.req.srf.locals.writeCallCount({account_sid, calls_in_progress: count});
})
.catch((err) => this.logger.error({err}, 'Error decrementing call count'));
this.unsubscribeForDTMF();
const trackingOn = process.env.JAMBONES_TRACK_ACCOUNT_CALLS ||
process.env.JAMBONES_TRACK_SP_CALLS ||
process.env.JAMBONES_TRACK_APP_CALLS;
if (process.env.JAMBONES_HOSTING || trackingOn) {
const {writeCallCount, writeCallCountSP, writeCallCountApp} = this.req.srf.locals;
if (!this.req.locals.callCountNudged) {
this.req.locals.callCountNudged = true;
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,
callId: this.req.locals.callId
}, this.decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
.catch((err) => this.logger.error(err, 'Error decrementing call counts'));
}
}
/* write cdr for connected call */
@@ -320,13 +481,31 @@ class CallSession extends Emitter {
const trunk = ['trunk', 'teams'].includes(this.req.locals.originator) ?
this.req.locals.carrier :
this.req.locals.originator;
const application = await this.srf.locals.getApplicationBySid(application_sid);
const isRecording = this.req.locals.account.record_all_calls || (application && application.record_all_calls);
const day = new Date();
let recording_url = `/Accounts/${this.account_sid}/RecentCalls/${call_sid}/record`;
recording_url += `/${day.getFullYear()}/${(day.getMonth() + 1).toString().padStart(2, '0')}`;
recording_url += `/${day.getDate().toString().padStart(2, '0')}/${this.req.locals.account.record_format}`;
const cdr = {...this.req.locals.cdr,
terminated_at: now,
termination_reason: dlg.type === 'uas' ? 'caller hungup' : 'called party hungup',
sip_status: 200,
duration: Math.floor((now - callStart) / 1000),
trunk,
...(isRecording && {recording_url})
};
this.logger.info({cdr}, 'going to write a cdr now..');
this.writeCdrs({...this.req.locals.cdr,
terminated_at: now,
termination_reason: dlg.type === 'uas' ? 'caller hungup' : 'called party hungup',
sip_status: 200,
duration: Math.floor((now - callStart) / 1000),
trunk
}).catch((err) => this.logger.error({err}, 'Error writing cdr for completed call'));
trunk,
...(isRecording && {recording_url})
})
.then(() => this.logger.debug('successfully wrote cdr'))
.catch((err) => this.logger.error({err}, 'Error writing cdr for completed call'));
}
/* de-link the 2 Dialogs for GC */
dlg.removeAllListeners();
@@ -334,18 +513,14 @@ class CallSession extends Emitter {
dlg.other = null;
other.other = null;
if (this.srsClient) {
this.srsClient.stop();
this.srsClient = null;
}
this._stopRecording();
this.logger.info(`call ended with normal termination, there are ${this.activeCallIds.size} active`);
this.srf.endSession(this.req);
});
});
this.subscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uas.tag,
this._onDTMF.bind(this, uac));
this.subscribeForDTMF(uac);
uas.on('modify', this._onReinvite.bind(this, uas));
uac.on('modify', this._onReinvite.bind(this, uac));
@@ -358,6 +533,12 @@ class CallSession extends Emitter {
// default forwarding of other request types
forwardInDialogRequests(uas, ['notify', 'options', 'message']);
// we need special handling for invite with null sdp followed by 3pcc re-invite
if (uas.local.sdp.includes('a=recvonly') || uas.local.sdp.includes('a=inactive')) {
this.logger.info('incoming call is recvonly or inactive, waiting for re-invite');
this._recvonly = true;
}
}
async _onDTMF(dlg, payload) {
@@ -439,7 +620,7 @@ Duration=${payload.duration} `
});
}
this.unsubscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uas.tag);
this.unsubscribeForDTMF();
const uas = await this.srf.createUAS(req, res, {
localSdp: response.sdp,
@@ -460,23 +641,100 @@ Duration=${payload.duration} `
}
async _onReinvite(dlg, req, res) {
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 reason = req.get('X-Reason');
const isReleasingMedia = reason && dlg.type === 'uac' && ['release-media', 'anchor-media'].includes(reason);
const isFullMediaRelease = reason === 'release-media-entirely' && process.env.JAMBONES_ENABLE_FULL_MEDIA_RELEASE;
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;
const direction = dlg.type === 'uas' ? ['public', 'private'] : ['private', 'public'];
try {
/* check for re-invite with no SDP -- seen that from BT when they provide UUI info */
if (!req.body) {
this.logger.info('got a reINVITE with no SDP; just respond with our current offer');
res.send(200, {body: dlg.local.sdp});
if (dlg.type === 'uas' && this._recvonly) {
/* seen this from Broadworks - initial INVITE has no SDP, then reINVITE with SDP */
this._recvonly = false; //one-time only
const myMungedSdp = dlg.local.sdp.replace('a=recvonly', 'a=sendrecv').replace('a=inactive', 'a=sendrecv');
this.logger.info({myMungedSdp}, '_onReinvite (3gpp): got a reINVITE with no SDP while in recvonly mode');
res.send(200,
{
body: myMungedSdp
},
(err, req) => {},
async(ack) => {
const remoteOffer = ack.body;
this.logger.info({remoteOffer}, '_onReinvite (3gpp): got ACK for reINVITE with SDP');
let opts = {
...this.rtpEngineOpts.common,
...offerMedia,
'from-tag': fromTag,
'to-tag': toTag,
direction,
sdp: remoteOffer,
};
let response = await this.offer(opts);
if ('ok' !== response.result) {
res.send(488);
throw new Error(`_onReinvite (3gpp): rtpengine failed: offer: ${JSON.stringify(response)}`);
}
this.logger.info({response}, '_onReinvite (3gpp): response from rtpengine for offer');
const fsSdp = await dlg.other.modify(response.sdp);
opts = {
...this.rtpEngineOpts.common,
...answerMedia,
'from-tag': fromTag,
'to-tag': toTag,
sdp: fsSdp
};
response = await this.answer(opts);
if ('ok' !== response.result) {
res.send(488);
throw new Error(`_onReinvite(3gpp): rtpengine failed answer: ${JSON.stringify(response)}`);
}
}
);
}
else {
this.logger.info('got a reINVITE with no SDP; just respond with our current offer');
res.send(200, {
headers: {
'Contact': this.contactHeader
},
body: dlg.local.sdp
});
}
return;
}
if (isFullMediaRelease) {
const b_sdp = await this.retrieveKey(makePartnerFullMediaReleaseKey(this.req.get('Call-ID')));
this.logger.info({b_sdp}, 'reinvite ourselves out of the media path with this reinvite offer');
const answerSdp = await dlg.other.modify(b_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;
}
const offeredSdp = Array.isArray(req.payload) && req.payload.length > 1 ?
req.payload.find((p) => p.type === 'application/sdp').content :
req.body;
const reason = req.get('X-Reason');
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;
const direction = dlg.type === 'uas' ? ['public', 'private'] : ['private', 'public'];
if (isReleasingMedia) {
if (!offerMedia.flags.includes('asymmetric')) offerMedia.flags.push('asymmetric');
offerMedia.flags = offerMedia.flags.filter((f) => f !== 'media handover');
}
let opts = {
...this.rtpEngineOpts.common,
...offerMedia,
@@ -485,7 +743,8 @@ Duration=${payload.duration} `
direction,
sdp: offeredSdp,
};
if (reason && opts.flags && !opts.flags.includes('reset')) opts.flags.push('reset');
// Dont reset ICE - causes audiocodes webrtrc to fail with "missing ice-ufrag and ice-pwd in re-invite"
// if (reason && opts.flags && !opts.flags.includes('reset')) opts.flags.push('reset');
let response = await this.offer(opts);
if ('ok' !== response.result) {
@@ -495,12 +754,16 @@ Duration=${payload.duration} `
/* if this is a re-invite from the FS to change media anchoring, avoid sending the reinvite out */
let sdp;
if (reason && dlg.type === 'uac' && ['release-media', 'anchor-media'].includes(reason) &&
!this.callerIsUsingSrtp) {
this.logger.info({response}, `got a reinvite from FS to ${reason}`);
//HL 2024-11-13: previously forwarded re-invites to webrtc clients but further testing has shown to be unnecessary
//if (isReleasingMedia && !this.callerIsUsingSrtp) {
//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) {
sdp = dlg.other.remote.sdp;
answerMedia.flags = ['asymmetric', 'port latching'];
this._mediaReleased = 'release-media' === reason;
if (!answerMedia.flags.includes('asymmetric')) answerMedia.flags.push('asymmetric');
answerMedia.flags = answerMedia.flags.filter((f) => f !== 'media handover');
this._mediaPath = 'release-media' === reason ? MediaPath.PartialMedia : MediaPath.FullMedia;
this.logger.debug(`media path is now ${this._mediaPath}`);
}
else {
sdp = await dlg.other.modify(response.sdp);
@@ -517,8 +780,14 @@ Duration=${payload.duration} `
res.send(488);
throw new Error(`_onReinvite: rtpengine failed: ${JSON.stringify(response)}`);
}
res.send(200, {body: response.sdp});
res.send(200, {
headers: {
'Contact': this.contactHeader
},
body: response.sdp
});
} catch (err) {
res.send(err.status || 500);
this.logger.error(err, 'Error handling reinvite');
}
}
@@ -549,6 +818,7 @@ 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');
@@ -561,7 +831,7 @@ Duration=${payload.duration} `
const callSid = req.get('X-Call-Sid');
const accountSid = req.get('X-Account-Sid');
const applicationSid = req.get('X-Application-Sid');
if (this.srsClient) {
if (this.srsClients.length) {
res.send(400);
this.logger.info('discarding duplicate startCallRecording request for a call');
return;
@@ -571,13 +841,14 @@ Duration=${payload.duration} `
res.send(400);
return;
}
this.srsClient = new SrsClient(this.logger, {
const arr = srsUrl.split(',');
this.srsClients = arr.map((url) => new SrsClient(this.logger, {
srf: dlg.srf,
direction: 'inbound',
originalInvite: this.req,
callingNumber: this.req.callingNumber,
calledNumber: this.req.calledNumber,
srsUrl,
srsUrl: url,
srsRecordingId,
callSid,
accountSid,
@@ -592,48 +863,108 @@ Duration=${payload.duration} `
del: this.del,
blockMedia: this.blockMedia,
unblockMedia: this.unblockMedia,
unsubscribe: this.unsubscribe
});
unsubscribe: this.unsubscribe,
headers,
isSipRecCall: Array.isArray(this.req.payload) && this.req.payload.length > 1
}));
try {
succeeded = await this.srsClient.start();
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.srsClient) {
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 this.srsClient.stop();
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.srsClient = null;
this.srsClients = [];
}
else if (reason === 'pauseCallRecording') {
if (!this.srsClient || this.srsClient.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 this.srsClient.pause();
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.srsClient || !this.srsClient.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 this.srsClient.resume();
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;
}
}
res.send(succeeded ? 200 : 503);
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.uac.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(200);
}
}
else if (dlg.type === 'uas' && ['application/dtmf-relay', 'application/dtmf'].includes(contentType)) {
const arr = /Signal=\s*([1-9#*])/.exec(req.body);
const arr = /Signal=\s*([0-9#*])/.exec(req.body);
if (!arr) {
this.logger.info({body: req.body}, '_onInfo: invalid INFO dtmf request');
throw new Error(`_onInfo: no dtmf in body for ${contentType}`);
@@ -667,10 +998,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 = {};
@@ -680,8 +1010,8 @@ Duration=${payload.duration} `
res.send(response.status, {headers: responseHeaders, body: response.body});
}
} catch (err) {
if (this.srsClient) {
this.srsClient = null;
if (this.srsClients.length) {
this.srsClients = [];
}
res.send(500);
this.logger.info({err}, `Error handing INFO request on ${dlg.type} leg`);
@@ -697,41 +1027,48 @@ Duration=${payload.duration} `
if (!arr) {
/* call transfer requested */
const {gateway} = this.req.locals;
const referredBy = req.getParsedHeader('Referred-By');
if (!referredBy) return res.send(400);
const u = parseUri(referredBy.uri);
let selectedGateway = false;
let e164 = false;
if (gateway) {
/* host of Refer-to to an outbound gateway */
const gw = await this.srf.locals.getOutboundGatewayForRefer(gateway.voip_carrier_sid);
if (gw) {
selectedGateway = true;
e164 = gw.e164_leading_plus;
uri.host = gw.ipv4;
uri.port = gw.port;
if (!req.has('Referred-By')) return res.send(400);
const leaveReferToAlone = req.has('X-Refer-To-Leave-Untouched');
if (leaveReferToAlone) {
this.logger.debug({referTo}, 'passing Refer-To header through untouched');
}
else {
const isDotDecimal = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(uri.host);
let selectedGateway = false;
let e164 = false;
if (gateway && isDotDecimal) {
/* host of Refer-to to an outbound gateway */
const gw = await this.srf.locals.getOutboundGatewayForRefer(gateway.voip_carrier_sid);
if (gw) {
selectedGateway = true;
e164 = gw.e164_leading_plus;
uri.host = gw.ipv4;
uri.port = gw.port;
}
}
if (!selectedGateway && isDotDecimal) {
uri.host = this.req.source_address;
uri.port = this.req.source_port;
}
if (e164 && !uri.user.startsWith('+')) {
uri.user = `+${uri.user}`;
}
}
if (!selectedGateway) {
uri.host = this.req.source_address;
uri.port = this.req.source_port;
}
if (e164 && !uri.user.startsWith('+')) {
uri.user = `+${uri.user}`;
}
// 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,
'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 response = await this.uas.request({
method: 'REFER',
headers: {
'Refer-To': stringifyUri(uri),
'Referred-By': stringifyUri(u),
'Refer-To': `<${stringifyUri(uri)}>`,
'Referred-By': referredby,
'Contact': this.contactHeader,
...customHeaders
}
});
@@ -740,15 +1077,16 @@ Duration=${payload.duration} `
res.send(202);
// invite to new fs
const headers = {};
if (req.has('X-Retain-Call-Sid')) {
Object.assign(headers, {'X-Retain-Call-Sid': req.get('X-Retain-Call-Sid')});
}
const headers = {
...(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.uac = uac;
uac.other = this.uas;
this.uas.other = uac;
uac.on('modify', this._onFeatureServerReinvite.bind(this, uac));
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', () => {
this.logger.info('call ended with normal termination');
@@ -757,19 +1095,22 @@ Duration=${payload.duration} `
uac.other.destroy();
this.srf.endSession(this.req);
});
// now we can destroy the old dialog
dlg.destroy().catch(() => {});
// modify rtpengine to stream to new feature server
const opts = Object.assign({sdp: uac.remote.sdp, 'to-tag': res.getParsedHeader('To').params.tag},
this.rtpEngineOpts.answer);
const opts = {
...this.rtpEngineOpts.common,
'from-tag': this.rtpEngineOpts.uas.tag,
'to-tag': this.rtpEngineOpts.uac.tag,
sdp: uac.remote.sdp,
flags: ['port latching']
};
const response = await this.answer(opts);
if ('ok' !== response.result) {
res.send(488);
throw new Error(`_onFeatureServerReinvite: rtpengine failed: ${JSON.stringify(response)}`);
throw new Error(`_onFeatureServerTransfer: rtpengine answer failed: ${JSON.stringify(response)}`);
}
dlg.destroy().catch(() => {});
this.logger.info('successfully moved call to new feature server');
} catch (err) {
res.send(488);
this.logger.error(err, 'Error handling refer from feature server');
}
}
@@ -827,7 +1168,7 @@ Duration=${payload.duration} `
// successfully connected
this.logger.info('successfully connected new call leg for REFER');
this.unsubscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uas.tag);
this.unsubscribeForDTMF();
this.referInvite = null;
sendNotify(this.uas, '200 OK');
this.uas.destroy();
+5
View File
@@ -3,5 +3,10 @@
"ScaleIn": "scale-in",
"StandbyEnter": "standby-enter",
"StandbyExit": "standby-exit"
},
"MediaPath": {
"NoMedia": "no-media",
"PartialMedia": "partial-media",
"FullMedia": "full-media"
}
}
+373 -79
View File
@@ -3,41 +3,41 @@ const CIDRMatcher = require('cidr-matcher');
const {parseUri} = require('drachtio-srf');
const {normalizeDID} = require('./utils');
const sqlSelectSPForAccount = 'SELECT service_provider_sid FROM accounts WHERE account_sid = ?';
const sqlSelectAllCarriersForAccountByRealm =
`SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.account_sid,
vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask
vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask, sg.pad_crypto
FROM sip_gateways sg, voip_carriers vc, accounts acc
WHERE acc.sip_realm = ?
AND vc.account_sid = acc.account_sid
AND vc.is_active = 1
AND sg.inbound = 1
AND sg.voip_carrier_sid = vc.voip_carrier_sid`;
const sqlSelectAllCarriersForSPByRealm =
`SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.account_sid,
vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask
vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask, sg.pad_crypto
FROM sip_gateways sg, voip_carriers vc, accounts acc
WHERE acc.sip_realm = ?
AND vc.service_provider_sid = acc.service_provider_sid
AND vc.account_sid IS NULL
AND vc.is_active = 1
AND sg.inbound = 1
AND sg.voip_carrier_sid = vc.voip_carrier_sid`;
const sqlSelectAllGatewaysForSP =
`SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.service_provider_sid,
vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask
vc.account_sid, vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask, sg.pad_crypto
FROM sip_gateways sg, voip_carriers vc
WHERE sg.voip_carrier_sid = vc.voip_carrier_sid
AND vc.service_provider_sid IS NOT NULL
AND vc.is_active = 1`;
AND vc.is_active = 1
AND sg.inbound = 1`;
const sqlCarriersForAccountBySid =
`SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.account_sid,
vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask
FROM sip_gateways sg, voip_carriers vc, accounts acc
WHERE acc.account_sid = ?
AND vc.account_sid = acc.account_sid
AND sg.voip_carrier_sid = vc.voip_carrier_sid`;
const sqlAccountByRealm = 'SELECT * from accounts WHERE sip_realm = ?';
const sqlAccountByRealm = 'SELECT * from accounts WHERE sip_realm = ? AND is_active = 1';
const sqlAccountBySid = 'SELECT * from accounts WHERE account_sid = ?';
const sqlApplicationBySid = 'SELECT * from applications WHERE application_sid = ?';
const sqlQueryApplicationByDid = `
SELECT * FROM phone_numbers
@@ -55,11 +55,25 @@ WHERE sg.voip_carrier_sid = ?
AND sg.voip_carrier_sid = vc.voip_carrier_sid
AND outbound = 1`;
const gatewayMatchesSourceAddress = (source_address, gw) => {
const sqlSelectCarrierRequiringRegistration = `
SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.service_provider_sid, vc.account_sid,
vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask, sg.pad_crypto
FROM sip_gateways sg, voip_carriers vc
WHERE sg.voip_carrier_sid = vc.voip_carrier_sid
AND vc.requires_register = 1
AND vc.is_active = 1
AND vc.register_sip_realm = ?
AND vc.register_username = ?`;
const gatewayMatchesSourceAddress = (logger, source_address, gw) => {
if (32 === gw.netmask && gw.ipv4 === source_address) return true;
if (gw.netmask < 32) {
const matcher = new CIDRMatcher([`${gw.ipv4}/${gw.netmask}`]);
return matcher.contains(source_address);
try {
const matcher = new CIDRMatcher([`${gw.ipv4}/${gw.netmask}`]);
return matcher.contains(source_address);
} catch (err) {
logger.info({err, gw}, 'gatewayMatchesSourceAddress: Error parsing netmask');
}
}
return false;
};
@@ -68,6 +82,18 @@ module.exports = (srf, logger) => {
const {pool} = srf.locals.dbHelpers;
const pp = pool.promise();
const getApplicationBySid = async(application_sid) => {
const [r] = await pp.query(sqlApplicationBySid, [application_sid]);
if (0 === r.length) return null;
return r[0];
};
const getSPForAccount = async(account_sid) => {
const [r] = await pp.query(sqlSelectSPForAccount, [account_sid]);
if (0 === r.length) return null;
return r[0].service_provider_sid;
};
const getOutboundGatewayForRefer = async(voip_carrier_sid) => {
try {
const [r] = await pp.query(sqlSelectOutboundGatewayForCarrier, [voip_carrier_sid]);
@@ -75,14 +101,30 @@ module.exports = (srf, logger) => {
/* if multiple, prefer a DNS name */
const hasDns = r.find((row) => row.ipv4.match(/^[A-Za-z]/));
return hasDns || r[0];
return hasDns /* || r[0] */;
} catch (err) {
logger.error({err}, 'getOutboundGatewayForRefer');
}
};
/**
* Retrieves the application for a given DID (phone number) and carrier combination.
* First attempts an exact DID match, then falls back to pattern/wildcard matching
* if no exact match is found. Pattern matching is done in order of pattern length
* (longest/most specific first) and handles both regex and wildcard (*) patterns.
*
* @param {Object} req - The request object containing the called number
* @param {string} voip_carrier_sid - The SID of the VoIP carrier
* @returns {Promise<(string|Object|null)>} Returns either:
* - application_sid (string) if appObject is false
* - full application object if appObject is true
* - null if no matching application is found
* @throws {Error} Database errors or other unexpected errors
*/
// Refactored to handle invalid regular expressions gracefully as part of
// https://github.com/jambonz/sbc-inbound/issues/201
const getApplicationForDidAndCarrier = async(req, voip_carrier_sid) => {
const did = normalizeDID(req.calledNumber);
const did = normalizeDID(req.calledNumber) || 'anonymous';
try {
/* straight DID match */
@@ -91,96 +133,345 @@ module.exports = (srf, logger) => {
/* wildcard / regex match */
const [r2] = await pp.query(sqlQueryAllDidsForCarrier, [voip_carrier_sid]);
const match = r2
const patterns = r2
.filter((o) => o.number.match(/\D/)) // look at anything with non-digit characters
.sort((a, b) => b.number.length - a.number.length) // prefer longest match
.find((o) => did.match(new RegExp(o.number.endsWith('*') ? `${o.number.slice(0, -1)}\\d*` : o.number)));
if (match) return match.application_sid;
.sort((a, b) => b.number.length - a.number.length);
for (const pattern of patterns) {
try {
const regexPattern = pattern.number.endsWith('*') ?
`${pattern.number.slice(0, -1)}\\d*` :
pattern.number;
if (did.match(new RegExp(regexPattern))) {
logger.debug({voip_carrier_sid, 'pattern':pattern.number, 'did':did}, 'Found a matching pattern');
return pattern.application_sid;
}
} catch (err) {
logger.warn({err, voip_carrier_sid, pattern: pattern.number}, 'Invalid regex pattern encountered - skipping');
continue;
}
}
logger.debug({voip_carrier_sid, did}, 'No matching pattern found');
return null;
} catch (err) {
logger.error({err}, 'getApplicationForDidAndCarrier');
}
};
/**
* Searches for a matching application across multiple carriers using pattern/wildcard matching.
* This function is similar to getApplicationForDidAndCarrier but operates on multiple carriers
* simultaneously for better performance. It searches through all patterns from the specified
* carriers, ordered by pattern length (longest/most specific first).
*
* @param {Object} req - The request object containing the called number
* @param {string} voip_carrier_sids - Comma-separated string of carrier SIDs, formatted for SQL IN clause
* (e.g., "'carrier1','carrier2'")
* @returns {Promise<Object|null>} Returns either:
* - The matching phone_numbers record containing application_sid, voip_carrier_sid, and account_sid
* - null if no matching pattern is found
* @throws {Error} Database errors or other unexpected errors
*/
const getApplicationForDidAndCarriers = async(req, voip_carrier_sids) => {
const did = normalizeDID(req.calledNumber) || 'anonymous';
const sql = `SELECT * FROM phone_numbers WHERE voip_carrier_sid IN (${voip_carrier_sids})`;
try {
/* wildcard / regex match */
const [r2] = await pp.query(sql);
const patterns = r2
.filter((o) => o.number.match(/\D/)) // look at anything with non-digit characters
.sort((a, b) => b.number.length - a.number.length);
for (const pattern of patterns) {
try {
const regexPattern = pattern.number.endsWith('*') ?
`${pattern.number.slice(0, -1)}\\d*` :
pattern.number;
if (did.match(new RegExp(regexPattern))) {
logger.debug({'pattern':pattern.number, 'did':did}, 'Found a matching pattern');
return pattern;
}
} catch (err) {
logger.warn({err, pattern: pattern.number}, 'Invalid regex pattern encountered - skipping');
continue;
}
}
logger.debug('No matching pattern found');
return null;
} catch (err) {
logger.error({err}, 'getApplicationForDidAndCarriers');
}
};
const wasOriginatedFromCarrier = async(req) => {
const failure = {fromCarrier: false};
const uri = parseUri(req.uri);
const isDotDecimal = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(uri.host);
const did = normalizeDID(req.calledNumber) || 'anonymous';
if (!isDotDecimal) {
/**
* The host part of the SIP URI is not a dot-decimal IP address,
* so this can be one of two things:
* (1) a sip realm value associate with an account, or
* (2) a carrier name for a carrier that we send outbound registrations to
*
* Let's look for case #1 first...
*/
if (isDotDecimal) {
if (process.env.JAMBONES_HOSTING) {
if (!process.env.SBC_ACCOUNT_SID) return failure;
/* does anyone own this sip realm? */
const [a] = await pp.query(sqlAccountByRealm, [uri.host]);
if (a.length) {
assert(a.length === 1);
/* look for carrier only within that account */
const [r] = await pp.query(sqlCarriersForAccountBySid,
[process.env.SBC_ACCOUNT_SID, req.source_address, req.source_port]);
if (0 === r.length) return failure;
return {
fromCarrier: true,
gateway: r[0],
account_sid: process.env.SBC_ACCOUNT_SID
};
}
else {
/* we may have a carrier at the service provider level */
const [gw] = await pp.query(sqlSelectAllGatewaysForSP);
const matches = gw.filter(gatewayMatchesSourceAddress.bind(null, req.source_address));
if (matches.length) {
/* we have one or more carriers that match. Now we need to find one with a provisioned phone number */
const vc_sids = matches.map((m) => `'${m.voip_carrier_sid}'`).join(',');
const did = normalizeDID(req.calledNumber);
const sql = `SELECT * FROM phone_numbers WHERE number = ${did} AND voip_carrier_sid IN (${vc_sids})`;
logger.debug({matches, sql, did, vc_sids}, 'looking up DID');
/* yes they do */
logger.debug(`sip realm is associated with account_sid: ${a[0].account_sid}`);
/**
* We have one of two cases:
* (1a). The user configured his or her carrier to send to their sip realm, or
* (1b). The user is making a call from a sip device.
*/
/* get all the carriers and gateways for the account owning this sip realm */
const [gwAcc] = await pp.query(sqlSelectAllCarriersForAccountByRealm, [uri.host]);
const [gwSP] = gwAcc.length ? [[]] : await pp.query(sqlSelectAllCarriersForSPByRealm, uri.host);
const gw = gwAcc
.concat(gwSP)
.sort((a, b) => b.netmask - a.netmask);
const gateways = gw.filter(gatewayMatchesSourceAddress.bind(null, logger, req.source_address));
let voip_carriers = gateways.map((gw) => {
return {
voip_carrier_sid: gw.voip_carrier_sid,
name: gw.name,
service_provider_sid: gw.service_provider_sid,
account_sid: gw.account_sid,
application_sid: gw.application_sid
};
});
/* remove duplicates, winnow down to voip_carriers, not gateways */
if (voip_carriers.length > 1) {
voip_carriers = [...new Set(voip_carriers.map(JSON.stringify))].map(JSON.parse);
}
if (voip_carriers.length) {
/* we have one or more matches. Now check for one with a provisioned phone number matching the DID */
const vc_sids = voip_carriers.map((m) => `'${m.voip_carrier_sid}'`).join(',');
const sql =
`SELECT * FROM phone_numbers WHERE number = '${did}'
AND voip_carrier_sid IN (${vc_sids})
AND account_sid = '${a[0].account_sid}'`;
//logger.debug({voip_carriers, sql, did}, 'looking up DID');
const [r] = await pp.query(sql);
if (0 === r.length) {
/* came from a carrier, but number is not provisioned..
check if we only have a single account, otherwise we have no
way of knowing which account this is for
*/
const [r] = await pp.query('SELECT count(*) as count from accounts where service_provider_sid = ?',
matches[0].service_provider_sid);
if (r[0].count === 0) return {fromCarrier: true};
else {
const [accounts] = await pp.query('SELECT * from accounts where service_provider_sid = ?',
matches[0].service_provider_sid);
if (r.length === 0) {
// if no matching phone number is found, find whether there are any applications having phone numbers
// matching the regex pattern or wild card
logger.debug({isDotDecimal},
'Did not find a matching phone number, checking for applications with Regex or wildcard');
const vc_sids = voip_carriers.map((m) => `'${m.voip_carrier_sid}'`).join(',');
const application = await getApplicationForDidAndCarriers(req, vc_sids);
if (application) {
logger.debug({application}, 'sip_realm looking up DID: found application with Regex or wildcard ');
const gateway = gateways.find((m) => m.voip_carrier_sid === application.voip_carrier_sid);
// the gateway may belong to "all accounts", so we need to use the account_sid of the application
gateway.account_sid = gateway.account_sid ? gateway.account_sid : application.account_sid;
return {
fromCarrier: true,
gateway: matches[0],
account_sid: accounts[0].account_sid,
account: accounts[0]
gateway: gateway,
service_provider_sid: a[0].service_provider_sid,
account_sid: gateway.account_sid,
application_sid: application.application_sid,
account: a[0]
};
}
}
const gateway = matches.find((m) => m.voip_carrier_sid === r[0].voip_carrier_sid);
const [accounts] = await pp.query(sqlAccountBySid, r[0].account_sid);
assert(accounts.length);
if (r.length > 1) {
logger.info({r},
'multiple carriers with the same gateway have the same number provisioned for the same account'
+ ' -- cannot determine which one to use');
return {
fromCarrier: true,
error: 'Multiple carriers with the same gateway are attempting to route the same number for this account'
};
}
/**
* We have one or no routes for this phone number, carrier and account combination
* Either take the matching gateway, or if no route has matched, the first gateway of the ones available.
*/
const gateway = r[0] ? gateways.find((m) => m.voip_carrier_sid === r[0]?.voip_carrier_sid) : gateways[0];
return {
fromCarrier: true,
gateway,
account_sid: r[0].account_sid,
application_sid: r[0].application_sid,
account: accounts[0]
gateway: gateway,
service_provider_sid: a[0].service_provider_sid,
account_sid: a[0].account_sid,
application_sid: r[0]?.application_sid || gateway.application_sid,
account: a[0]
};
}
return failure;
}
/* no match, so let's look for case #2 */
try {
logger.info({
host: uri.host,
user: uri.user
}, 'sip realm is not associated with an account, checking carriers');
const [gw] = await pp.query(sqlSelectCarrierRequiringRegistration, [uri.host, uri.user]);
const matches = gw
.sort((a, b) => b.netmask - a.netmask)
.filter(gatewayMatchesSourceAddress.bind(null, logger, req.source_address));
if (1 === matches.length) {
// bingo
//TODO: this assumes the carrier is associate to an account, not an SP
//if the carrier is associated with an SP (which would mean we
//must see a dialed number in the To header, not the register username),
//then we need to look up the account based on the dialed number in the To header
const [a] = await pp.query(sqlAccountBySid, [[matches[0].account_sid]]);
if (0 === a.length) return failure;
logger.debug({matches}, `found registration carrier using ${uri.host} and ${uri.user}`);
const sql =
`SELECT application_sid FROM phone_numbers WHERE number = '${did}'
AND voip_carrier_sid = '${matches[0].voip_carrier_sid}'
AND account_sid = '${matches[0].account_sid}'`;
//logger.debug({matches: matches[0], sql, did}, 'looking up DID');
const [r] = await pp.query(sql);
return {
fromCarrier: true,
gateway: matches[0],
service_provider_sid: a[0].service_provider_sid,
account_sid: a[0].account_sid,
application_sid: r[0]?.application_sid || matches[0].application_sid,
account: a[0]
};
}
else if (matches.length > 1) {
logger.warn({matches, source_address: req.source_address}, 'multiple gateways match source address');
return {
fromCarrier: true,
error: 'Multiple gateways match registration carrier source address'
};
}
} catch (err) {
logger.info({err, host: uri.host, user: uri.user}, 'Error looking up carrier by host and user');
}
/* no match, so fall through */
}
/* get all the carriers and gateways for the account owning this sip realm */
const [gwAcc] = await pp.query(sqlSelectAllCarriersForAccountByRealm, uri.host);
const [gwSP] = gwAcc.length ? [[]] : await pp.query(sqlSelectAllCarriersForSPByRealm, uri.host);
const gw = gwAcc.concat(gwSP);
const selected = gw.find(gatewayMatchesSourceAddress.bind(null, req.source_address));
if (selected) {
const [a] = await pp.query(sqlAccountByRealm, uri.host);
if (0 === a.length) return failure;
/* find all carrier entries that have an inbound gateway matching the source IP */
const [gw] = await pp.query(sqlSelectAllGatewaysForSP);
//logger.debug({gw}, `checking gateways for source address ${req.source_address}`);
let matches = gw
.sort((a, b) => b.netmask - a.netmask)
.filter(gatewayMatchesSourceAddress.bind(null, logger, req.source_address))
.map((gw) => {
return {
voip_carrier_sid: gw.voip_carrier_sid,
name: gw.name,
service_provider_sid: gw.service_provider_sid,
account_sid: gw.account_sid,
application_sid: gw.application_sid,
pad_crypto: gw.pad_crypto
};
});
/* remove duplicates, winnow down to voip_carriers, not gateways */
if (matches.length > 1) {
matches = [...new Set(matches.map(JSON.stringify))].map(JSON.parse);
}
//logger.debug({matches}, `matches for source address ${req.source_address}`);
if (matches.length) {
/* we have one or more matches. Now check for one with a provisioned phone number matching the DID */
const vc_sids = matches.map((m) => `'${m.voip_carrier_sid}'`).join(',');
const sql = `SELECT * FROM phone_numbers WHERE number = '${did}' AND voip_carrier_sid IN (${vc_sids})`;
logger.debug({matches, sql, did, vc_sids}, 'looking up DID');
const [r] = await pp.query(sql);
if (0 === r.length) {
// if no matching phone number is found, find whether there are any applications having phone numbers
// matching the regex pattern or wild card
logger.debug({isDotDecimal},
'Did not find a matching phone number, checking for applications with Regex or wildcard');
const vc_sids = matches.map((m) => `'${m.voip_carrier_sid}'`).join(',');
const application = await getApplicationForDidAndCarriers(req, vc_sids);
if (application) {
logger.debug({application}, 'looking up DID: found application with Regex or wildcard');
const gateway = matches.find((m) => m.voip_carrier_sid === application.voip_carrier_sid);
// the gateway may belong to "all accounts", so we need to use the account_sid of the application
const [a] = await pp.query(sqlAccountBySid, [application.account_sid]);
return {
fromCarrier: true,
gateway: gateway,
service_provider_sid: gateway.service_provider_sid,
account_sid: application.account_sid,
application_sid: application.application_sid,
account: a[0],
};
}
/* came from a provisioned carrier, but the dialed number is not provisioned.
check if we have an account with default routing of that carrier to an application
*/
const accountLevelGateways = matches.filter((m) => m.account_sid && m.application_sid);
if (accountLevelGateways.length > 1) {
logger.info({accounts: accountLevelGateways.map((m) => m.account_sid)},
'multiple accounts have added this carrier with default routing -- cannot determine which to use');
return {
fromCarrier: true,
error: 'Multiple accounts are attempting to default route this carrier'
};
}
else if (accountLevelGateways.length === 1) {
const [accounts] = await pp.query('SELECT * from accounts where account_sid = ?',
[accountLevelGateways[0].account_sid]);
return {
fromCarrier: true,
gateway: accountLevelGateways[0],
service_provider_sid: accountLevelGateways[0].service_provider_sid,
account_sid: accountLevelGateways[0].account_sid,
application_sid: accountLevelGateways[0].application_sid,
account: accounts[0]
};
}
else {
/* check if we only have a single account, otherwise we have no
- way of knowing which account this is for
*/
const [r] = await pp.query('SELECT count(*) as count from accounts where service_provider_sid = ?',
[matches[0].service_provider_sid]);
if (r[0].count === 0 || r[0].count > 1) return {fromCarrier: true};
else {
const [accounts] = await pp.query('SELECT * from accounts where service_provider_sid = ?',
[matches[0].service_provider_sid]);
return {
fromCarrier: true,
gateway: matches[0],
service_provider_sid: accounts[0].service_provider_sid,
account_sid: accounts[0].account_sid,
account: accounts[0]
};
}
}
}
else if (r.length > 1) {
logger.info({r},
'multiple accounts have added this carrier with default routing -- cannot determine which to use');
return {
fromCarrier: true,
error: 'Multiple accounts are attempting to route the same phone number from the same carrier'
};
}
/* we have a route for this phone number and carrier combination */
const gateway = matches.find((m) => m.voip_carrier_sid === r[0].voip_carrier_sid);
const [accounts] = await pp.query(sqlAccountBySid, [r[0].account_sid]);
assert(accounts.length);
return {
fromCarrier: true,
gateway: selected,
account_sid: a[0].account_sid,
application_sid: selected.application_sid,
account: a[0]
gateway,
service_provider_sid: accounts[0].service_provider_sid,
account_sid: r[0].account_sid,
application_sid: r[0].application_sid,
account: accounts[0]
};
}
return failure;
@@ -189,6 +480,9 @@ module.exports = (srf, logger) => {
return {
wasOriginatedFromCarrier,
getApplicationForDidAndCarrier,
getOutboundGatewayForRefer
getApplicationForDidAndCarriers,
getOutboundGatewayForRefer,
getSPForAccount,
getApplicationBySid
};
};
+3 -1
View File
@@ -15,7 +15,9 @@ module.exports = (srf, logger) => {
return async() => {
try {
if (process.env.K8S) {
return process.env.K8S_FEATURE_SERVER_SERVICE_NAME;
return process.env.K8S_FEATURE_SERVER_TRANSPORT ?
`${process.env.K8S_FEATURE_SERVER_SERVICE_NAME};transport=${process.env.K8S_FEATURE_SERVER_TRANSPORT}` :
process.env.K8S_FEATURE_SERVER_SERVICE_NAME;
}
else {
const fs = await retrieveSet(setName);
+134 -83
View File
@@ -1,8 +1,8 @@
const debug = require('debug')('jambonz:sbc-inbound');
const assert = require('assert');
const Emitter = require('events');
const parseUri = require('drachtio-srf').parseUri;
const {makeCallCountKey} = require('./utils');
const {nudgeCallCounts, roundTripTime, isMSTeamsCIDR} = require('./utils');
const digestChallenge = require('@jambonz/digest-utils');
const msProxyIps = process.env.MS_TEAMS_SIP_PROXY_IPS ?
process.env.MS_TEAMS_SIP_PROXY_IPS.split(',').map((i) => i.trim()) :
[];
@@ -22,56 +22,20 @@ const initCdr = (req) => {
};
module.exports = function(srf, logger) {
class AuthOutcomeReporter extends Emitter {
constructor(stats) {
super();
this
.on('regHookOutcome', ({rtt, status}) => {
stats.histogram('app.hook.response_time', rtt, ['hook_type:auth', `status:${status}`]);
})
.on('error', async(err, req) => {
const {account_sid} = req.locals;
const {writeAlerts, AlertType} = req.srf.locals;
if (account_sid) {
let opts = {account_sid};
if (err.code === 'ECONNREFUSED') {
opts = {...opts, alert_type: AlertType.WEBHOOK_CONNECTION_FAILURE, url: err.hook};
}
else if (err.code === 'ENOTFOUND') {
opts = {...opts, alert_type: AlertType.WEBHOOK_CONNECTION_FAILURE, url: err.hook};
}
else if (err.name === 'StatusError') {
opts = {...opts, alert_type: AlertType.WEBHOOK_STATUS_FAILURE, url: err.hook, status: err.statusCode};
}
if (opts.alert_type) {
try {
await writeAlerts(opts);
} catch (err) {
logger.error({err, opts}, 'Error writing alert');
}
}
}
});
}
}
const {
lookupAuthHook,
lookupAppByTeamsTenant,
lookupAccountBySipRealm,
lookupAccountBySid,
lookupAccountCapacitiesBySid
lookupAccountCapacitiesBySid,
queryCallLimits
} = srf.locals.dbHelpers;
const {stats, writeCdrs} = srf.locals;
const authenticator = require('@jambonz/http-authenticator')(lookupAuthHook, logger, {
blacklistUnknownRealms: true,
emitter: new AuthOutcomeReporter(stats)
});
const initLocals = (req, res, next) => {
const callId = req.get('Call-ID');
req.locals = req.locals || {callId};
req.locals.nudge = 0;
/* check if forwarded by a proxy that applied an X-Forwarded-For Header */
if (req.has('X-Forwarded-For') || req.has('X-Subspace-Forwarded-For')) {
@@ -134,14 +98,20 @@ module.exports = function(srf, logger) {
const identifyAccount = async(req, res, next) => {
try {
const {siprec, callId} = req.locals;
const {wasOriginatedFromCarrier, getApplicationForDidAndCarrier} = req.srf.locals;
const {getSPForAccount, wasOriginatedFromCarrier, getApplicationForDidAndCarrier, stats} = req.srf.locals;
const startAt = process.hrtime();
const {
fromCarrier,
gateway,
account_sid,
application_sid,
account
service_provider_sid,
account,
error
} = await wasOriginatedFromCarrier(req);
const rtt = roundTripTime(startAt);
stats.histogram('app.mysql.response_time', rtt, [
'query:wasOriginatedFromCarrier', 'app:sbc-inbound']);
/**
* calls come from 3 sources:
* (1) A carrier
@@ -149,12 +119,23 @@ module.exports = function(srf, logger) {
* (3) A SIP user
*/
if (fromCarrier) {
if (error) {
return res.send(503, {
headers: {
'X-Reason': error
}
});
}
if (!gateway) {
logger.info('identifyAccount: rejecting call from carrier because DID has not been provisioned');
return res.send(404, 'Number Not Provisioned');
}
logger.debug({gateway}, 'identifyAccount: incoming call from gateway');
logger.info({gateway}, 'identifyAccount: incoming call from gateway');
const appSidHeader = req.get('x-application-sid');
if (appSidHeader && appSidHeader == application_sid) {
logger.info({callId}, 'Loop Detected, x-application-sid header on incoming call matches applicationSid');
return res.send(482, 'Loop Detected on x-application-sid');
}
let sid;
if (siprec) {
if (!account.siprec_hook_sid) {
@@ -173,13 +154,14 @@ module.exports = function(srf, logger) {
gateway,
voip_carrier_sid: gateway.voip_carrier_sid,
application_sid: sid || gateway.application_sid,
service_provider_sid,
account_sid,
account,
...req.locals
};
}
else if (msProxyIps.includes(req.source_address)) {
logger.debug({source_address: req.source_address}, 'identifyAccount: incoming call from Microsoft Teams');
else if (msProxyIps.includes(req.source_address) || isMSTeamsCIDR(req.source_address)) {
logger.info({source_address: req.source_address}, 'identifyAccount: incoming call from Microsoft Teams');
const uri = parseUri(req.uri);
const app = await lookupAppByTeamsTenant(uri.host);
@@ -188,19 +170,20 @@ module.exports = function(srf, logger) {
res.send(404, {headers: {'X-Reason': 'no configured application'}});
return req.srf.endSession(req);
}
const service_provider_sid = await getSPForAccount(app.account_sid);
req.locals = {
originator: 'teams',
carrier: 'Microsoft Teams',
msTeamsTenantFqdn: uri.host,
account_sid: app.account_sid,
service_provider_sid,
...req.locals
};
}
else {
req.locals.originator = 'user';
const uri = parseUri(req.uri);
logger.debug({source_address: req.source_address, realm: uri.host},
logger.info({source_address: req.source_address, realm: uri.host},
'identifyAccount: incoming user call');
const account = await lookupAccountBySipRealm(uri.host);
if (!account) {
@@ -219,18 +202,29 @@ module.exports = function(srf, logger) {
return req.srf.endSession(req);
}
req.locals = {
service_provider_sid: account.service_provider_sid,
account_sid: account.account_sid,
account,
application_sid: account.device_calling_application_sid,
webhook_secret: account.webhook_secret,
realm: uri.host,
...(account.registration_hook && {
registration_hook_url: account.registration_hook.url,
registration_hook_method: account.registration_hook.method,
registration_hook_username: account.registration_hook.username,
registration_hook_password: account.registration_hook.password
}),
...req.locals
};
}
assert(req.locals.service_provider_sid);
assert(req.locals.account_sid);
req.locals.cdr.account_sid = req.locals.account_sid;
if (!req.locals.account) {
req.locals.account = await lookupAccountBySid(req.locals.account_sid);
}
req.locals.cdr.service_provider_sid = req.locals.account?.service_provider_sid;
if (!req.locals.account.is_active) {
stats.increment('sbc.terminations', ['sipStatus:503']);
@@ -242,7 +236,13 @@ module.exports = function(srf, logger) {
delete req.locals.cdr;
}
req.locals.logger = logger.child({callId: req.get('Call-ID'), account_sid: req.locals.account_sid});
req.locals.logger = logger.child({
callId: req.get('Call-ID'),
service_provider_sid: req.locals.service_provider_sid,
account_sid: req.locals.account_sid
}, {
...(req.locals.account.enable_debug_log && {level: 'debug'})
});
next();
} catch (err) {
@@ -253,35 +253,44 @@ module.exports = function(srf, logger) {
};
const checkLimits = async(req, res, next) => {
if (!process.env.JAMBONES_HOSTING && !process.env.JAMBONES_TRACK_ACCOUNT_CALLS) return next(); // skip
const trackingOn = process.env.JAMBONES_TRACK_ACCOUNT_CALLS ||
process.env.JAMBONES_TRACK_SP_CALLS ||
process.env.JAMBONES_TRACK_APP_CALLS;
if (!process.env.JAMBONES_HOSTING && !trackingOn) return next(); // skip
const {incrKey, decrKey} = req.srf.locals.realtimeDbHelpers;
const {logger, account_sid} = req.locals;
const {writeCallCount, writeAlerts, AlertType} = req.srf.locals;
const {logger, account_sid, account, service_provider_sid, application_sid} = req.locals;
const {writeCallCount, writeCallCountSP, writeCallCountApp, writeAlerts, AlertType} = req.srf.locals;
assert(account_sid);
const key = makeCallCountKey(account_sid);
assert(service_provider_sid);
/* if quick cancel then we are done */
if (req.canceled) {
logger.info('checkLimits: call was immediately canceled, no need to increment call count as we are done');
return;
}
/* decrement count if INVITE is later rejected */
res.once('end', ({status}) => {
res.once('end', async({status}) => {
if (status > 200) {
decrKey(key)
.then((count) => {
logger.info({key}, `after rejection there are ${count} active calls for this account`);
debug({key}, `after rejection there are ${count} active calls for this account`);
return count;
})
.then((count) => writeCallCount({account_sid, calls_in_progress: count}))
.catch((err) => logger.error({err}, 'checkLimits: decrKey err'));
nudgeCallCounts(req, 'failure', {
service_provider_sid,
account_sid,
application_sid,
callId: req.locals.callId
}, decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
.catch((err) => logger.error(err, 'Error decrementing call counts'));
}
});
try {
/* increment the call count */
const calls = await incrKey(key);
writeCallCount({account_sid, calls_in_progress: calls})
.then(() => logger.info(`checkLimits: after incrementing there are ${calls} active calls for this account`))
.catch((err) => logger.error({err}, 'checkLimits: error writing call count'));
if (!process.env.JAMBONES_HOSTING) return next();
const {callsSP, calls} = await nudgeCallCounts(req, 'init', {
service_provider_sid,
account_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 */
const minLimit = process.env.MIN_CALL_LIMIT ?
@@ -290,20 +299,62 @@ module.exports = function(srf, logger) {
logger.debug(`checkLimits: call count is now ${calls}, limit is ${minLimit}`);
if (calls <= minLimit) return next();
const capacities = await lookupAccountCapacitiesBySid(account_sid);
const limit = capacities.find((c) => c.category == 'voice_call_session');
if (!limit) throw new Error('no account_capacities found');
const limit_sessions = limit.quantity;
if (calls > limit_sessions) {
debug(`checkLimits: limits exceeded: call count ${calls}, limit ${limit_sessions}`);
logger.info({calls, limit_sessions}, 'checkLimits: limits exceeded');
writeAlerts({
alert_type: AlertType.CALL_LIMIT,
account_sid,
count: limit_sessions
}).catch((err) => logger.info({err}, 'checkLimits: error writing alert'));
res.send(503, 'Maximum Calls In Progress');
return req.srf.endSession(req);
const accountCapacities = await lookupAccountCapacitiesBySid(account_sid);
const accountLimit = accountCapacities.find((c) => c.category == 'voice_call_session');
if (accountLimit) {
/* check account limit */
const limit_sessions = accountLimit.quantity;
if (calls > limit_sessions) {
debug(`checkLimits: limits exceeded: call count ${calls}, limit ${limit_sessions}`);
logger.info({calls, limit_sessions}, 'checkLimits: limits exceeded');
writeAlerts({
alert_type: AlertType.ACCOUNT_CALL_LIMIT,
service_provider_sid: account.service_provider_sid,
account_sid,
count: limit_sessions
}).catch((err) => logger.info({err}, 'checkLimits: error writing alert'));
res.send(503, 'Maximum Calls In Progress');
return req.srf.endSession(req);
}
}
else if (trackingOn) {
const {account_limit, sp_limit} = await queryCallLimits(service_provider_sid, account_sid);
if (process.env.JAMBONES_TRACK_ACCOUNT_CALLS && account_limit > 0 && calls > account_limit) {
logger.info({calls, account_limit}, 'checkLimits: account limits exceeded');
writeAlerts({
alert_type: AlertType.ACCOUNT_CALL_LIMIT,
service_provider_sid: service_provider_sid,
account_sid,
count: account_limit
}).catch((err) => logger.info({err}, 'checkLimits: error writing alert'));
res.send(503, 'Max Account Calls In Progress', {
headers: {
'X-Account-Sid': account_sid,
'X-Call-Limit': account_limit
}
});
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({
alert_type: AlertType.SP_CALL_LIMIT,
service_provider_sid: service_provider_sid,
count: sp_limit
}).catch((err) => logger.info({err}, 'checkLimits: error writing alert'));
res.send(503, 'Max Service Provider Calls In Progress', {
headers: {
'X-Service-Provider-Sid': service_provider_sid,
'X-Call-Limit': sp_limit
}
});
return req.srf.endSession(req);
}
}
next();
} catch (err) {
@@ -318,7 +369,7 @@ module.exports = function(srf, logger) {
try {
/* TODO: check if this is a gateway that we have an ACL for */
if (req.locals.originator !== 'user') return next();
return authenticator(req, res, next);
return digestChallenge(req, res, next);
} catch (err) {
stats.increment('sbc.terminations', ['sipStatus:500']);
logger.error(err, `${req.get('Call-ID')} Error looking up related info for inbound call`);
+287 -17
View File
@@ -1,5 +1,8 @@
const CIDRMatcher = require('cidr-matcher');
const express = require('express');
const rtpCharacteristics = require('../data/rtp-transcoding');
const srtpCharacteristics = require('../data/srtp-transcoding');
let idx = 0;
const isWSS = (req) => {
@@ -8,23 +11,40 @@ const isWSS = (req) => {
const getAppserver = (srf) => {
const len = srf.locals.featureServers.length;
return srf.locals.featureServers[ idx++ % len];
return srf.locals.featureServers[idx++ % len];
};
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) {
const rtpCopy = JSON.parse(JSON.stringify(rtpCharacteristics));
const srtpCopy = JSON.parse(JSON.stringify(srtpCharacteristics));
const from = req.getParsedHeader('from');
const srtpOpts = teams ? srtpCharacteristics['teams'] : srtpCharacteristics['default'];
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCharacteristics;
const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCharacteristics;
const srtpOpts = teams ? srtpCopy['teams'] : srtpCopy['default'];
if ((req.locals.gateway?.pad_crypto || 0) > 0) {
srtpOpts.flags.push('pad crypto');
}
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCopy;
const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCopy;
/* webrtc clients (e.g. sipjs) send DMTF via SIP INFO */
if ((srcIsUsingSrtp || dstIsUsingSrtp) && !teams) {
/* Allow Feature server to inject DTMF to both leg except call from Teams */
if (!teams) {
dstOpts.flags.push('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 acceptCodecs = process.env.JAMBONES_ACCEPT_AND_TRANSCODE ?
process.env.JAMBONES_ACCEPT_AND_TRANSCODE :
process.env.JAMBONES_ACCEPT_G729 ? 'g729' : '';
const common = {
'call-id': req.get('Call-ID'),
'replace': ['origin', 'session-connection']
'replace': ['origin', 'session-connection'],
'record call': process.env.JAMBONES_RECORD_ALL_CALLS ? 'yes' : 'no',
...(acceptCodecs && { codec: { mask: acceptCodecs, transcode: 'pcmu,pcma' } })
};
return {
common,
@@ -39,11 +59,16 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) {
};
}
const SdpWantsSDES = (sdp) => {
return /m=audio.*\s+RTP\/SAVP/.test(sdp);
};
const SdpWantsSrtp = (sdp) => {
return /m=audio.*SAVP/.test(sdp);
};
const makeCallCountKey = (sid) => `${sid}:incalls`;
const makeAccountCallCountKey = (sid) => `incalls:account:${sid}`;
const makeSPCallCountKey = (sid) => `incalls:sp:${sid}:`;
const makeAppCallCountKey = (sid) => `incalls:app${sid}:`;
const normalizeDID = (tel) => {
const regex = /^\+(\d+)$/;
@@ -62,34 +87,279 @@ const equalsIgnoreOrder = (a, b) => {
return true;
};
const systemHealth = async(redisClient, ping, getCount) => {
const systemHealth = async(redisClient, ping, getCount) => {
await Promise.all([redisClient.ping(), ping()]);
return getCount();
};
const doListen = (logger, app, port, resolve) => {
return app.listen(port, () => {
logger.info(`Health check server listening on http://localhost:${port}`);
resolve(app);
});
};
const handleErrors = (logger, app, resolve, reject, e) => {
if (e.code === 'EADDRINUSE' &&
process.env.HTTP_PORT_MAX &&
e.port < process.env.HTTP_PORT_MAX) {
logger.info(`Health check server failed to bind port on ${e.port}, will try next port`);
const server = doListen(logger, app, ++e.port, resolve);
server.on('error', handleErrors.bind(null, logger, app, resolve, reject));
return;
}
reject(e);
};
const createHealthCheckApp = (port, logger) => {
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
return new Promise((resolve) => {
app.listen(port, () => {
logger.info(`Health check server started at http://localhost:${port}`);
resolve(app);
});
return new Promise((resolve, reject) => {
const server = doListen(logger, app, port, resolve);
server.on('error', handleErrors.bind(null, logger, app, resolve, reject));
});
};
/**
* 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 = [];
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:incalls:${account_sid}:${callId}`, new Date().toISOString(), 259200);
}
else {
await deleteKey(`debug:incalls:${account_sid}:${callId}`);
}
}
if (process.env.JAMBONES_TRACK_SP_CALLS) {
const key = makeSPCallCountKey(service_provider_sid);
nudges.push(nudgeOperator(key));
}
else {
nudges.push(() => Promise.resolve(null));
}
if (process.env.JAMBONES_TRACK_ACCOUNT_CALLS || process.env.JAMBONES_HOSTING) {
const key = makeAccountCallCountKey(account_sid);
nudges.push(nudgeOperator(key));
}
else {
nudges.push(() => Promise.resolve(null));
}
if (process.env.JAMBONES_TRACK_APP_CALLS && application_sid) {
const key = makeAppCallCountKey(application_sid);
nudges.push(nudgeOperator(key));
}
else {
nudges.push(() => Promise.resolve(null));
}
try {
const [callsSP, calls, callsApp] = await Promise.all(nudges);
logger.debug({
calls, callsSP, callsApp,
service_provider_sid, account_sid, application_sid}, 'call counts after adjustment');
if (process.env.JAMBONES_TRACK_SP_CALLS) {
writes.push(writeCallCountSP({service_provider_sid, calls_in_progress: callsSP}));
}
if (process.env.JAMBONES_TRACK_ACCOUNT_CALLS || process.env.JAMBONES_HOSTING) {
writes.push(writeCallCount({service_provider_sid, account_sid, calls_in_progress: calls}));
}
if (process.env.JAMBONES_TRACK_APP_CALLS && application_sid) {
writes.push(writeCallCountApp({service_provider_sid, account_sid, application_sid, calls_in_progress: callsApp}));
}
/* write the call counts to the database */
Promise.all(writes).catch((err) => logger.error({err}, 'Error writing call counts'));
return {callsSP, calls, callsApp};
} catch (err) {
logger.error(err, 'error incrementing call counts');
}
return {callsSP: null, calls: null, callsApp: null};
};
const roundTripTime = (startAt) => {
const diff = process.hrtime(startAt);
const time = diff[0] * 1e3 + diff[1] * 1e-6;
return time.toFixed(0);
};
const parseConnectionIp = (sdp) => {
const regex = /c=IN IP4 ([0-9.]+)/;
const arr = regex.exec(sdp);
return arr ? arr[1] : null;
};
/**
* Checks if ip is one of MS Teams sip signalling ips
* https://learn.microsoft.com/en-us/azure/communication-services/concepts
* /telephony/direct-routing-infrastructure#sip-signaling-fqdns
* @param ip IP address, example 172.31.0.1
* */
const isMSTeamsCIDR = (ip) => {
const cidrs = [
'52.112.0.0/14',
'52.120.0.0/14'
];
const matcher = new CIDRMatcher(cidrs);
return matcher.contains(ip);
};
const isPrivateVoipNetwork = (ip) => {
const {srf, logger} = require('..');
const {privateNetworkCidr} = srf.locals;
if (privateNetworkCidr) {
try {
const matcher = new CIDRMatcher(privateNetworkCidr.split(','));
return matcher.contains(ip);
} catch (err) {
logger.info({err, privateNetworkCidr},
'Error checking private network CIDR, probably misconfigured must be a comma separated list of CIDRs');
}
}
return false;
};
/**
* @param hostports can be a string or an array of hostports
*/
const parseHostPorts = (logger, hostports, srf) => {
typeof hostports === 'string' && (hostports = hostports.split(','));
const obj = {};
for (const hp of hostports) {
const [, protocol, ipv4, port] = hp.match(/^(.*)\/(.*):(\d+)$/);
if (protocol && ipv4 && port) {
obj[protocol] = `${ipv4}:${port}`;
}
}
if (!obj.tls) {
obj.tls = `${srf.locals.sipAddress}:5061`;
}
if (!obj.tcp) {
obj.tcp = `${srf.locals.sipAddress}:5060`;
}
logger.info({ obj }, 'sip endpoints');
return obj;
};
const makeFullMediaReleaseKey = (callId) => {
return `a_sdp:${callId}`;
};
const makePartnerFullMediaReleaseKey = (callId) => {
return `b_sdp:${callId}`;
};
module.exports = {
isWSS,
SdpWantsSrtp,
SdpWantsSDES,
getAppserver,
makeRtpEngineOpts,
makeCallCountKey,
makeAccountCallCountKey,
makeSPCallCountKey,
makeAppCallCountKey,
normalizeDID,
equalsIgnoreOrder,
systemHealth,
createHealthCheckApp
createHealthCheckApp,
nudgeCallCounts,
roundTripTime,
parseConnectionIp,
isMSTeamsCIDR,
isPrivateVoipNetwork,
parseHostPorts,
makeFullMediaReleaseKey,
makePartnerFullMediaReleaseKey
};
+5827 -3512
View File
File diff suppressed because it is too large Load Diff
+23 -22
View File
@@ -1,9 +1,9 @@
{
"name": "sbc-inbound",
"version": "v0.7.6",
"version": "0.9.5",
"main": "app.js",
"engines": {
"node": ">= 12.0.0"
"node": ">= 18.0.0"
},
"keywords": [
"sip",
@@ -20,36 +20,37 @@
},
"scripts": {
"start": "node app",
"test": "NODE_ENV=test HTTP_PORT=3050 JAMBONES_NETWORK_CIDR='127.0.0.1/32' JAMBONES_HOSTING=1 SBC_ACCOUNT_SID=ed649e33-e771-403a-8c99-1780eabbc803 JAMBONES_TIME_SERIES_HOST=127.0.0.1 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_LOGLEVEL=info DRACHTIO_SECRET=cymru DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 JAMBONES_RTPENGINES=127.0.0.1:12222 JAMBONES_FEATURE_SERVERS=172.38.0.11 node test/ ",
"test": "NODE_ENV=test HTTP_PORT=3050 JAMBONES_NETWORK_CIDR='127.0.0.1/32' JAMBONES_HOSTING=1 JWT_SECRET=foobarbazzle SBC_ACCOUNT_SID=ed649e33-e771-403a-8c99-1780eabbc803 JAMBONES_TIME_SERIES_HOST=127.0.0.1 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_LOGLEVEL=error DRACHTIO_SECRET=cymru DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 JAMBONES_RTPENGINES=127.0.0.1:12222 JAMBONES_FEATURE_SERVERS=172.38.0.11 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",
"jslint:fix": "npm run jslint --fix"
},
"dependencies": {
"@jambonz/db-helpers": "^0.6.18",
"@jambonz/http-authenticator": "^0.2.1",
"@aws-sdk/client-auto-scaling": "^3.549.0",
"@aws-sdk/client-sns": "^3.549.0",
"@babel/helpers": "^7.26.10",
"@jambonz/db-helpers": "^0.9.12",
"@jambonz/digest-utils": "^0.0.6",
"@jambonz/http-health-check": "^0.0.1",
"@jambonz/realtimedb-helpers": "^0.4.29",
"@jambonz/rtpengine-utils": "^0.3.1",
"@jambonz/siprec-client-utils": "^0.1.4",
"@jambonz/stats-collector": "^0.1.6",
"@jambonz/time-series": "^0.1.12",
"aws-sdk": "^2.1152.0",
"@jambonz/realtimedb-helpers": "^0.8.13",
"@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",
"bent": "^7.3.12",
"cidr-matcher": "^2.1.1",
"debug": "^4.3.4",
"drachtio-fn-b2b-sugar": "0.0.12",
"drachtio-srf": "^4.5.1",
"express": "^4.18.1",
"pino": "^7.11.0",
"sdp-transform": "^2.14.1",
"uuid": "^8.3.2",
"verify-aws-sns-signature": "^0.0.7",
"xml2js": "^0.4.23"
"drachtio-fn-b2b-sugar": "0.2.1",
"drachtio-srf": "^5.0.5",
"express": "^4.19.2",
"pino": "^8.20.0",
"verify-aws-sns-signature": "^0.1.0",
"xml2js": "^0.6.2"
},
"devDependencies": {
"eslint": "^7.32.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-promise": "^6.1.1",
"nyc": "^15.1.0",
"tape": "^4.15.1"
"tape": "^5.7.5"
}
}
+220 -26
View File
@@ -4,6 +4,8 @@ SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS account_static_ips;
DROP TABLE IF EXISTS account_limits;
DROP TABLE IF EXISTS account_products;
DROP TABLE IF EXISTS account_subscriptions;
@@ -12,32 +14,52 @@ 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;
DROP TABLE IF EXISTS lcr_carrier_set_entry;
DROP TABLE IF EXISTS lcr_routes;
DROP TABLE IF EXISTS password_settings;
DROP TABLE IF EXISTS user_permissions;
DROP TABLE IF EXISTS permissions;
DROP TABLE IF EXISTS predefined_sip_gateways;
DROP TABLE IF EXISTS predefined_smpp_gateways;
DROP TABLE IF EXISTS predefined_carriers;
DROP TABLE IF EXISTS account_offers;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS schema_version;
DROP TABLE IF EXISTS api_keys;
DROP TABLE IF EXISTS sbc_addresses;
DROP TABLE IF EXISTS ms_teams_tenants;
DROP TABLE IF EXISTS service_provider_limits;
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;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS smpp_gateways;
@@ -65,6 +87,15 @@ private_ipv4 VARBINARY(16) NOT NULL UNIQUE ,
PRIMARY KEY (account_static_ip_sid)
);
CREATE TABLE account_limits
(
account_limits_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
category ENUM('api_rate','voice_call_session', 'device','voice_call_minutes','voice_call_session_license', 'voice_call_minutes_license') NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (account_limits_sid)
);
CREATE TABLE account_subscriptions
(
account_subscription_sid CHAR(36) NOT NULL UNIQUE ,
@@ -101,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 ,
@@ -113,11 +157,38 @@ PRIMARY KEY (dns_record_sid)
CREATE TABLE lcr_routes
(
lcr_route_sid CHAR(36),
lcr_sid CHAR(36) NOT NULL,
regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
description VARCHAR(1024),
priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted first',
priority INTEGER NOT NULL COMMENT 'lower priority routes are attempted first',
PRIMARY KEY (lcr_route_sid)
) COMMENT='Least cost routing table';
) COMMENT='An ordered list of digit patterns in an LCR table. The patterns are tested in sequence until one matches';
CREATE TABLE lcr
(
lcr_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) COMMENT 'User-assigned name for this LCR table',
is_active BOOLEAN NOT NULL DEFAULT 1,
default_carrier_set_entry_sid CHAR(36) COMMENT 'default carrier/route to use when no digit match based results are found.',
service_provider_sid CHAR(36),
account_sid CHAR(36),
PRIMARY KEY (lcr_sid)
) COMMENT='An LCR (least cost routing) table that is used by a service provider or account to make decisions about routing outbound calls when multiple carriers are available.';
CREATE TABLE password_settings
(
min_password_length INTEGER NOT NULL DEFAULT 8,
require_digit BOOLEAN NOT NULL DEFAULT false,
require_special_character BOOLEAN NOT NULL DEFAULT false
);
CREATE TABLE permissions
(
permission_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(32) NOT NULL UNIQUE ,
description VARCHAR(255),
PRIMARY KEY (permission_sid)
);
CREATE TABLE predefined_carriers
(
@@ -148,6 +219,20 @@ predefined_carrier_sid CHAR(36) NOT NULL,
PRIMARY KEY (predefined_sip_gateway_sid)
);
CREATE TABLE predefined_smpp_gateways
(
predefined_smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. ',
port INTEGER NOT NULL DEFAULT 2775 COMMENT 'smpp signaling port',
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound SMS from the gateway',
outbound BOOLEAN NOT NULL COMMENT 'i',
netmask INTEGER NOT NULL DEFAULT 32,
is_primary BOOLEAN NOT NULL DEFAULT 1,
use_tls BOOLEAN DEFAULT 0,
predefined_carrier_sid CHAR(36) NOT NULL,
PRIMARY KEY (predefined_smpp_gateway_sid)
);
CREATE TABLE products
(
product_sid CHAR(36) NOT NULL UNIQUE ,
@@ -174,6 +259,11 @@ stripe_product_id VARCHAR(56) NOT NULL,
PRIMARY KEY (account_offer_sid)
);
CREATE TABLE schema_version
(
version VARCHAR(16)
);
CREATE TABLE api_keys
(
api_key_sid CHAR(36) NOT NULL UNIQUE ,
@@ -191,7 +281,10 @@ CREATE TABLE sbc_addresses
sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
ipv4 VARCHAR(255) NOT NULL,
port INTEGER NOT NULL DEFAULT 5060,
tls_port INTEGER,
wss_port INTEGER,
service_provider_sid CHAR(36),
last_updated DATETIME,
PRIMARY KEY (sbc_address_sid)
);
@@ -205,6 +298,15 @@ tenant_fqdn VARCHAR(255) NOT NULL UNIQUE ,
PRIMARY KEY (ms_teams_tenant_sid)
) COMMENT='A Microsoft Teams customer tenant';
CREATE TABLE service_provider_limits
(
service_provider_limits_sid CHAR(36) NOT NULL UNIQUE ,
service_provider_sid CHAR(36) NOT NULL,
category ENUM('api_rate','voice_call_session', 'device','voice_call_minutes','voice_call_session_license', 'voice_call_minutes_license') NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (service_provider_limits_sid)
);
CREATE TABLE signup_history
(
email VARCHAR(255) NOT NULL,
@@ -238,9 +340,29 @@ 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),
private_network_cidr VARCHAR(8192),
log_level ENUM('info', 'debug') NOT NULL DEFAULT 'info'
);
CREATE TABLE users
(
user_sid CHAR(36) NOT NULL UNIQUE ,
@@ -260,6 +382,7 @@ email_activation_code VARCHAR(16),
email_validated BOOLEAN NOT NULL DEFAULT false,
phone_validated BOOLEAN NOT NULL DEFAULT false,
email_content_opt_out BOOLEAN NOT NULL DEFAULT false,
is_active BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (user_sid)
);
@@ -287,9 +410,21 @@ smpp_password VARCHAR(64),
smpp_enquire_link_interval INTEGER DEFAULT 0,
smpp_inbound_system_id VARCHAR(255),
smpp_inbound_password VARCHAR(64),
register_from_user VARCHAR(128),
register_from_domain VARCHAR(255),
register_public_ip_in_contact BOOLEAN NOT NULL DEFAULT false,
register_status VARCHAR(4096),
PRIMARY KEY (voip_carrier_sid)
) COMMENT='A Carrier or customer PBX that can send or receive calls';
CREATE TABLE user_permissions
(
user_permissions_sid CHAR(36) NOT NULL UNIQUE ,
user_sid CHAR(36) NOT NULL,
permission_sid CHAR(36) NOT NULL,
PRIMARY KEY (user_permissions_sid)
);
CREATE TABLE smpp_gateways
(
smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
@@ -307,7 +442,7 @@ PRIMARY KEY (smpp_gateway_sid)
CREATE TABLE phone_numbers
(
phone_number_sid CHAR(36) UNIQUE ,
number VARCHAR(32) NOT NULL UNIQUE ,
number VARCHAR(132) NOT NULL,
voip_carrier_sid CHAR(36),
account_sid CHAR(36),
application_sid CHAR(36),
@@ -320,11 +455,15 @@ 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';
@@ -357,12 +496,24 @@ account_sid CHAR(36) COMMENT 'account that this application belongs to (if null,
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ',
call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events',
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 ';
@@ -395,6 +546,15 @@ disable_cdrs BOOLEAN NOT NULL DEFAULT 0,
trial_end_date DATETIME,
deactivated_reason VARCHAR(255),
device_to_call_ratio INTEGER NOT NULL DEFAULT 5,
subspace_client_id VARCHAR(255),
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';
@@ -402,24 +562,43 @@ CREATE INDEX account_static_ip_sid_idx ON account_static_ips (account_static_ip_
CREATE INDEX account_sid_idx ON account_static_ips (account_sid);
ALTER TABLE account_static_ips ADD FOREIGN KEY account_sid_idxfk (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX account_sid_idx ON account_limits (account_sid);
ALTER TABLE account_limits ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid) ON DELETE CASCADE;
CREATE INDEX account_subscription_sid_idx ON account_subscriptions (account_subscription_sid);
CREATE INDEX account_sid_idx ON account_subscriptions (account_sid);
ALTER TABLE account_subscriptions ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE account_subscriptions ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX invite_code_idx ON beta_invite_codes (invite_code);
CREATE INDEX call_route_sid_idx ON call_routes (call_route_sid);
ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid);
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_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);
CREATE INDEX lcr_sid_idx ON lcr_routes (lcr_sid);
ALTER TABLE lcr_routes ADD FOREIGN KEY lcr_sid_idxfk (lcr_sid) REFERENCES lcr (lcr_sid);
CREATE INDEX lcr_sid_idx ON lcr (lcr_sid);
ALTER TABLE lcr ADD FOREIGN KEY default_carrier_set_entry_sid_idxfk (default_carrier_set_entry_sid) REFERENCES lcr_carrier_set_entry (lcr_carrier_set_entry_sid);
CREATE INDEX service_provider_sid_idx ON lcr (service_provider_sid);
CREATE INDEX account_sid_idx ON lcr (account_sid);
CREATE INDEX permission_sid_idx ON permissions (permission_sid);
CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_sid);
CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid);
CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid);
ALTER TABLE predefined_sip_gateways ADD FOREIGN KEY predefined_carrier_sid_idxfk (predefined_carrier_sid) REFERENCES predefined_carriers (predefined_carrier_sid);
CREATE INDEX predefined_smpp_gateway_sid_idx ON predefined_smpp_gateways (predefined_smpp_gateway_sid);
CREATE INDEX predefined_carrier_sid_idx ON predefined_smpp_gateways (predefined_carrier_sid);
ALTER TABLE predefined_smpp_gateways ADD FOREIGN KEY predefined_carrier_sid_idxfk_1 (predefined_carrier_sid) REFERENCES predefined_carriers (predefined_carrier_sid);
CREATE INDEX product_sid_idx ON products (product_sid);
CREATE INDEX account_product_sid_idx ON account_products (account_product_sid);
CREATE INDEX account_subscription_sid_idx ON account_products (account_subscription_sid);
@@ -429,14 +608,14 @@ ALTER TABLE account_products ADD FOREIGN KEY product_sid_idxfk (product_sid) REF
CREATE INDEX account_offer_sid_idx ON account_offers (account_offer_sid);
CREATE INDEX account_sid_idx ON account_offers (account_sid);
ALTER TABLE account_offers ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE account_offers ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX product_sid_idx ON account_offers (product_sid);
ALTER TABLE account_offers ADD FOREIGN KEY product_sid_idxfk_1 (product_sid) REFERENCES products (product_sid);
CREATE INDEX api_key_sid_idx ON api_keys (api_key_sid);
CREATE INDEX account_sid_idx ON api_keys (account_sid);
ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_6 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX service_provider_sid_idx ON api_keys (service_provider_sid);
ALTER TABLE api_keys ADD FOREIGN KEY service_provider_sid_idxfk (service_provider_sid) REFERENCES service_providers (service_provider_sid);
@@ -450,59 +629,72 @@ ALTER TABLE sbc_addresses ADD FOREIGN KEY service_provider_sid_idxfk_1 (service_
CREATE INDEX ms_teams_tenant_sid_idx ON ms_teams_tenants (ms_teams_tenant_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY service_provider_sid_idxfk_2 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_6 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_7 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY application_sid_idxfk_1 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX tenant_fqdn_idx ON ms_teams_tenants (tenant_fqdn);
CREATE INDEX service_provider_sid_idx ON service_provider_limits (service_provider_sid);
ALTER TABLE service_provider_limits ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid) ON DELETE CASCADE;
CREATE INDEX email_idx ON signup_history (email);
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_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid);
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_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_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX account_sid_idx ON speech_credentials (account_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_7 (account_sid) REFERENCES accounts (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);
CREATE INDEX account_sid_idx ON users (account_sid);
ALTER TABLE users ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE users ADD FOREIGN KEY account_sid_idxfk_9 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX service_provider_sid_idx ON users (service_provider_sid);
ALTER TABLE users ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE users ADD FOREIGN KEY service_provider_sid_idxfk_6 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX email_activation_code_idx ON users (email_activation_code);
CREATE INDEX voip_carrier_sid_idx ON voip_carriers (voip_carrier_sid);
CREATE INDEX account_sid_idx ON voip_carriers (account_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_9 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_10 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX service_provider_sid_idx ON voip_carriers (service_provider_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY service_provider_sid_idxfk_6 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY service_provider_sid_idxfk_7 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX user_permissions_sid_idx ON user_permissions (user_permissions_sid);
CREATE INDEX user_sid_idx ON user_permissions (user_sid);
ALTER TABLE user_permissions ADD FOREIGN KEY user_sid_idxfk (user_sid) REFERENCES users (user_sid) ON DELETE CASCADE;
ALTER TABLE user_permissions ADD FOREIGN KEY permission_sid_idxfk (permission_sid) REFERENCES permissions (permission_sid);
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);
ALTER TABLE phone_numbers ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY account_sid_idxfk_10 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY account_sid_idxfk_11 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY application_sid_idxfk_3 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX service_provider_sid_idx ON phone_numbers (service_provider_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_7 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
@@ -518,10 +710,10 @@ CREATE UNIQUE INDEX applications_idx_name ON applications (account_sid,name);
CREATE INDEX application_sid_idx ON applications (application_sid);
CREATE INDEX service_provider_sid_idx ON applications (service_provider_sid);
ALTER TABLE applications ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE applications ADD FOREIGN KEY service_provider_sid_idxfk_9 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX account_sid_idx ON applications (account_sid);
ALTER TABLE applications ADD FOREIGN KEY account_sid_idxfk_11 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE applications ADD FOREIGN KEY account_sid_idxfk_12 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE applications ADD FOREIGN KEY call_hook_sid_idxfk (call_hook_sid) REFERENCES webhooks (webhook_sid);
@@ -537,7 +729,7 @@ ALTER TABLE service_providers ADD FOREIGN KEY registration_hook_sid_idxfk (regis
CREATE INDEX account_sid_idx ON accounts (account_sid);
CREATE INDEX sip_realm_idx ON accounts (sip_realm);
CREATE INDEX service_provider_sid_idx ON accounts (service_provider_sid);
ALTER TABLE accounts ADD FOREIGN KEY service_provider_sid_idxfk_9 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE accounts ADD FOREIGN KEY service_provider_sid_idxfk_10 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE accounts ADD FOREIGN KEY registration_hook_sid_idxfk_1 (registration_hook_sid) REFERENCES webhooks (webhook_sid);
@@ -545,4 +737,6 @@ ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hoo
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid);
SET FOREIGN_KEY_CHECKS=1;
+63 -4
View File
@@ -9,6 +9,8 @@ insert into webhooks(webhook_sid, url, username, password) values('90dda62e-0ea2
insert into service_providers (service_provider_sid, name, root_domain, registration_hook_sid)
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'SP A', 'jambonz.org', '90dda62e-0ea2-47d1-8164-5bd49003476c');
insert into service_provider_limits (service_provider_limits_sid, service_provider_sid, category, quantity)
values ('a79d3ade-e0da-4461-80f3-7c73f01e18b4', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'voice_call_session', 1);
insert into accounts(account_sid, service_provider_sid, name, sip_realm, registration_hook_sid, webhook_secret)
values ('ed649e33-e771-403a-8c99-1780eabbc803', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'test account', 'jambonz.org', '90dda62e-0ea2-47d1-8164-5bd49003476c', 'foobar');
@@ -17,7 +19,8 @@ values ('f4e1848d-3ff8-40eb-b9c1-30e1ef053f94','ed649e33-e771-403a-8c99-1780eabb
insert into account_products(account_product_sid, account_subscription_sid, product_sid,quantity)
values ('f23ff996-6534-4aba-8666-4b347391eca2', 'f4e1848d-3ff8-40eb-b9c1-30e1ef053f94', 'c4403cdb-8e75-4b27-9726-7d8315e3216d', 10);
insert into voip_carriers (voip_carrier_sid, name, account_sid) values ('287c1452-620d-4195-9f19-c9814ef90d78', 'westco', 'ed649e33-e771-403a-8c99-1780eabbc803');
insert into voip_carriers (voip_carrier_sid, name, account_sid, service_provider_sid)
values ('287c1452-620d-4195-9f19-c9814ef90d78', 'westco', 'ed649e33-e771-403a-8c99-1780eabbc803', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0');
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('124a5339-c62c-4075-9e19-f4de70a96597', '287c1452-620d-4195-9f19-c9814ef90d78', '172.38.0.20', true, true);
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, port, inbound, outbound)
@@ -38,8 +41,10 @@ insert into account_subscriptions(account_subscription_sid, account_sid, pending
values ('73bbcc5d-512f-4cea-8535-9a6e3d2bd19d','d7cc37cb-d152-49ef-a51b-485f6e917089',0);
insert into account_products(account_product_sid, account_subscription_sid, product_sid,quantity)
values ('92f137f7-4bc3-4157-b096-6817e54b1874', '73bbcc5d-512f-4cea-8535-9a6e3d2bd19d', 'c4403cdb-8e75-4b27-9726-7d8315e3216d', 0);
insert into account_limits(account_limits_sid, account_sid, category, quantity) values('a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d', 'd7cc37cb-d152-49ef-a51b-485f6e917089', 'voice_call_session', 0);
insert into voip_carriers (voip_carrier_sid, name, account_sid) values ('9b1abdc7-0220-4964-bc66-32b5c70cd9ab', 'westco', 'd7cc37cb-d152-49ef-a51b-485f6e917089');
insert into voip_carriers (voip_carrier_sid, name, account_sid, service_provider_sid)
values ('9b1abdc7-0220-4964-bc66-32b5c70cd9ab', 'westco', 'd7cc37cb-d152-49ef-a51b-485f6e917089', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0');
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('12f401d9-cbb1-49e5-bd33-cefbca0badc3', '9b1abdc7-0220-4964-bc66-32b5c70cd9ab', '172.38.0.20', true, true);
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, port, inbound, outbound)
@@ -49,7 +54,8 @@ values ('1401eb72-0daf-4471-aba6-038a0a2587b3', '9b1abdc7-0220-4964-bc66-32b5c70
insert into applications (application_sid, name, account_sid, call_hook_sid, call_status_hook_sid)
values ('3b43e39f-4346-4218-8434-a53130e8be49', 'test', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', '90dda62e-0ea2-47d1-8164-5bd49003476c', '4d7ce0aa-5ead-4e61-9a6b-3daa732218b1');
insert into voip_carriers (voip_carrier_sid, name, account_sid, application_sid) values ('999c1452-620d-4195-9f19-c9814ef90d78', 'customer PBX', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', '3b43e39f-4346-4218-8434-a53130e8be49');
insert into voip_carriers (voip_carrier_sid, name, account_sid, application_sid, service_provider_sid)
values ('999c1452-620d-4195-9f19-c9814ef90d78', 'customer PBX', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', '3b43e39f-4346-4218-8434-a53130e8be49', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0');
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('888a5339-c62c-4075-9e19-f4de70a96597', '999c1452-620d-4195-9f19-c9814ef90d78', '172.38.0.21', true, false);
@@ -64,4 +70,57 @@ insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_s
values ('d458bf7a-bcea-47b2-ac96-66dfc9c5c220', '150822233*', '287c1452-620d-4195-9f19-c9814ef90d78', 'ed649e33-e771-403a-8c99-1780eabbc803');
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid)
values ('f7ad205d-b92f-4363-8160-f8b5216b40d3', '15083871234', '287c1452-620d-4195-9f19-c9814ef90d78', 'd7cc37cb-d152-49ef-a51b-485f6e917089');
values ('f7ad205d-b92f-4363-8160-f8b5216b40d3', '15083871234', '287c1452-620d-4195-9f19-c9814ef90d78', 'd7cc37cb-d152-49ef-a51b-485f6e917089');
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid, application_sid)
values ('c17d5a7d-9328-4663-92c0-f65aa8381264', '12125551212', '287c1452-620d-4195-9f19-c9814ef90d78', 'ed649e33-e771-403a-8c99-1780eabbc803', '3b43e39f-4346-4218-8434-a53130e8be49');
-- two accounts that both have the same carrier with default routing (ambiguity test)
insert into accounts (account_sid, name, service_provider_sid, webhook_secret, sip_realm)
values ('239d7d49-b3e4-4fdb-9d66-661149f717e8', 'Account B1', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'foobar', 'echo2.sip.jambonz.org');
insert into accounts (account_sid, name, service_provider_sid, webhook_secret, sip_realm)
values ('909d7d49-b3e4-4fdb-9d66-661149f717e8', 'Account B2', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'foobar', 'foxtrot.sip.jambonz.org');
insert into applications (application_sid, name, account_sid, call_hook_sid, call_status_hook_sid)
values ('8843e39f-4346-4218-8434-a53130e8be49', 'test', '239d7d49-b3e4-4fdb-9d66-661149f717e8', '90dda62e-0ea2-47d1-8164-5bd49003476c', '4d7ce0aa-5ead-4e61-9a6b-3daa732218b1');
insert into applications (application_sid, name, account_sid, call_hook_sid, call_status_hook_sid)
values ('7743e39f-4346-4218-8434-a53130e8be49', 'test', '909d7d49-b3e4-4fdb-9d66-661149f717e8', '90dda62e-0ea2-47d1-8164-5bd49003476c', '4d7ce0aa-5ead-4e61-9a6b-3daa732218b1');
insert into voip_carriers (voip_carrier_sid, name, service_provider_sid, account_sid, application_sid)
values ('731abdc7-0220-4964-bc66-32b5c70cd9ab', 'twilio-1', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '239d7d49-b3e4-4fdb-9d66-661149f717e8', '8843e39f-4346-4218-8434-a53130e8be49');
insert into voip_carriers (voip_carrier_sid, name, service_provider_sid, account_sid, application_sid)
values ('987abdc7-0220-4964-bc66-32b5c70cd9ab', 'twilio-2', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '909d7d49-b3e4-4fdb-9d66-661149f717e8', '7743e39f-4346-4218-8434-a53130e8be49');
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('664a5339-c62c-4075-9e19-f4de70a96597', '731abdc7-0220-4964-bc66-32b5c70cd9ab', '172.38.0.40', true, false);
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('554a5339-c62c-4075-9e19-f4de70a96597', '987abdc7-0220-4964-bc66-32b5c70cd9ab', '172.38.0.40', true, false);
-- voip carrier belonging to all accounts
insert into voip_carriers (voip_carrier_sid, name, service_provider_sid)
values ('voip100', 'test-voip-carrier', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0');
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('sip100', 'voip100', '172.38.0.50', true, false);
insert into voip_carriers (voip_carrier_sid, name, service_provider_sid)
values ('voip101', 'test-voip-carrier-101', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0');
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('sip101', 'voip101', '172.38.0.50', true, false);
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('sip102', 'voip101', '172.38.0.51', true, false);
insert into applications (application_sid, name, account_sid, call_hook_sid, call_status_hook_sid)
values ('app100', 'app100', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', '90dda62e-0ea2-47d1-8164-5bd49003476c', '90dda62e-0ea2-47d1-8164-5bd49003476c');
insert into applications (application_sid, name, account_sid, call_hook_sid, call_status_hook_sid)
values ('app101', 'app101', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', '90dda62e-0ea2-47d1-8164-5bd49003476c', '90dda62e-0ea2-47d1-8164-5bd49003476c');
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid, application_sid)
values ('phone100', '^100', 'voip101', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', 'app100');
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid, application_sid)
values ('phone101', '^10012', 'voip100', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', 'app101');
-- insert an invalid regex pattern, the below pattern should be ignored during pattern
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid, application_sid)
values ('phone102', '\\dkjfhmdf\\', 'voip100', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', 'app101');
-- account with a sip realm that is not associated with any voip carriers
insert into accounts (account_sid, name, service_provider_sid, webhook_secret, sip_realm)
values ('acct-100', 'Account 100', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'foobar', 'ram.sip.jambonz.org');
+1 -1
View File
@@ -23,7 +23,7 @@ services:
sbc-inbound:
ipv4_address: 172.38.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"
+1
View File
@@ -2,3 +2,4 @@ require('./docker_start');
require('./create-test-db');
require('./sip-tests');
require('./docker_stop');
require('./utils');
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="UAC with x-application-sid header">
<send retrans="500">
<![CDATA[
INVITE sip:+12125551212@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:12125551212@jambonz.org>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-pcap-carrier-success
Content-Type: application/sdp
Content-Length: [len]
X-Application-Sid: 3b43e39f-4346-4218-8434-a53130e8be49
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100" optional="true">
</recv>
<recv response="482" rtd="true" >
</recv>
</scenario>
@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<!-- This program is free software; you can redistribute it and/or -->
<!-- modify it under the terms of the GNU General Public License as -->
<!-- published by the Free Software Foundation; either version 2 of the -->
<!-- License, or (at your option) any later version. -->
<!-- -->
<!-- This program is distributed in the hope that it will be useful, -->
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
<!-- GNU General Public License for more details. -->
<!-- -->
<!-- You should have received a copy of the GNU General Public License -->
<!-- along with this program; if not, write to the -->
<!-- Free Software Foundation, Inc., -->
<!-- 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -->
<!-- -->
<!-- Sipp 'uac' scenario with pcap (rtp) play -->
<!-- -->
<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:100678@172.38.0.10:5060 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:100678@172.38.0.10:5060>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-pcap-carrier-success
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="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:100678@172.38.0.10:5060 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:100678@172.38.0.10:5060>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Max-Forwards: 70
Subject: uac-pcap-carrier-success
Content-Length: 0
]]>
</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:100678@172.38.0.10:5060 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:100678@172.38.0.10:5060>[peer_tag_param]
Call-ID: [call_id]
CSeq: 2 BYE
Subject: uac-pcap-carrier-success
Content-Length: 0
]]>
</send>
<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>
@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<!-- This program is free software; you can redistribute it and/or -->
<!-- modify it under the terms of the GNU General Public License as -->
<!-- published by the Free Software Foundation; either version 2 of the -->
<!-- License, or (at your option) any later version. -->
<!-- -->
<!-- This program is distributed in the hope that it will be useful, -->
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
<!-- GNU General Public License for more details. -->
<!-- -->
<!-- You should have received a copy of the GNU General Public License -->
<!-- along with this program; if not, write to the -->
<!-- Free Software Foundation, Inc., -->
<!-- 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -->
<!-- -->
<!-- Sipp 'uac' scenario with pcap (rtp) play -->
<!-- -->
<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:+100678@ram.sip.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:100678@ram.sip.jambonz.org>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-pcap-carrier-success
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="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:100678@ram.sip.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:100678@ram.sip.jambonz.org>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Max-Forwards: 70
Subject: uac-pcap-carrier-success
Content-Length: 0
]]>
</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:100678@ram.sip.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:100678@ram.sip.jambonz.org>[peer_tag_param]
Call-ID: [call_id]
CSeq: 2 BYE
Subject: uac-pcap-carrier-success
Content-Length: 0
]]>
</send>
<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>
+120
View File
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<!-- This program is free software; you can redistribute it and/or -->
<!-- modify it under the terms of the GNU General Public License as -->
<!-- published by the Free Software Foundation; either version 2 of the -->
<!-- License, or (at your option) any later version. -->
<!-- -->
<!-- This program is distributed in the hope that it will be useful, -->
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
<!-- GNU General Public License for more details. -->
<!-- -->
<!-- You should have received a copy of the GNU General Public License -->
<!-- along with this program; if not, write to the -->
<!-- Free Software Foundation, Inc., -->
<!-- 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -->
<!-- -->
<!-- Sipp 'uac' scenario with pcap (rtp) play -->
<!-- -->
<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@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>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-late-media
Content-Length: 0
]]>
</send>
<recv response="100" optional="true">
</recv>
<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@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: 1 ACK
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>
<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>
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<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:+15083871234@172.38.0.10 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:15083871234@172.38.0.10>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-pcap-carrier-fail-ambiguous
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>
<!-- 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="503" 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:15083871234@172.38.0.10 SIP/2.0
[last_Via]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
To: <sip:15083871234@172.38.0.10>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Max-Forwards: 70
Subject: uac-pcap-carrier-fail-ambiguous
Content-Length: 0
]]>
</send>
<!-- 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>
+2
View File
@@ -36,6 +36,8 @@
Subject: uac-pcap-carrier-success
Content-Type: application/sdp
Content-Length: [len]
X-Authenticated-User: xhoaluu@jambonz.org
X-Application-Sid: APP_ID_1
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
+27 -5
View File
@@ -24,6 +24,7 @@ function waitFor(ms) {
test('incoming call tests', async(t) => {
const {srf} = require('../app');
const { queryCdrs } = srf.locals;
let res;
try {
await connect(srf);
@@ -33,9 +34,15 @@ test('incoming call tests', async(t) => {
obj = await getJSON('http://127.0.0.1:3050/system-health');
t.ok(obj.calls === 0, 'HTTP GET /system-health works (health check)')
await sippUac('uac-late-media.xml', '172.38.0.20');
t.pass('incoming call with no SDP packet is rejected with a 488');
await sippUac('uac-did-applicationsid-loop.xml', '172.38.0.20');
t.pass('incoming call with x-application-sid header is rejected with 482');
await sippUac('uac-pcap-carrier-success.xml', '172.38.0.20');
t.pass('incoming call from carrier completed successfully');
await sippUac('uac-pcap-pbx-success.xml', '172.38.0.21');
t.pass('incoming call from account-level carrier completed successfully');
@@ -58,12 +65,27 @@ test('incoming call tests', async(t) => {
t.pass('handles in-dialog requests');
await sippUac('uac-pcap-carrier-max-call-limit.xml', '172.38.0.20');
t.pass('rejects incoming call with 503 when max calls reached')
t.pass('rejects incoming call with 503 when max calls per account reached');
await sippUac('uac-did-regex-match-vc-all-accts.xml', '172.38.0.50');
t.pass('incoming call matched by trailing wildcard *, voice gateway belongs to all accounts, with sip realm');
await sippUac('uac-did-regex-match-vc-all-accts-nosiprealm.xml', '172.38.0.51');
t.pass('incoming call matched by trailing wildcard *, voice gateway belongs to all accounts, without sip realm');
await sippUac('uac-did-regex-match-vc-all-accts-nosiprealm.xml', '172.38.0.50');
t.pass('incoming call matched by trailing wildcard *, voice gateway belongs to all accounts, without sip realm');
/* switch off this env for remaining tests (JAMBONES_HOSTING is for Saas sts) */
delete process.env.JAMBONES_HOSTING;
await sippUac('uac-pcap-carrier-fail-ambiguous.xml', '172.38.0.40');
t.pass('rejects incoming call with 503 when multiple accounts have same carrier witrh default routing')
await waitFor(10);
await waitFor(12);
const res = await queryCdrs({account_sid: 'ed649e33-e771-403a-8c99-1780eabbc803'});
console.log(`cdrs: ${JSON.stringify(res)}`);
t.ok(7 === res.total, 'successfully wrote 7 cdrs for calls');
console.log(`cdrs res.total: ${res.total}`);
//console.log(`cdrs: ${JSON.stringify(res)}`);
t.ok(7 === res.total, 'successfully wrote 8 cdrs for calls');
srf.disconnect();
t.end();
+89
View File
@@ -0,0 +1,89 @@
const test = require('tape');
const { parseHostPorts } = require('../lib/utils');
const { parseUri } = require('drachtio-srf');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
const hostports = "tls/3.70.141.74:5061,wss/3.70.141.74:8443,tcp/3.70.141.74:5060,udp/3.70.141.74:5060";
const hostportsNoTls = "wss/3.70.141.74:8443,tcp/3.70.141.74:5060,udp/3.70.141.74:5060";
const logger = { info: (args) => console.log(args) };
const srf = {
locals: {
sipAddress: '127.0.0.1'
}
};
test('utils tests - parseHostPorts', async (t) => {
try {
let obj = parseHostPorts(logger, hostports, srf);
const expected = {
tls: '3.70.141.74:5061',
wss: '3.70.141.74:8443',
tcp: '3.70.141.74:5060',
udp: '3.70.141.74:5060'
};
t.ok(obj.tls === expected.tls, 'sip endpoint tls');
t.ok(obj.wss === expected.wss, 'sip endpoint wss');
t.ok(obj.tcp === expected.tcp, 'sip endpoint tcp');
t.ok(obj.udp === expected.udp, 'sip endpoint udp');
obj = parseHostPorts(logger, hostportsNoTls.split(','), srf);
t.ok(obj.tls === '127.0.0.1:5061', 'sip endpoint tls');
t.end();
} catch (err) {
console.log(`error received: ${err}`);
t.error(err);
}
});
test('utils tests - parse URI user', async (t) => {
try {
let invalidUri = "sip:@202660.tenios.com";
const req = {
getParsedHeader: () => ({ uri: invalidUri })
};
const referTo = req.getParsedHeader('Refer-To');
let uri = parseUri(referTo.uri);
const expected = {
family: "ipv4",
scheme: "sip",
user: "",
password: undefined,
host: "202660.tenios.com",
port: NaN,
params: {},
headers: {},
}
t.ok(uri.family === expected.family, 'family eq ipv4');
t.ok(uri.scheme === expected.scheme, 'scheme eq sip');
t.ok(uri.password === expected.password, 'pw eq undefined');
t.ok(uri.host === expected.host, 'host eq 202660.tenios.com');
t.ok(uri.user === "", 'user eq empty string');
t.ok(isNaN(uri.port), 'port eq NaN');
t.ok(typeof uri.params === 'object', 'params eq object');
t.ok(typeof uri.headers === 'object', 'headers eq object');
invalidUri = "<sip:@202660.tenios.com>";
uri = parseUri(invalidUri);
/* TODO: uri can be undefined - check these conditions in call-session */
t.ok(uri === undefined, 'uri is undefined');
const validUri = "<sip:+49221578952870@202660.tenios.com>";
t.end();
} catch (err) {
console.log(`error received: ${err}`);
t.error(err);
}
});