Compare commits

...
194 Commits
Author SHA1 Message Date
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
Dave Horton acead419d5 bump version to 0.7.6 2022-08-26 20:09:15 +02:00
Dave Horton 911d208e0f when releasing media anchor from FS, if we are using SRTP on A leg we need to reinvite 2022-08-24 14:26:24 +02:00
xquanluu c824367086 feat: update time-series 0.11.12 (#45) 2022-08-19 16:17:55 +02:00
Dave Horton f39df615f2 update time-series 2022-08-19 09:59:08 +02:00
Dave Horton 58e80afc92 deps 2022-08-12 14:09:19 +02:00
Dave Horton 738f151066 Feature/sip info dtmf (#44)
* initial changes for handling SIP INFO from webrtc clients

* SIP INFO DTMF handling: if media has been released, just relay SIP INFO to FS otherwise transcode to RFC 2833
2022-08-11 14:33:01 +02:00
Dave Horton a94f25b0bd update to latest @jambonz/siprec-utils with fix for sdp version 2022-08-08 16:27:42 +02:00
Dave Horton a1542b161b update siprec-utils to increment sdp version on reinvite 2022-08-08 13:48:52 +02:00
Dave Horton 4e961491a6 Feature/siprec server (#42)
* changes to handle siprec invites

* retain xml for sending

* send multipart body for siprec

* fixup sdp

* possibly trim xml string

* fix prev commit

* fix prev commit

* still tweaking sdp

* tweaks

* tweaking

* bit of refactoring

* refactor siprec client stuff into a library

* add direction in siprec metadata

* fix
2022-08-05 10:27:50 +01:00
Dave Horton 4e5f7ae908 bugfix: MS Teams warm transfer (invite w/replaces) 2022-08-01 14:49:28 +01:00
Dave Horton 798e070127 Dockerfile: update base image 2022-07-28 12:53:31 +01:00
Dave Horton 2d350d4850 bugfix: regression - adding custom headers to refer caused preferred hostname on Refer-To to be lost 2022-07-27 11:31:29 +01:00
Dave Horton baad125924 when releasing media, use asymetric flag so that rtpengine does react to a spurious final packet from freeswitch by incorrectly sending rtp there 2022-07-26 12:26:17 +01:00
Dave Horton 73528f5ce2 bugfix #131: pass on custom headers in REFER 2022-07-18 14:58:12 +02:00
Snyk bot c8329a94f3 fix: upgrade drachtio-srf from 4.5.0 to 4.5.1 (#39)
Snyk has created this PR to upgrade drachtio-srf from 4.5.0 to 4.5.1.

See this package in npm:
https://www.npmjs.com/package/drachtio-srf

See this project in Snyk:
https://app.snyk.io/org/davehorton/project/be6a10dd-83a0-4fef-a6b9-7dea4a956a5a?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-07-13 09:17:33 +02:00
Dave Horton dceeef5549 update to latest verify-aws-sns-signature 2022-07-06 18:27:24 +02:00
Paulo Tellesandp.souza b5551fffba improve dockerfile to fix snyk security issues (#38)
Co-authored-by: p.souza <p.souza@cognigy.com>
2022-07-06 18:17:20 +02:00
Dave Horton d2b5597571 initial support for siprec recording (#36)
* initial support for siprec recording

* handle pause/resume siprec recording
2022-06-23 16:23:09 -04:00
Dave Horton c9401ab3c8 update to azure 1.22.0 2022-06-11 16:22:26 -04:00
Dave Horton cc5c712a5b update deps 2022-06-11 11:40:44 -04:00
Dave Horton a923227e4a fix test scenario 2022-05-15 10:05:57 -04:00
Dave Horton 5f7df4d135 #32 - allow wildcard matches 2022-05-07 11:17:21 -04:00
Dave Horton 7dd4d4a045 Feature/healthcheck improvements (#30)
* health check tests mysql and redis connectivity

* health check tests mysql and redis connectivity

* minor
2022-04-12 15:46:20 -04:00
Dave Horton 7382bf6bd6 bump version 2022-04-06 08:18:08 -04:00
Dave Horton abfce38150 logging 2022-04-01 15:12:44 -04:00
Dave Horton d8035c978e bugfix: error writing call counts 2022-04-01 15:09:46 -04:00
Dave Horton 91cd677ad8 write call_counts time series data tracking inbound call counts by account 2022-04-01 13:08:25 -04:00
Dave Horton 17b53438e2 track account level calls if env JAMBONES_TRACK_ACCOUNT_CALLS is set 2022-04-01 06:51:44 -04:00
Dave Horton f1e2f192b7 Bugfix/mem leak (#29)
* wait for 200 OK to BYE before closing connection to drachtio

* minor logging
2022-03-27 21:50:09 -04:00
Dave Horton 0af8bcc348 remove dlg event handlers on destroy 2022-03-27 20:23:30 -04:00
Dave Horton e067125974 de-link Dialogs so they can be GC'ed at call end 2022-03-27 19:43:39 -04:00
Dave Horton 2dadde64f4 write otel trace_id to call history 2022-03-23 09:29:22 -04:00
Dave Horton d5a1337811 bugfix: connection to drachtio was not closed on non-success response to invite (#27) 2022-03-18 14:04:52 -04:00
Snyk bot a6571134cd fix: Dockerfile to reduce vulnerabilities (#26)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-DEBIAN11-GNUTLS28-2419151
- https://snyk.io/vuln/SNYK-DEBIAN11-OPENSSL-2388380
- https://snyk.io/vuln/SNYK-DEBIAN11-OPENSSL-2426309
- https://snyk.io/vuln/SNYK-DEBIAN11-UTILLINUX-2401081
- https://snyk.io/vuln/SNYK-DEBIAN11-UTILLINUX-2401081
2022-03-18 07:54:19 -04:00
Dave Horton e0e5c75496 bump version 2022-03-08 20:15:17 -05:00
Dave Horton c08e35c261 Feature/incoming refer (#25)
* handle incoming REFER and send on to the FS

* clarity
2022-03-05 15:22:11 -05:00
Dave Horton 883c63723c update to dbhelpers with support for searching root domains to identify account 2022-02-17 20:59:17 -05:00
Dave Horton 13f78bf8d9 added pre-commit hook for linting 2022-02-14 13:18:04 -05:00
Dave Horton 55056c1771 bump version 2022-02-09 15:43:40 -05:00
Dave Horton 87ec5f8e09 update to latest @jambonz/realtimedb-helpers with support for redis username / password auth 2022-02-09 15:13:54 -05:00
Dave Horton 317280befc update deps 2022-02-09 08:22:14 -05:00
Dave Horton 7b97a0a137 0.7.2 version 2022-01-28 09:12:35 -05:00
Dave Horton 75d8381ebb Feature/rtpengine locate by dns (#22)
* initial changes to use udp and dns in K8s for rtpengine ng

* github actions

* logging

* remove port from service name before dns lookup

* query rtpengine dns immediately on startup

* bugfix prev checkin

* use dns.lookup instead of resolve4 (k8s does not appear to consult search patterns in resolve.conf with the latter)

* return all rtpengine endpoints in dns lookup

* bugfix: polling rtpengine endpoints

* fix prev commit

* catch error on rtpengine failure

* update deps

* require node 14 in gh action build

* specify node version in gh actions
2022-01-23 18:03:11 -05:00
Dave Horton 7750fdc3f2 healthcheck only in k8s 2022-01-22 21:44:14 -05:00
Dave Horton fcf90916e1 update to rtpengine-utils that reconnects ws 2022-01-19 19:27:30 -05:00
Dave Horton 4a0aa79456 no need to reset rtpengine session when releasing media, and it was causing ICE to be reset which caused Teams to drop 2022-01-17 18:39:09 -05:00
Dave Horton 1cdaae7773 use ws rather than tcp for rtpengine connection in K8S 2022-01-12 08:28:41 -05:00
Dave Horton 82ac86389d add support for tcp connections to rtpengine, needed for K8S (#21)
* add support for tcp connections to rtpengine, needed for K8S

* update to correct version of @jambonz/rtpengine-utils

* update deps
2022-01-11 15:42:49 -05:00
Dave Horton e9184ad208 add support for using ws to connect to rtpengine 2022-01-10 22:00:04 -05:00
Dave Horton 804bb890b5 JAMBONES_NETWORK_CIDR not needed for K8S (#20) 2022-01-09 14:59:00 -05:00
Dave Horton a892a87eb5 K8s changes (#19)
* K8S changes

* k8s: test explicit dns lookup of service

* bugfix prev commit

* typo

* k8s: more dns

* k8s: more dns

* k8s: more dns fun

* k8s cleanup

* k8s: user service for rtpengine location

* k8s cleanup

* typo

* change env name for fs in k8s

* change k8s service name for feature server

* add support for outbound connection mode

* k8s change for outbound

* minor

* bugfix: drachtio connection was dropped after successful connect

* drop drachtio connection on call end

* Dockerfile

* k8s pre-stop hook

* actual hook committed

* make hjook executable

* dockerfile change

* time series fix

* bugfix: teams transfer using replaces
2022-01-06 12:37:49 -05:00
Dave Horton 56205dc852 bump version 2021-12-21 09:41:01 -05:00
Dave Horton 927b7be637 SIGTERM handler to remove entry from active-sip 2021-12-20 16:09:34 -05:00
Dave Horton 63b482e562 bugfix: special case of single-tenant system 2021-12-20 12:23:04 -05:00
Dave Horton a7d047a7e8 remove unneeded refs 2021-12-20 10:09:51 -05:00
Dave Horton ea4f5ea0a8 bugfix: incoming call to accounts.sip_realm did not look for SP-level carriers 2021-12-20 09:54:21 -05:00
Dave Horton 13d8ee8c3c version bump, add docker publish 2021-12-13 09:52:41 -05:00
Dave Horton c717470c5e version bump 2021-12-02 19:20:57 -05:00
Dave Horton f16598e144 Feature/sip refer (#18)
* support for sip refer to transfer an incoming call

* when sending REFER for call transfer, format Refer-To with carrier trunk if applicable

* better handling of e164 on Refer-To header
2021-11-20 11:41:12 -05:00
Dave Horton 8584020d4c add support for proxies that add X-Forwarded-For 2021-11-05 09:29:12 -04:00
Dave Horton 0b1b49bd59 Dockerfile 2021-11-04 12:57:15 -04:00
Dave Horton e22b2ae63f version bump 2021-11-03 13:51:50 -04:00
Dave Horton 2a5200673d bump version 2021-10-21 13:08:53 -04:00
Dave Horton 13ca308df7 bump version 2021-10-21 13:01:04 -04:00
Dave Horton 981788dc58 Feature/minimal media anchoring (#11)
* add support for sitting behind a sip proxy that adds X-Forwarded-For header

* feature: release media from freeswitch

* handle mute/unmute

* fix for relaying INFO

* support for relaying dtmf via SIP INFO to FS

* deps
2021-10-21 12:00:02 -04:00
Dave Horton ca4599da22 bugfix: autoscaling 2021-10-02 17:51:04 -04:00
Dave Horton 04c4ad5975 add support for AWS autoscaling (#10) 2021-10-02 12:41:52 -04:00
Dave Horton 5267b6f9a4 deps 2021-10-01 15:37:41 -04:00
Dave Horton 2fa1f08244 slight change to data format written to redis for set of active sbcs 2021-10-01 15:37:07 -04:00
Dave Horton 03cb1cceb5 maintain a redis set of active SBC SIP servers, under {prefix}:active-sip 2021-09-29 18:28:37 -04:00
Dave Horton 9b78a818b5 bugfix: reinvite handling mixed up the public-private direction in rtpengine offer 2021-08-09 15:44:23 -04:00
Dave Horton e6a90a32ad if we get re-invite with no SDP (looking at you, BT) just respond with current offer 2021-08-03 10:41:41 -04:00
Dave Horton fb21fd2333 brackets around From header 2021-07-28 16:16:47 -04:00
Dave Horton 819fecf455 LICENSE 2021-07-21 12:40:25 -04:00
Dave Horton b0406287f5 fix tests 2021-06-29 12:47:30 -04:00
Dave Horton e63820ec2c bugfix: add only external addresses to sbc_addresses on startup 2021-06-29 11:39:28 -04:00
32 changed files with 9585 additions and 3059 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
"jsx": false,
"modules": false
},
"ecmaVersion": 2018
"ecmaVersion": 2020
},
"plugins": ["promise"],
"rules": {
+54
View File
@@ -0,0 +1,54 @@
name: Docker
on:
push:
branches:
- main
tags:
- '*'
jobs:
push:
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: prepare tag
id: prepare_tag
run: |
IMAGE_ID=jambonz/sbc-inbound
# 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//')
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
- 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
+17
View File
@@ -0,0 +1,17 @@
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18.x
- run: npm ci
- run: npm run jslint
- run: npm test
-19
View File
@@ -1,19 +0,0 @@
name: CI
on:
push:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- run: npm install
- run: npm run jslint
- run: npm test
+1 -1
View File
@@ -1,7 +1,7 @@
# Logs
logs
*.log
.vscode/
# Runtime data
pids
*.pid
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run jslint
+18 -11
View File
@@ -1,16 +1,23 @@
FROM node:alpine as builder
RUN apk update && apk add --no-cache python make g++
WORKDIR /opt/app/
COPY package.json ./
RUN npm install
RUN npm prune
FROM --platform=linux/amd64 node:18.15-alpine3.16 as base
FROM node:alpine as app
WORKDIR /opt/app
COPY . /opt/app
COPY --from=builder /opt/app/node_modules ./node_modules
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
WORKDIR /opt/app/
FROM base as build
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
FROM base
COPY --from=build /opt/app /opt/app/
ARG NODE_ENV
ENV NODE_ENV $NODE_ENV
CMD [ "npm", "start" ]
CMD [ "node", "app.js" ]
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 jambonz
Copyright (c) 2021 Drachtio Communications Services, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+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
```
{
+224 -38
View File
@@ -3,9 +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({
@@ -13,22 +21,31 @@ const opts = Object.assign({
}, {level: process.env.JAMBONES_LOGLEVEL || 'info'});
const logger = require('pino')(opts);
const {
writeCallCount,
writeCallCountSP,
writeCallCountApp,
queryCdrs,
writeCdrs,
writeAlerts,
AlertType
} = require('@jambonz/time-series')(logger, {
host: process.env.JAMBONES_TIME_SERIES_HOST,
port: process.env.JAMBONES_TIME_SERIES_PORT || 8086,
commitSize: 50,
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 {LifeCycleEvents} = require('./lib/constants');
const setNameRtp = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-rtp`;
const rtpServers = [];
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-sip`;
const {
pool,
ping,
lookupAuthHook,
lookupSipGatewayBySignalingAddress,
addSbcAddress,
@@ -36,22 +53,38 @@ const {
lookupAppByTeamsTenant,
lookupAccountBySipRealm,
lookupAccountBySid,
lookupAccountCapacitiesBySid
lookupAccountCapacitiesBySid,
queryCallLimits,
lookupClientByAccountAndUsername
} = 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,
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
}, logger);
const {createSet, retrieveSet, incrKey, decrKey} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
const {
client: redisClient,
createSet,
retrieveSet,
addToSet,
removeFromSet,
incrKey,
decrKey} = require('@jambonz/realtimedb-helpers')({}, logger);
const {getRtpEngine, setRtpEngines} = require('@jambonz/rtpengine-utils')([], logger, {emitter: stats});
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,
dtmfListenPort: process.env.DTMF_LISTEN_PORT || 22224,
protocol: ngProtocol
});
srf.locals = {...srf.locals,
stats,
writeCallCount,
writeCallCountSP,
writeCallCountApp,
queryCdrs,
writeCdrs,
writeAlerts,
@@ -60,13 +93,16 @@ srf.locals = {...srf.locals,
getRtpEngine,
dbHelpers: {
pool,
ping,
lookupAuthHook,
lookupSipGatewayBySignalingAddress,
lookupAccountByPhoneNumber,
lookupAppByTeamsTenant,
lookupAccountBySid,
lookupAccountBySipRealm,
lookupAccountCapacitiesBySid
lookupAccountCapacitiesBySid,
queryCallLimits,
lookupClientByAccountAndUsername
},
realtimeDbHelpers: {
createSet,
@@ -75,33 +111,102 @@ srf.locals = {...srf.locals,
retrieveSet
}
};
srf.locals.getFeatureServer = require('./lib/fs-tracking')(srf, logger);
const {
getSPForAccount,
wasOriginatedFromCarrier,
getApplicationForDidAndCarrier,
getOutboundGatewayForRefer,
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;
const {
initLocals,
handleSipRec,
identifyAccount,
checkLimits,
challengeDeviceCalls
} = require('./lib/middleware')(srf, logger);
const CallSession = require('./lib/call-session');
if (process.env.DRACHTIO_HOST) {
srf.connect({host: process.env.DRACHTIO_HOST, port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET });
srf.on('connect', (err, hp) => {
const last = hp.split(',').pop();
const arr = /^(.*)\/(.*):(\d+)$/.exec(last);
logger.info(`connected to drachtio listening on ${hp}: adding ${arr[2]} to sbc_addresses table`);
srf.locals.sipAddress = arr[2];
if (process.env.DRACHTIO_HOST && !process.env.K8S) {
const cidrs = process.env.JAMBONES_NETWORK_CIDR
.split(',')
.map((s) => s.trim());
const matcher = new CIDRMatcher(cidrs);
/* don't add IP to the general SBC table if this is a static IP for a single account */
if (!process.env.SBC_ACCOUNT_SID) {
addSbcAddress(arr[2]);
srf.connect({host: process.env.DRACHTIO_HOST, 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');
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])) {
logger.info(`adding sbc public address to database: ${arr[2]}`);
srf.locals.sipAddress = arr[2];
if (!process.env.SBC_ACCOUNT_SID) addSbcAddress(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;
srf.locals.addToRedis = () => addToSet(setName, hostport);
srf.locals.removeFromRedis = () => removeFromSet(setName, hostport);
srf.locals.addToRedis();
}
}
});
}
else {
srf.on('listening', () => {
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;
}
}
}
});
}
if (process.env.NODE_ENV === 'test') {
srf.on('error', (err) => {
@@ -110,7 +215,13 @@ if (process.env.NODE_ENV === 'test') {
}
/* install middleware */
srf.use('invite', [initLocals, identifyAccount, checkLimits, challengeDeviceCalls]);
srf.use('invite', [
initLocals,
handleSipRec,
identifyAccount,
checkLimits,
challengeDeviceCalls
]);
srf.invite((req, res) => {
if (req.has('Replaces')) {
@@ -133,35 +244,80 @@ srf.use((req, res, next, err) => {
res.send(500);
});
/* update call stats periodically */
setInterval(() => {
stats.gauge('sbc.sip.calls.count', activeCallIds.size, ['direction:inbound']);
}, 20000);
if (process.env.K8S || process.env.HTTP_PORT) {
const PORT = process.env.HTTP_PORT || 3000;
const healthCheck = require('@jambonz/http-health-check');
const arrayCompare = (a, b) => {
if (a.length !== b.length) return false;
const uniqueValues = new Set([...a, ...b]);
for (const v of uniqueValues) {
const aCount = a.filter((e) => e === v).length;
const bCount = b.filter((e) => e === v).length;
if (aCount !== bCount) return false;
}
return true;
const getCount = () => srf.locals.activeCallIds.size;
createHealthCheckApp(PORT, logger)
.then((app) => {
healthCheck({
app,
logger,
path: '/',
fn: getCount
});
healthCheck({
app,
logger,
path: '/system-health',
fn: systemHealth.bind(null, redisClient, ping, getCount)
});
return;
})
.catch((err) => {
logger.error({err}, 'Error creating health check server');
});
}
if ('test' !== process.env.NODE_ENV) {
/* update call stats periodically */
setInterval(() => {
stats.gauge('sbc.sip.calls.count', activeCallIds.size,
['direction:inbound', `instance_id:${process.env.INSTANCE_ID || 0}`]);
}, 20000);
}
const lookupRtpServiceEndpoints = (lookup, serviceName) => {
lookup(serviceName, {family: 4, all: true}, (err, addresses) => {
if (err) {
logger.error({err}, `Error looking up ${serviceName}`);
return;
}
logger.debug({addresses, rtpServers}, `dns lookup for ${serviceName} returned`);
const addrs = addresses.map((a) => a.address);
if (!equalsIgnoreOrder(addrs, rtpServers)) {
rtpServers.length = 0;
Array.prototype.push.apply(rtpServers, addrs);
logger.info({rtpServers}, 'rtpserver endpoints have been updated');
setRtpEngines(rtpServers.map((a) => `${a}:${ngPort}`));
}
});
};
/* update rtpengines periodically */
if (process.env.JAMBONES_RTPENGINES) {
if (process.env.K8S_RTPENGINE_SERVICE_NAME) {
/* poll dns for endpoints every so often */
const arr = /^(.*):(\d+)$/.exec(process.env.K8S_RTPENGINE_SERVICE_NAME);
const svc = arr[1];
logger.info(`rtpengine(s) will be found at dns name: ${svc}`);
const {lookup} = require('dns');
lookupRtpServiceEndpoints(lookup, svc);
setInterval(lookupRtpServiceEndpoints.bind(null, lookup, svc), process.env.RTPENGINE_DNS_POLL_INTERVAL || 10000);
}
else if (process.env.JAMBONES_RTPENGINES) {
/* static list of rtpengines */
setRtpEngines([process.env.JAMBONES_RTPENGINES]);
}
else {
/* poll redis periodically for rtpengines that have registered via OPTIONS ping */
const getActiveRtpServers = async() => {
try {
const set = await retrieveSet(setNameRtp);
const newArray = Array.from(set);
logger.debug({newArray, rtpServers}, 'getActiveRtpServers');
if (!arrayCompare(newArray, rtpServers)) {
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);
}
@@ -169,11 +325,41 @@ else {
logger.error({err}, 'Error setting new rtpengines');
}
};
setInterval(() => {
getActiveRtpServers();
}, 30000);
getActiveRtpServers();
}
const {lifecycleEmitter} = require('./lib/autoscale-manager')(logger);
/* if we are scaling in, check every so often if call count has gone to zero */
setInterval(async() => {
if (lifecycleEmitter.operationalState === LifeCycleEvents.ScaleIn) {
if (0 === activeCallIds.size) {
logger.info('scale-in complete now that calls have dried up');
lifecycleEmitter.scaleIn();
}
}
}, 20000);
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}`);
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};
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env node
const bent = require('bent');
const getJSON = bent('json');
const PORT = process.env.HTTP_PORT || 3000;
const sleep = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
(async function() {
try {
do {
const obj = await getJSON(`http://127.0.0.1:${PORT}/`);
const {calls} = obj;
if (calls === 0) {
console.log('no calls on the system, we can exit');
process.exit(0);
}
else {
console.log(`waiting for ${calls} to exit..`);
}
await sleep(10000);
} while (1);
} catch (err) {
console.error(err, 'Error querying health endpoint');
process.exit(-1);
}
})();
+1 -1
View File
@@ -3,6 +3,6 @@
"DTLS": "off",
"SDES": "off",
"ICE": "remove",
"flags": ["media handover"],
"flags": ["media handover", "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"],
"flags": ["generate mid", "SDES-no", "media handover", "port latching"],
"rtcp-mux": ["require"]
},
"teams": {
"transport-protocol": "RTP/SAVP",
"ICE": "force",
"ICE": "default",
"SDES": "off",
"flags": ["generate mid", "SDES-no", "media handover"],
"flags": ["generate mid", "SDES-no", "media handover", "port latching"],
"rtcp-mux": ["accept"]
}
}
+66
View File
@@ -0,0 +1,66 @@
const noopLogger = {info: () => {}, error: () => {}};
const {LifeCycleEvents} = require('./constants');
const Emitter = require('events');
module.exports = (logger) => {
logger = logger || noopLogger;
// listen for SNS lifecycle changes
let lifecycleEmitter = new Emitter();
lifecycleEmitter.dryUpCalls = false;
if (process.env.AWS_SNS_TOPIC_ARM) {
(async function() {
try {
lifecycleEmitter = await require('./aws-sns-lifecycle')(logger);
lifecycleEmitter
.on(LifeCycleEvents.ScaleIn, async() => {
logger.info('AWS scale-in notification: begin drying up calls');
lifecycleEmitter.dryUpCalls = true;
lifecycleEmitter.operationalState = LifeCycleEvents.ScaleIn;
const {srf} = require('..');
const {activeCallIds, removeFromRedis} = srf.locals;
/* remove our private IP from the set of active SBCs so rtp and fs know we are gone */
removeFromRedis();
/* if we have zero calls, we can complete the scale-in right now */
const calls = activeCallIds.size;
if (0 === calls) {
logger.info('scale-in can complete immediately as we have no calls in progress');
lifecycleEmitter.completeScaleIn();
}
else {
logger.info(`${calls} calls in progress; scale-in will complete when they are done`);
}
})
.on(LifeCycleEvents.StandbyEnter, () => {
lifecycleEmitter.dryUpCalls = true;
const {srf} = require('..');
const {removeFromRedis} = srf.locals;
removeFromRedis();
logger.info('AWS enter pending state notification: begin drying up calls');
})
.on(LifeCycleEvents.StandbyExit, () => {
lifecycleEmitter.dryUpCalls = false;
const {srf} = require('..');
const {addToRedis} = srf.locals;
addToRedis();
logger.info('AWS exit pending state notification: re-enable calls');
});
} catch (err) {
logger.error({err}, 'Failure creating SNS notifier, lifecycle events will be disabled');
}
})();
}
else if (process.env.K8S) {
lifecycleEmitter.scaleIn = () => process.exit(0);
}
return {lifecycleEmitter};
};
+218
View File
@@ -0,0 +1,218 @@
const Emitter = require('events');
const bent = require('bent');
const assert = require('assert');
const PORT = process.env.AWS_SNS_PORT || 3010;
const {LifeCycleEvents} = require('./constants');
const express = require('express');
const app = express();
const getString = bent('string');
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');
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 {
const parsedBody = JSON.parse(req.body);
this.logger.debug({headers: req.headers, body: parsedBody}, 'Received HTTP POST from AWS');
if (!validatePayload(parsedBody)) {
this.logger.info('incoming AWS SNS HTTP POST failed signature validation');
return res.sendStatus(403);
}
this.logger.debug('incoming HTTP POST passed validation');
res.sendStatus(200);
switch (parsedBody.Type) {
case 'SubscriptionConfirmation':
const response = await getString(parsedBody.SubscribeURL);
const result = await parser.parseStringPromise(response);
this.subscriptionArn = result.ConfirmSubscriptionResponse.ConfirmSubscriptionResult[0].SubscriptionArn[0];
this.subscriptionRequestId = result.ConfirmSubscriptionResponse.ResponseMetadata[0].RequestId[0];
this.logger.info({
subscriptionArn: this.subscriptionArn,
subscriptionRequestId: this.subscriptionRequestId
}, 'response from SNS SubscribeURL');
const data = await this.describeInstance();
this.lifecycleState = data.AutoScalingGroups[0].Instances[0].LifecycleState;
this.emit('SubscriptionConfirmation', {publicIp: this.publicIp});
break;
case 'Notification':
if (parsedBody.Subject.startsWith('Auto Scaling: Lifecycle action \'TERMINATING\'')) {
const msg = JSON.parse(parsedBody.Message);
if (msg.EC2InstanceId === this.instanceId) {
this.logger.info('SnsNotifier - begin scale-in operation');
this.scaleInParams = {
AutoScalingGroupName: msg.AutoScalingGroupName,
LifecycleActionResult: 'CONTINUE',
LifecycleActionToken: msg.LifecycleActionToken,
LifecycleHookName: msg.LifecycleHookName
};
this.operationalState = LifeCycleEvents.ScaleIn;
this.emit(LifeCycleEvents.ScaleIn);
this.unsubscribe();
}
else {
this.logger.debug(`SnsNotifier - instance ${msg.EC2InstanceId} is scaling in (not us)`);
}
}
break;
default:
this.logger.info(`unhandled SNS Post Type: ${parsedBody.Type}`);
}
} catch (err) {
this.logger.error({err}, 'Error processing SNS POST request');
if (!res.headersSent) res.sendStatus(500);
}
}
async init() {
try {
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.logger.info({
instanceId: this.instanceId,
publicIp: this.publicIp
}, 'retrieved AWS instance data');
// start listening
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(express.text());
app.post('/', this._handlePost.bind(this));
app.use((err, req, res, next) => {
this.logger.error(err, 'burped error');
res.status(err.status || 500).json({msg: err.message});
});
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');
}
}
async subscribe() {
try {
const params = {
Protocol: 'http',
TopicArn: process.env.AWS_SNS_TOPIC_ARM,
Endpoint: this.snsEndpoint
};
const response = await snsClient.send(new SubscribeCommand(params));
this.logger.info({response}, `response to SNS subscribe to ${process.env.AWS_SNS_TOPIC_ARM}`);
} catch (err) {
this.logger.error({err}, `Error subscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARM}`);
}
}
async unsubscribe() {
if (!this.subscriptionArn) throw new Error('SnsNotifier#unsubscribe called without an active subscription');
try {
const params = {
SubscriptionArn: this.subscriptionArn
};
const response = await snsClient.send(new UnsubscribeCommand(params));
this.logger.info({response}, `response to SNS unsubscribe to ${process.env.AWS_SNS_TOPIC_ARM}`);
} catch (err) {
this.logger.error({err}, `Error unsubscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARM}`);
}
}
completeScaleIn() {
assert(this.scaleInParams);
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');
autoScalingClient.send(new DescribeAutoScalingGroupsCommand({
InstanceIds: [this.instanceId]
}))
.then((data) => {
this.logger.info({data}, 'SnsNotifier: describeInstance');
return resolve(data);
})
.catch((err) => {
this.logger.error({err}, 'Error describing instances');
reject(err);
});
});
}
}
module.exports = async function(logger) {
const notifier = new SnsNotifier(logger);
await notifier.init();
await notifier.subscribe();
process.on('SIGHUP', async() => {
try {
const data = await notifier.describeInstance();
const state = data.AutoScalingGroups[0].Instances[0].LifecycleState;
if (state !== notifier.lifecycleState) {
notifier.lifecycleState = state;
switch (state) {
case 'Standby':
notifier.emit(LifeCycleEvents.StandbyEnter);
break;
case 'InService':
notifier.emit(LifeCycleEvents.StandbyExit);
break;
}
}
} catch (err) {
console.error(err);
}
});
return notifier;
};
+713 -63
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
{
"LifeCycleEvents" : {
"ScaleIn": "scale-in",
"StandbyEnter": "standby-enter",
"StandbyExit": "standby-exit"
}
}
+283 -50
View File
@@ -3,21 +3,37 @@ 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
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
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
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,
@@ -25,21 +41,49 @@ 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 vc.is_active = 1
AND sg.inbound = 1
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
WHERE number = ?
AND voip_carrier_sid = ?`;
const gatewayMatchesSourceAddress = (source_address, gw) => {
const sqlQueryAllDidsForCarrier = `
SELECT * FROM phone_numbers
WHERE voip_carrier_sid = ?`;
const sqlSelectOutboundGatewayForCarrier = `
SELECT ipv4, port, e164_leading_plus
FROM sip_gateways sg, voip_carriers vc
WHERE sg.voip_carrier_sid = ?
AND sg.voip_carrier_sid = vc.voip_carrier_sid
AND outbound = 1`;
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
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;
};
@@ -48,13 +92,47 @@ 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]);
if (0 === r.length) return null;
/* if multiple, prefer a DNS name */
const hasDns = r.find((row) => row.ipv4.match(/^[A-Za-z]/));
return hasDns /* || r[0] */;
} catch (err) {
logger.error({err}, 'getOutboundGatewayForRefer');
}
};
const getApplicationForDidAndCarrier = async(req, voip_carrier_sid) => {
const did = normalizeDID(req.calledNumber);
const did = normalizeDID(req.calledNumber) || 'anonymous';
try {
/* straight DID match */
const [r] = await pp.query(sqlQueryApplicationByDid, [did, voip_carrier_sid]);
if (0 === r.length) return null;
return r[0].application_sid;
if (r.length) return r[0].application_sid;
/* wildcard / regex match */
const [r2] = await pp.query(sqlQueryAllDidsForCarrier, [voip_carrier_sid]);
const match = 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;
return null;
} catch (err) {
logger.error({err}, 'getApplicationForDidAndCarrier');
}
@@ -64,67 +142,222 @@ module.exports = (srf, logger) => {
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 selected = gw.find(gatewayMatchesSourceAddress.bind(null, logger, req.source_address));
if (selected) {
const sql =
`SELECT application_sid FROM phone_numbers WHERE number = '${did}'
AND voip_carrier_sid = '${selected.voip_carrier_sid}'
AND account_sid = '${a[0].account_sid}'`;
logger.debug({selected, sql, did}, 'looking up DID');
const [r] = await pp.query(sql);
if (0 === r.length) {
/* came from a carrier, but number is not provisioned */
return {fromCarrier: true};
}
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,
account_sid: r[0].account_sid,
application_sid: r[0].application_sid,
account: accounts[0]
gateway: selected,
service_provider_sid: a[0].service_provider_sid,
account_sid: a[0].account_sid,
application_sid: r[0]?.application_sid || selected.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 [gw] = await pp.query(sqlSelectAllCarriersForAccountByRealm, uri.host);
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;
if (isDotDecimal && process.env.JAMBONES_HOSTING) {
if (!process.env.SBC_ACCOUNT_SID) return failure;
/* 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;
const service_provider_sid = await getSPForAccount(process.env.SBC_ACCOUNT_SID);
return {
fromCarrier: true,
gateway: selected,
account_sid: a[0].account_sid,
application_sid: selected.application_sid,
account: a[0]
gateway: r[0],
account_sid: process.env.SBC_ACCOUNT_SID,
service_provider_sid
};
}
else {
/* find all carrier entries that have an inbound gateway matching the source IP */
const [gw] = await pp.query(sqlSelectAllGatewaysForSP);
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
};
});
/* remove duplicates, winnow down to voip_carriers, not gateways */
matches = [...new Set(matches.map(JSON.stringify))].map(JSON.parse);
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) {
/* 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,
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;
};
return {
wasOriginatedFromCarrier,
getApplicationForDidAndCarrier
getApplicationForDidAndCarrier,
getOutboundGatewayForRefer,
getSPForAccount,
getApplicationBySid
};
};
+17 -6
View File
@@ -1,4 +1,8 @@
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const assert = require('assert');
assert.ok(!process.env.K8S || process.env.K8S_FEATURE_SERVER_SERVICE_NAME,
'when running in Kubernetes, an env var K8S_FEATURE_SERVER_SERVICE_NAME is required');
module.exports = (srf, logger) => {
const {retrieveSet, createSet} = srf.locals.realtimeDbHelpers;
@@ -10,13 +14,20 @@ module.exports = (srf, logger) => {
return async() => {
try {
const fs = await retrieveSet(setName);
if (0 === fs.length) {
logger.info('No available feature servers to handle incoming call');
return;
if (process.env.K8S) {
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);
if (0 === fs.length) {
logger.info('No available feature servers to handle incoming call');
return;
}
logger.debug({fs}, `retrieved ${setName}`);
return fs[idx++ % fs.length];
}
logger.debug({fs}, `retrieved ${setName}`);
return fs[idx++ % fs.length];
} catch (err) {
logger.error({err}, `Error retrieving ${setName}`);
}
+180 -85
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,59 +22,31 @@ 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 {wasOriginatedFromCarrier, getApplicationForDidAndCarrier} = require('./db-utils')(srf, logger);
const initLocals = (req, res, next) => {
req.locals = req.locals || {};
req.locals.cdr = initCdr(req);
const callId = req.get('Call-ID');
req.locals = req.locals || {callId};
/* 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')) {
const original_source_address = req.get('X-Forwarded-For') || req.get('X-Subspace-Forwarded-For');
logger.info({
callId: req.get('Call-ID'),
original_source_address,
proxy_source_address: req.source_address,
}, 'overwriting source address for proxied SIP INVITE');
req.source_address = original_source_address;
}
req.locals.cdr = initCdr(req);
req.on('cancel', () => {
logger.info({callId}, 'caller hungup before connecting to feature server');
req.canceled = true;
@@ -100,10 +72,45 @@ module.exports = function(srf, logger) {
next();
};
const handleSipRec = async(req, res, next) => {
const {callId} = req.locals;
if (Array.isArray(req.payload) && req.payload.length > 1) {
const sdp = req.payload
.find((p) => p.type === 'application/sdp')
.content;
if (!sdp) {
logger.error({callId}, 'No SDP in multipart sdp');
return res.send(503);
}
const xml = req.payload.find((p) => p.type !== 'application/sdp');
const endPos = xml.content.indexOf('</recording>');
xml.content = endPos !== -1 ?
`${xml.content.substring(0, endPos + 12)}` :
xml.content;
logger.debug({callId, xml}, 'incoming call with SIPREC body');
req.locals = {...req.locals, sdp, siprec: true, xml};
}
else req.locals = {...req.locals, sdp: req.body};
next();
};
const identifyAccount = async(req, res, next) => {
try {
const {fromCarrier, gateway, account_sid, application_sid, account} = await wasOriginatedFromCarrier(req);
const {siprec, callId} = req.locals;
const {getSPForAccount, wasOriginatedFromCarrier, getApplicationForDidAndCarrier, stats} = req.srf.locals;
const startAt = process.hrtime();
const {
fromCarrier,
gateway,
account_sid,
application_sid,
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
@@ -111,50 +118,73 @@ 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');
/* check for phone number level routing */
const sid = application_sid || await getApplicationForDidAndCarrier(req, gateway.voip_carrier_sid);
let sid;
if (siprec) {
if (!account.siprec_hook_sid) {
logger.info({callId}, 'identifyAccount: rejecting call because SIPREC hook has not been provisioned');
return res.send(404);
}
sid = account.siprec_hook_sid;
}
else {
/* check for phone number level routing */
sid = application_sid || await getApplicationForDidAndCarrier(req, gateway.voip_carrier_sid);
}
req.locals = {
originator: 'trunk',
carrier: gateway.name,
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);
if (!app) {
stats.increment('sbc.terminations', ['sipStatus:404']);
return res.send(404, {headers: {'X-Reason': 'no configured application'}});
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) {
stats.increment('sbc.terminations', ['sipStatus:404']);
return res.send(404);
res.send(404);
return req.srf.endSession(req);
}
/* if this is a dedicated SBC (static IP) only take calls for that account's sip realm */
@@ -163,21 +193,33 @@ module.exports = function(srf, logger) {
`identifyAccount: static IP for ${process.env.SBC_ACCOUNT_SID} but call for ${account.account_sid}`);
stats.increment('sbc.terminations', ['sipStatus:404']);
delete req.locals.cdr;
return res.send(404);
res.send(404);
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']);
@@ -189,7 +231,11 @@ 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
});
next();
} catch (err) {
@@ -200,30 +246,36 @@ module.exports = function(srf, logger) {
};
const checkLimits = async(req, res, next) => {
if (!process.env.JAMBONES_HOSTING) 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 {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);
/* decrement count if INVITE is later rejected */
res.once('end', ({status}) => {
res.once('end', async({status}) => {
if (status > 200) {
decrKey(key)
.then((count) => {
logger.debug({key}, `after rejection there are ${count} active calls for this account`);
debug({key}, `after rejection there are ${count} active calls for this account`);
return;
})
.catch((err) => logger.error({err}, 'checkLimits: decrKey err'));
nudgeCallCounts(logger, {
service_provider_sid,
account_sid,
application_sid
}, decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
.catch((err) => logger.error(err, 'Error decrementing call counts'));
}
});
try {
/* increment the call count */
const calls = await incrKey(key);
const {callsSP, calls} = await nudgeCallCounts(logger, {
service_provider_sid,
account_sid,
application_sid
}, 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 ?
@@ -232,25 +284,66 @@ 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'));
return res.send(503, 'Maximum Calls In Progress');
if (process.env.JAMBONES_HOSTING) {
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: calls
}).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 (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: callsSP
}).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) {
stats.increment('sbc.terminations', ['sipStatus:500']);
logger.error({err}, 'error checking limits error for inbound call');
res.send(500);
req.srf.endSession(req);
}
};
@@ -258,16 +351,18 @@ 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`);
res.send(500);
req.srf.endSession(req);
}
};
return {
initLocals,
handleSipRec,
challengeDeviceCalls,
identifyAccount,
checkLimits
+178 -8
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) => {
@@ -12,19 +15,29 @@ const getAppserver = (srf) => {
};
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 srctOpts = srcIsUsingSrtp ? srtpOpts : rtpCharacteristics;
const srtpOpts = teams ? srtpCopy['teams'] : srtpCopy['default'];
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCopy;
const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCopy;
/* 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');
}
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',
...(process.env.JAMBONES_ACCEPT_G729 && { codec: { mask: 'g729', transcode: 'pcmu'}})
};
return {
common,
uas: {
tag: from.params.tag,
mediaOpts: srctOpts
mediaOpts: srcOpts
},
uac: {
tag: null,
@@ -33,11 +46,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+)$/;
@@ -45,11 +63,163 @@ const normalizeDID = (tel) => {
return arr ? arr[1] : tel;
};
const equalsIgnoreOrder = (a, b) => {
if (a.length !== b.length) return false;
const uniqueValues = new Set([...a, ...b]);
for (const v of uniqueValues) {
const aCount = a.filter((e) => e === v).length;
const bCount = b.filter((e) => e === v).length;
if (aCount !== bCount) return false;
}
return true;
};
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 app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
return new Promise((resolve, reject) => {
const server = doListen(logger, app, port, resolve);
server.on('error', handleErrors.bind(null, logger, app, resolve, reject));
});
};
const nudgeCallCounts = async(logger, sids, nudgeOperator, writers) => {
const {service_provider_sid, account_sid, application_sid} = sids;
const {writeCallCount, writeCallCountSP, writeCallCountApp} = writers;
const nudges = [];
const writes = [];
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) => {
if (process.env.PRIVATE_VOIP_NETWORK_CIDR) {
const matcher = new CIDRMatcher(process.env.PRIVATE_VOIP_NETWORK_CIDR.split(','));
return matcher.contains(ip);
}
return false;
};
module.exports = {
isWSS,
SdpWantsSrtp,
SdpWantsSDES,
getAppserver,
makeRtpEngineOpts,
makeCallCountKey,
normalizeDID
makeAccountCallCountKey,
makeSPCallCountKey,
makeAppCallCountKey,
normalizeDID,
equalsIgnoreOrder,
systemHealth,
createHealthCheckApp,
nudgeCallCounts,
roundTripTime,
parseConnectionIp,
isMSTeamsCIDR,
isPrivateVoipNetwork
};
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
TCP_SERVER_PORT="${DRACHTIO_PORT:-4000}"
nc -v -z localhost $TCP_SERVER_PORT
# if last command exited with non zero
if [ $? != 0 ]
then
exit 1
fi
HTTP_SERVER_PORT="${HTTP_PORT:-3000}"
printf 'GET /system-health HTTP/1.1\r\nHost: localhost\r\n\r\n' | nc -v localhost 3000 | grep calls
# grep will automatically exit with 1 if string is not matched, however, will leave that call there in case
# we pivot to pipe to dev/null
if [ $? != 0 ]
then
exit 1
fi
exit 0
+7131 -2721
View File
File diff suppressed because it is too large Load Diff
+24 -18
View File
@@ -1,9 +1,9 @@
{
"name": "sbc-inbound",
"version": "0.3.6",
"version": "0.9.0",
"main": "app.js",
"engines": {
"node": ">= 10.16.0"
"node": ">= 12.0.0"
},
"keywords": [
"sip",
@@ -20,29 +20,35 @@
},
"scripts": {
"start": "node app",
"test": "NODE_ENV=test 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=debug 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"
},
"dependencies": {
"@jambonz/db-helpers": "^0.6.12",
"@jambonz/http-authenticator": "^0.2.0",
"@jambonz/realtimedb-helpers": "^0.4.3",
"@jambonz/rtpengine-utils": "^0.1.12",
"@jambonz/stats-collector": "^0.1.5",
"@jambonz/time-series": "^0.1.5",
"@jambonz/db-helpers": "^0.9.3",
"@jambonz/http-health-check": "^0.0.1",
"@jambonz/realtimedb-helpers": "^0.8.8",
"@jambonz/rtpengine-utils": "^0.4.4",
"@jambonz/siprec-client-utils": "^0.2.7",
"@jambonz/stats-collector": "^0.1.10",
"@jambonz/time-series": "^0.2.8",
"@jambonz/digest-utils": "^0.0.5",
"@aws-sdk/client-sns": "^3.549.0",
"@aws-sdk/client-auto-scaling": "^3.549.0",
"bent": "^7.3.12",
"cidr-matcher": "^2.1.1",
"debug": "^4.3.1",
"drachtio-fn-b2b-sugar": "0.0.12",
"drachtio-srf": "^4.4.49",
"pino": "^6.8.0",
"rtpengine-client": "^0.2.0"
"debug": "^4.3.4",
"drachtio-fn-b2b-sugar": "0.1.0",
"drachtio-srf": "^4.5.38",
"express": "^4.19.2",
"pino": "^8.20.0",
"verify-aws-sns-signature": "^0.1.0",
"xml2js": "^0.6.2"
},
"devDependencies": {
"clear-module": "^4.1.1",
"eslint": "^7.15.0",
"eslint-plugin-promise": "^4.2.1",
"eslint": "^7.32.0",
"eslint-plugin-promise": "^6.1.1",
"nyc": "^15.1.0",
"tape": "^4.13.3"
"tape": "^5.7.5"
}
}
+93 -18
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;
@@ -18,20 +20,28 @@ DROP TABLE IF EXISTS lcr_carrier_set_entry;
DROP TABLE IF EXISTS lcr_routes;
DROP TABLE IF EXISTS password_settings;
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;
@@ -65,6 +75,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 ,
@@ -119,6 +138,13 @@ priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted f
PRIMARY KEY (lcr_route_sid)
) COMMENT='Least cost routing table';
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 predefined_carriers
(
predefined_carrier_sid CHAR(36) NOT NULL UNIQUE ,
@@ -148,6 +174,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 +214,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 ,
@@ -205,6 +250,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,
@@ -260,6 +314,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,6 +342,9 @@ 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,
PRIMARY KEY (voip_carrier_sid)
) COMMENT='A Carrier or customer PBX that can send or receive calls';
@@ -395,6 +453,11 @@ 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),
PRIMARY KEY (account_sid)
) COMMENT='An enterprise that uses the platform for comm services';
@@ -402,24 +465,31 @@ 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);
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_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 +499,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,41 +520,44 @@ 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);
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid);
CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid);
CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_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 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);
@@ -497,12 +570,12 @@ 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 +591,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 +610,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 +618,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;
+32 -1
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');
@@ -38,6 +40,7 @@ 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 sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
@@ -57,4 +60,32 @@ insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_s
values ('999a5339-c62c-4075-9e19-f4de70a96597', '16173333456', '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 ('29543d4e-d959-4a25-836a-cde7161cd7d5', '1508222*', '287c1452-620d-4195-9f19-c9814ef90d78', 'ed649e33-e771-403a-8c99-1780eabbc803');
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid)
values ('dddd5c34-feae-4d70-98af-bb4d1f8dc965', '1508*', '287c1452-620d-4195-9f19-c9814ef90d78', 'ed649e33-e771-403a-8c99-1780eabbc803');
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid)
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');
-- 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);
+2 -1
View File
@@ -10,6 +10,7 @@ networks:
services:
mysql:
image: mysql:5.7
platform: linux/x86_64
ports:
- "3306:3306"
environment:
@@ -71,7 +72,7 @@ services:
ipv4_address: 172.38.0.14
influxdb:
image: influxdb:1.8-alpine
image: influxdb:1.8
ports:
- "8086:8086"
networks:
+118
View File
@@ -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:+15082223333@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:15082223333@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:15082223333@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:15082223333@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:15082223333@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:15082223333@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>
@@ -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]
+18 -1
View File
@@ -124,7 +124,24 @@
]]>
</send>
<recv request="BYE">
</recv>
<send next="2">
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Length: 0
]]>
</send>
<label id="2"/>
</scenario>
+22 -9
View File
@@ -1,8 +1,7 @@
const test = require('tape');
const { output, sippUac } = require('./sipp')('test_sbc-inbound');
const debug = require('debug')('drachtio:sbc-inbound');
const clearModule = require('clear-module');
const consoleLogger = {error: console.error, info: console.log, debug: console.log};
const { sippUac } = require('./sipp')('test_sbc-inbound');
const bent = require('bent');
const getJSON = bent('json');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
@@ -25,15 +24,24 @@ function waitFor(ms) {
test('incoming call tests', async(t) => {
const {srf} = require('../app');
const { queryCdrs } = srf.locals;
let res;
try {
await connect(srf);
let obj = await getJSON('http://127.0.0.1:3050/');
t.ok(obj.calls === 0, 'HTTP GET / works (current call count)')
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-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');
await sippUac('uac-did-regex-match.xml', '172.38.0.20');
t.pass('incoming call matched by trailing wildcard *');
await sippUac('uac-pcap-device-success.xml', '172.38.0.30');
t.pass('incoming call from authenticated device completed successfully');
@@ -50,12 +58,17 @@ 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');
/* 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(6 === res.total, 'successfully wrote 6 cdrs for calls');
//console.log(`cdrs: ${JSON.stringify(res)}`);
t.ok(7 === res.total, 'successfully wrote 7 cdrs for calls');
srf.disconnect();
t.end();