From f40469efccfbcc4129975afb98d8cbccf81c2059 Mon Sep 17 00:00:00 2001 From: Serhii Ivanov <11349489+s3rj1k@users.noreply.github.com> Date: Thu, 7 May 2026 23:53:07 +0200 Subject: [PATCH 01/24] [GHA] Use release libs for `trixie` releases (#3016) --- .github/docker/debian/trixie/amd64/public.release.Dockerfile | 2 +- .github/docker/debian/trixie/arm32v7/public.release.Dockerfile | 2 +- .github/docker/debian/trixie/arm64v8/public.release.Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/docker/debian/trixie/amd64/public.release.Dockerfile b/.github/docker/debian/trixie/amd64/public.release.Dockerfile index 4fa212d656..4a6c07d63b 100644 --- a/.github/docker/debian/trixie/amd64/public.release.Dockerfile +++ b/.github/docker/debian/trixie/amd64/public.release.Dockerfile @@ -54,7 +54,7 @@ RUN . ~/.env && ./debian/util.sh prep-create-dsc -a amd64 ${CODENAME} RUN --mount=type=secret,id=REPO_PASSWORD,required=true \ sha512sum /run/secrets/REPO_PASSWORD && \ curl -sSL https://freeswitch.org/fsget | \ - bash -s $(cat /run/secrets/REPO_PASSWORD) prerelease && \ + bash -s $(cat /run/secrets/REPO_PASSWORD) release && \ apt-get --quiet update && \ mk-build-deps \ --install \ diff --git a/.github/docker/debian/trixie/arm32v7/public.release.Dockerfile b/.github/docker/debian/trixie/arm32v7/public.release.Dockerfile index e378b3224e..5f5c412295 100644 --- a/.github/docker/debian/trixie/arm32v7/public.release.Dockerfile +++ b/.github/docker/debian/trixie/arm32v7/public.release.Dockerfile @@ -54,7 +54,7 @@ RUN . ~/.env && ./debian/util.sh prep-create-dsc -a armhf ${CODENAME} RUN --mount=type=secret,id=REPO_PASSWORD,required=true \ sha512sum /run/secrets/REPO_PASSWORD && \ curl -sSL https://freeswitch.org/fsget | \ - bash -s $(cat /run/secrets/REPO_PASSWORD) prerelease && \ + bash -s $(cat /run/secrets/REPO_PASSWORD) release && \ apt-get --quiet update && \ mk-build-deps \ --install \ diff --git a/.github/docker/debian/trixie/arm64v8/public.release.Dockerfile b/.github/docker/debian/trixie/arm64v8/public.release.Dockerfile index fb4e4d46e2..4fed01978b 100644 --- a/.github/docker/debian/trixie/arm64v8/public.release.Dockerfile +++ b/.github/docker/debian/trixie/arm64v8/public.release.Dockerfile @@ -54,7 +54,7 @@ RUN . ~/.env && ./debian/util.sh prep-create-dsc -a arm64 ${CODENAME} RUN --mount=type=secret,id=REPO_PASSWORD,required=true \ sha512sum /run/secrets/REPO_PASSWORD && \ curl -sSL https://freeswitch.org/fsget | \ - bash -s $(cat /run/secrets/REPO_PASSWORD) prerelease && \ + bash -s $(cat /run/secrets/REPO_PASSWORD) release && \ apt-get --quiet update && \ mk-build-deps \ --install \ From 7fbfe11d015787969f73b0fee1e2f48a40a6c132 Mon Sep 17 00:00:00 2001 From: Andrey Volk Date: Fri, 8 May 2026 02:26:52 +0300 Subject: [PATCH 02/24] version bump --- build/next-release.txt | 2 +- configure.ac | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/next-release.txt b/build/next-release.txt index 27cbef8a6e..095b667276 100644 --- a/build/next-release.txt +++ b/build/next-release.txt @@ -1 +1 @@ -1.10.13-dev +1.11.1-dev diff --git a/configure.ac b/configure.ac index d32c7c263f..03fe5fe991 100644 --- a/configure.ac +++ b/configure.ac @@ -3,10 +3,10 @@ # Must change all of the below together # For a release, set revision for that tagged release as well and uncomment -AC_INIT([freeswitch], [1.10.13-dev], bugs@freeswitch.org) +AC_INIT([freeswitch], [1.11.1-dev], bugs@freeswitch.org) AC_SUBST(SWITCH_VERSION_MAJOR, [1]) -AC_SUBST(SWITCH_VERSION_MINOR, [10]) -AC_SUBST(SWITCH_VERSION_MICRO, [13-dev]) +AC_SUBST(SWITCH_VERSION_MINOR, [11]) +AC_SUBST(SWITCH_VERSION_MICRO, [1-dev]) AC_SUBST(SWITCH_VERSION_REVISION, []) AC_SUBST(SWITCH_VERSION_REVISION_HUMAN, []) From 4bc49f57b733e699a508aa51b83a16f09bf505dc Mon Sep 17 00:00:00 2001 From: Andrey Volk Date: Wed, 20 May 2026 23:18:38 +0300 Subject: [PATCH 03/24] [Build-System] Update libks requirements to 2.0.11 (#3025) --- configure.ac | 2 +- w32/libks-version.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index 03fe5fe991..a9bc51a1a9 100644 --- a/configure.ac +++ b/configure.ac @@ -1518,7 +1518,7 @@ PKG_CHECK_MODULES([V8FS_STATIC], [v8-6.1_static >= 6.1.298],[ ]) ]) -PKG_CHECK_MODULES([KS], [libks2 >= 2.0.0],[ +PKG_CHECK_MODULES([KS], [libks2 >= 2.0.11],[ AM_CONDITIONAL([HAVE_KS],[true])],[ PKG_CHECK_MODULES([KS], [libks >= 1.8.2],[ AM_CONDITIONAL([HAVE_KS],[true])],[ diff --git a/w32/libks-version.props b/w32/libks-version.props index d19eb46e6b..c541afa58d 100644 --- a/w32/libks-version.props +++ b/w32/libks-version.props @@ -4,7 +4,7 @@ - 2.0.7 + 2.0.11 0 From 90da63c0d1bc50f7afe4de211676191651f423b2 Mon Sep 17 00:00:00 2001 From: Niall Dooley Date: Mon, 25 May 2026 20:11:10 +0200 Subject: [PATCH 04/24] [mod_commands] Fix reloadacl description --- src/mod/applications/mod_commands/mod_commands.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mod/applications/mod_commands/mod_commands.c b/src/mod/applications/mod_commands/mod_commands.c index 9184a78b93..75b79b2ebf 100644 --- a/src/mod/applications/mod_commands/mod_commands.c +++ b/src/mod/applications/mod_commands/mod_commands.c @@ -7653,7 +7653,7 @@ SWITCH_MODULE_LOAD_FUNCTION(mod_commands_load) SWITCH_ADD_API(commands_api_interface, "pool_stats", "Core pool memory usage", pool_stats_function, "Core pool memory usage."); SWITCH_ADD_API(commands_api_interface, "quote_shell_arg", "Quote/escape a string for use on shell command line", quote_shell_arg_function, ""); SWITCH_ADD_API(commands_api_interface, "regex", "Evaluate a regex", regex_function, "|[|][n|b]"); - SWITCH_ADD_API(commands_api_interface, "reloadacl", "Reload XML", reload_acl_function, ""); + SWITCH_ADD_API(commands_api_interface, "reloadacl", "Reload ACL", reload_acl_function, ""); SWITCH_ADD_API(commands_api_interface, "reload", "Reload module", reload_function, UNLOAD_SYNTAX); SWITCH_ADD_API(commands_api_interface, "reloadxml", "Reload XML", reload_xml_function, ""); SWITCH_ADD_API(commands_api_interface, "replace", "Replace a string", replace_function, "||"); From 7d35ea29865e67f04448be115a60d0a2c1b3e81a Mon Sep 17 00:00:00 2001 From: Gustavo Almeida Date: Mon, 25 May 2026 19:15:13 +0100 Subject: [PATCH 05/24] [mod_sofia] Fix handling of sip-options-respond-503-on-busy profile parameter --- src/mod/endpoints/mod_sofia/sofia.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/mod/endpoints/mod_sofia/sofia.c b/src/mod/endpoints/mod_sofia/sofia.c index 77e4750812..d2e94f16ce 100644 --- a/src/mod/endpoints/mod_sofia/sofia.c +++ b/src/mod/endpoints/mod_sofia/sofia.c @@ -2460,13 +2460,15 @@ void sofia_event_callback(nua_event_t event, } if (!sofia_private) { - if (sess_count >= sess_max || !sofia_test_pflag(profile, PFLAG_RUNNING) || !switch_core_ready_inbound()) { + int unavailable = (sess_count >= sess_max || !sofia_test_pflag(profile, PFLAG_RUNNING) || !switch_core_ready_inbound()); + int bypass = (event == nua_i_options && !sofia_test_pflag(profile, PFLAG_OPTIONS_RESPOND_503_ON_BUSY)); + + if (unavailable && !bypass) { nua_respond(nh, 503, "Maximum Calls In Progress", SIPTAG_RETRY_AFTER_STR("300"), NUTAG_WITH_THIS(nua), TAG_END()); nua_handle_destroy(nh); goto end; } - if (switch_queue_size(mod_sofia_globals.msg_queue) > (unsigned int)critical) { nua_respond(nh, 503, "System Busy", SIPTAG_RETRY_AFTER_STR("300"), NUTAG_WITH_THIS(nua), TAG_END()); nua_handle_destroy(nh); From 1544dfb755ef0fd3b3611ad96a016af3f08fc39b Mon Sep 17 00:00:00 2001 From: Andrey Volk Date: Mon, 25 May 2026 23:56:13 +0300 Subject: [PATCH 06/24] [Core, modules] Fix various dead assignments. --- .../applications/mod_commands/mod_commands.c | 6 +++--- .../mod_conference/mod_conference.c | 4 ++-- .../mod_spandsp/mod_spandsp_fax.c | 2 +- src/mod/endpoints/mod_sofia/mod_sofia.c | 2 +- src/mod/endpoints/mod_sofia/sofia.c | 8 ++++---- src/mod/endpoints/mod_sofia/sofia_glue.c | 4 ++-- src/mod/endpoints/mod_sofia/sofia_presence.c | 4 ++-- src/mod/endpoints/mod_verto/mod_verto.c | 2 +- .../mod_event_socket/mod_event_socket.c | 2 +- src/mod/formats/mod_shout/mod_shout.c | 2 +- .../mod_python3/freeswitch_python.cpp | 2 +- src/mod/languages/mod_v8/mod_v8.cpp | 2 +- src/switch_core_media.c | 19 ++++++++++++++----- src/switch_msrp.c | 2 +- src/switch_rtp.c | 2 +- 15 files changed, 36 insertions(+), 27 deletions(-) diff --git a/src/mod/applications/mod_commands/mod_commands.c b/src/mod/applications/mod_commands/mod_commands.c index 75b79b2ebf..22e25ea8a6 100644 --- a/src/mod/applications/mod_commands/mod_commands.c +++ b/src/mod/applications/mod_commands/mod_commands.c @@ -549,7 +549,7 @@ SWITCH_STANDARD_API(reg_url_function) char *domain = NULL, *dup_domain = NULL; char *concat = NULL; const char *exclude_contact = NULL; - char *reply = "error/facility_not_subscribed"; + char *reply; switch_stream_handle_t mystream = { 0 }; if (!cmd) { @@ -2324,7 +2324,7 @@ SWITCH_STANDARD_API(status_function) int sps = 0, last_sps = 0, max_sps = 0, max_sps_fivemin = 0; int sessions_peak = 0, sessions_peak_fivemin = 0; /* Max Concurrent Sessions buffers */ switch_bool_t html = SWITCH_FALSE; /* shortcut to format.html */ - char * nl = "\n"; /* shortcut to format.nl */ + char *nl; /* shortcut to format.nl */ stream_format format = { 0 }; switch_size_t cur = 0, max = 0; @@ -7523,7 +7523,7 @@ SWITCH_STANDARD_JSON_API(json_status_function) SWITCH_STANDARD_API(json_function) { cJSON *jcmd = NULL, *format = NULL; - const char *message = ""; + const char *message; char *response = NULL; if (zstr(cmd)) { diff --git a/src/mod/applications/mod_conference/mod_conference.c b/src/mod/applications/mod_conference/mod_conference.c index aa606170d5..4f0374263d 100644 --- a/src/mod/applications/mod_conference/mod_conference.c +++ b/src/mod/applications/mod_conference/mod_conference.c @@ -1911,8 +1911,8 @@ SWITCH_STANDARD_APP(conference_function) member_flag_t mflags[MFLAG_MAX] = { 0 }; switch_core_session_message_t msg = { 0 }; uint8_t isbr = 0; - char *dpin = ""; - const char *mdpin = ""; + char *dpin; + const char *mdpin; conference_xml_cfg_t xml_cfg = { 0 }; switch_event_t *params = NULL; int locked = 0; diff --git a/src/mod/applications/mod_spandsp/mod_spandsp_fax.c b/src/mod/applications/mod_spandsp/mod_spandsp_fax.c index 8c8d0103c4..0767315c92 100644 --- a/src/mod/applications/mod_spandsp/mod_spandsp_fax.c +++ b/src/mod/applications/mod_spandsp/mod_spandsp_fax.c @@ -562,7 +562,7 @@ static void phase_e_handler(void *user_data, int result) switch_event_t *event; const char *var; char *expanded; - const char *fax_result_str = ""; + const char *fax_result_str; pvt = (pvt_t *) user_data; switch_assert(pvt); diff --git a/src/mod/endpoints/mod_sofia/mod_sofia.c b/src/mod/endpoints/mod_sofia/mod_sofia.c index 086d6dd088..4fd5d57c1f 100644 --- a/src/mod/endpoints/mod_sofia/mod_sofia.c +++ b/src/mod/endpoints/mod_sofia/mod_sofia.c @@ -4091,7 +4091,7 @@ SWITCH_STANDARD_API(sofia_contact_function) sofia_profile_t *profile = NULL; const char *exclude_contact = NULL; const char *match_user_agent = NULL; - char *reply = "error/facility_not_subscribed"; + char *reply; switch_stream_handle_t mystream = { 0 }; if (!cmd) { diff --git a/src/mod/endpoints/mod_sofia/sofia.c b/src/mod/endpoints/mod_sofia/sofia.c index d2e94f16ce..2901ffdd63 100644 --- a/src/mod/endpoints/mod_sofia/sofia.c +++ b/src/mod/endpoints/mod_sofia/sofia.c @@ -1210,7 +1210,7 @@ void sofia_update_callee_id(switch_core_session_t *session, sofia_profile_t *pro switch_channel_t *channel = switch_core_session_get_channel(session); sip_p_asserted_identity_t *passerted = NULL; char *name = NULL; - const char *number = "unknown", *tmp; + const char *number, *tmp; switch_caller_profile_t *caller_profile; char *dup = NULL; switch_event_t *event; @@ -7535,7 +7535,7 @@ static void sofia_handle_sip_i_state(switch_core_session_t *session, int status, } if (channel && profile->pres_type && ss_state == nua_callstate_ready && status == 200) { - const char* to_tag = ""; + const char* to_tag; char *sql = NULL; to_tag = switch_str_nil(switch_channel_get_variable(channel, "sip_to_tag")); sql = switch_mprintf("update sip_dialogs set sip_to_tag='%q' " @@ -10392,7 +10392,7 @@ void sofia_handle_sip_i_invite(switch_core_session_t *session, nua_t *nua, sofia nua_handle_t *bnh = NULL; char sip_acl_authed_by[512] = ""; char sip_acl_token[512] = ""; - const char *dialog_from_user = "", *dialog_from_host = "", *to_user = "", *to_host = "", *contact_user = "", *contact_host = ""; + const char *dialog_from_user = "", *dialog_from_host = "", *to_user = "", *to_host = "", *contact_user, *contact_host; const char *user_agent = "", *call_id = ""; url_t *from = NULL, *to = NULL, *contact = NULL; const char *to_tag = ""; @@ -11635,7 +11635,7 @@ void sofia_handle_sip_i_invite(switch_core_session_t *session, nua_t *nua, sofia if (profile->pres_type) { const char *presence_data = switch_channel_get_variable(channel, "presence_data"); const char *presence_id = switch_channel_get_variable(channel, "presence_id"); - char *full_contact = ""; + char *full_contact; char *p = NULL; time_t now; diff --git a/src/mod/endpoints/mod_sofia/sofia_glue.c b/src/mod/endpoints/mod_sofia/sofia_glue.c index 33904aaac4..d490ebd65c 100644 --- a/src/mod/endpoints/mod_sofia/sofia_glue.c +++ b/src/mod/endpoints/mod_sofia/sofia_glue.c @@ -1038,7 +1038,7 @@ switch_status_t sofia_glue_do_invite(switch_core_session_t *session) switch_caller_profile_t *caller_profile; const char *cid_name, *cid_num; char *e_dest = NULL; - const char *holdstr = ""; + const char *holdstr; char *extra_headers = NULL; switch_status_t status = SWITCH_STATUS_FALSE; uint32_t session_timeout = tech_pvt->profile->session_timeout; @@ -3334,7 +3334,7 @@ char *sofia_glue_gen_contact_str(sofia_profile_t *profile, sip_t const *sip, nua const char *contact_host;//, *contact_user; sip_contact_t const *contact; char *port; - const char *display = "\"user\""; + const char *display; char new_port[25] = ""; sofia_nat_parse_t lnp = { { 0 } }; const char *ipv6; diff --git a/src/mod/endpoints/mod_sofia/sofia_presence.c b/src/mod/endpoints/mod_sofia/sofia_presence.c index 151d6ec634..6108cc5a6a 100644 --- a/src/mod/endpoints/mod_sofia/sofia_presence.c +++ b/src/mod/endpoints/mod_sofia/sofia_presence.c @@ -2392,7 +2392,7 @@ static int sofia_dialog_probe_notify_callback(void *pArg, int argc, char **argv, switch_stream_handle_t stream = { 0 }; char *to; const char *pl = NULL; - const char *ct = "application/dialog-info+xml"; + const char *ct; if (mod_sofia_globals.debug_presence > 0) { int i; @@ -3659,7 +3659,7 @@ void sofia_presence_handle_sip_i_subscribe(int status, char *orig_proto = ""; char *alt_proto = NULL; char *d_user = NULL; - char *contact_str = ""; + char *contact_str; const char *call_id = NULL; char *to_str = NULL; char *full_from = NULL; diff --git a/src/mod/endpoints/mod_verto/mod_verto.c b/src/mod/endpoints/mod_verto/mod_verto.c index 40bb51171a..1363416139 100644 --- a/src/mod/endpoints/mod_verto/mod_verto.c +++ b/src/mod/endpoints/mod_verto/mod_verto.c @@ -174,7 +174,7 @@ void verto_broadcast(const char *event_channel, cJSON *json, const char *key, sw static int verto_init_ssl(verto_profile_t *profile) { - const char *err = ""; + const char *err; int i = 0; profile->ssl_method = SSLv23_server_method(); /* create server instance */ diff --git a/src/mod/event_handlers/mod_event_socket/mod_event_socket.c b/src/mod/event_handlers/mod_event_socket/mod_event_socket.c index 1c35b6f049..31bac3305b 100644 --- a/src/mod/event_handlers/mod_event_socket/mod_event_socket.c +++ b/src/mod/event_handlers/mod_event_socket/mod_event_socket.c @@ -1117,7 +1117,7 @@ SWITCH_STANDARD_API(event_sink_function) } if (listener->format == EVENT_FORMAT_JSON) { - char *p = "{}"; + char *p; cJSON_AddItemToObject(cj, "events", cjevents); p = cJSON_Print(cj); if (cj && p) stream->write_function(stream, p); diff --git a/src/mod/formats/mod_shout/mod_shout.c b/src/mod/formats/mod_shout/mod_shout.c index d6cad23bbd..5cc54a4f1a 100644 --- a/src/mod/formats/mod_shout/mod_shout.c +++ b/src/mod/formats/mod_shout/mod_shout.c @@ -1289,7 +1289,7 @@ void do_telecast(switch_stream_handle_t *stream) char *path_info = switch_event_get_header(stream->param_event, "http-path-info"); char *uuid = strdup(path_info + 4); switch_core_session_t *tsession; - char *fname = "stream.mp3"; + char *fname; switch_assert(uuid); if ((fname = strchr(uuid, '/'))) { diff --git a/src/mod/languages/mod_python3/freeswitch_python.cpp b/src/mod/languages/mod_python3/freeswitch_python.cpp index e35c7ed464..600d8f87df 100644 --- a/src/mod/languages/mod_python3/freeswitch_python.cpp +++ b/src/mod/languages/mod_python3/freeswitch_python.cpp @@ -293,7 +293,7 @@ switch_status_t Session::run_dtmf_callback(void *input, switch_input_type_t ityp PyObject *pyresult, *arglist, *io = NULL; int ts = 0; - char *str = NULL, *what = (char*)""; + char *str = NULL, *what; if (TS) { ts++; diff --git a/src/mod/languages/mod_v8/mod_v8.cpp b/src/mod/languages/mod_v8/mod_v8.cpp index b297fa39e1..96ac67f2f6 100644 --- a/src/mod/languages/mod_v8/mod_v8.cpp +++ b/src/mod/languages/mod_v8/mod_v8.cpp @@ -424,7 +424,7 @@ static void v8_error(Isolate* isolate, TryCatch* try_catch) String::Utf8Value exception(try_catch->Exception()); const char *exception_string = js_safe_str(*exception); Handle message = try_catch->Message(); - const char *msg = ""; + const char *msg; string filename = __FILE__; int line = __LINE__; string text = ""; diff --git a/src/switch_core_media.c b/src/switch_core_media.c index de5d0eff74..51a4d32604 100644 --- a/src/switch_core_media.c +++ b/src/switch_core_media.c @@ -4549,7 +4549,7 @@ static void restore_pmaps(switch_rtp_engine_t *engine) static const char *media_flow_varname(switch_media_type_t type) { - const char *varname = "invalid"; + const char *varname; switch(type) { case SWITCH_MEDIA_TYPE_AUDIO: @@ -4561,6 +4561,9 @@ static const char *media_flow_varname(switch_media_type_t type) case SWITCH_MEDIA_TYPE_TEXT: varname = "text_media_flow"; break; + default: + varname = "invalid"; + break; } return varname; @@ -4568,7 +4571,7 @@ static const char *media_flow_varname(switch_media_type_t type) static const char *remote_media_flow_varname(switch_media_type_t type) { - const char *varname = "invalid"; + const char *varname; switch(type) { case SWITCH_MEDIA_TYPE_AUDIO: @@ -4580,6 +4583,9 @@ static const char *remote_media_flow_varname(switch_media_type_t type) case SWITCH_MEDIA_TYPE_TEXT: varname = "remote_text_media_flow"; break; + default: + varname = "invalid"; + break; } return varname; @@ -4587,7 +4593,7 @@ static const char *remote_media_flow_varname(switch_media_type_t type) static void media_flow_get_mode(switch_media_flow_t smode, const char **mode_str, switch_media_flow_t *opp_mode) { - const char *smode_str = ""; + const char *smode_str; switch_media_flow_t opp_smode = smode; switch(smode) { @@ -4608,6 +4614,9 @@ static void media_flow_get_mode(switch_media_flow_t smode, const char **mode_str case SWITCH_MEDIA_FLOW_SENDRECV: smode_str = "sendrecv"; break; + default: + smode_str = ""; + break; } *mode_str = smode_str; @@ -11775,7 +11784,7 @@ SWITCH_DECLARE(void) switch_core_media_set_udptl_image_sdp(switch_core_session_t char max_data[128] = ""; const char *ip; uint32_t port; - const char *family = "IP4"; + const char *family; const char *username; const char *bit_removal_on = "a=T38FaxFillBitRemoval\r\n"; const char *bit_removal_off = ""; @@ -12033,7 +12042,7 @@ SWITCH_DECLARE(void) switch_core_media_patch_sdp(switch_core_session_t *session) switch_size_t len; if (oe) { - const char *family = "IP4"; + const char *family; char o_line[1024] = ""; if (oe >= pe) { diff --git a/src/switch_msrp.c b/src/switch_msrp.c index 9fd84d846b..89b2a886c6 100644 --- a/src/switch_msrp.c +++ b/src/switch_msrp.c @@ -114,7 +114,7 @@ static void msrp_deinit_ssl(void) static void msrp_init_ssl(void) { - const char *err = ""; + const char *err; globals.ssl_client_method = SSLv23_client_method(); globals.ssl_client_ctx = SSL_CTX_new(globals.ssl_client_method); diff --git a/src/switch_rtp.c b/src/switch_rtp.c index d038566d90..d92b23001e 100644 --- a/src/switch_rtp.c +++ b/src/switch_rtp.c @@ -3822,7 +3822,7 @@ SWITCH_DECLARE(switch_status_t) switch_rtp_add_dtls(switch_rtp_t *rtp_session, d switch_dtls_t *dtls; const char *var; int ret; - const char *kind = ""; + const char *kind; unsigned long ssl_method_error = 0; unsigned long ssl_ctx_error = 0; const SSL_METHOD *ssl_method; From c25af8dd81637f528ed4258cc7c2488052331cf1 Mon Sep 17 00:00:00 2001 From: Dmitry Verenitsin Date: Tue, 26 May 2026 02:12:08 +0500 Subject: [PATCH 07/24] [mod_erlang_event] Fix correctness, OTP compatibility, and memory issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - Snapshot `erl_errno` after `ei_xreceive_msg_tmo()` — outbound `ei_*` calls in the same loop iteration clobber the thread-local errno before the listener checks it, causing wrong exit decisions and misleading logs. - Fix `switch_size_t ` cast of `int` in `ei_link`* — `(switch_size_t *)&index` reads/writes 8 bytes through a 4-byte `int` on LP64. Use a real `switch_size_t` local. - Dispatch `ERL_NEWER_REFERENCE_EXT` — newer OTP encodes refs with this tag; spawn replies from modern nodes were silently dropped to the default branch. - Handle `ERL_EXIT2` — processes killed via `erlang:exit/2` arrive with this tag, not `ERL_EXIT`. Without it, sessions stayed attached to dead Erlang pids. - Modernize `-spec` syntax in `freeswitch.erl` — old `-spec(F/N :: (...))` form was removed in OTP 21+; module no longer compiled. - Fix multiple memory issues: - `ei_hash_ref()`: replace unbounded `sprintf` with `snprintf` + shared `EI_HASH_REF_LEN`. - `handle_msg_sendevent` / `handle_msg_sendmsg`: free the heap `value` on `ei_decode_string` failure; remove dead `if (!fail)` branches. - `listener_main_loop`: free `buf`/`rbuf` on the two `handle_msg` early-exit paths. - `erlang_sendmsg_function` app: move `ei_x_new_with_version` past arg validation and add `ei_x_free` at the end. --- .../mod_erlang_event/ei_helpers.c | 7 +++--- .../mod_erlang_event/freeswitch.erl | 4 +-- .../mod_erlang_event/handle_msg.c | 25 ++++++++++--------- .../mod_erlang_event/mod_erlang_event.c | 24 ++++++++++++------ .../mod_erlang_event/mod_erlang_event.h | 1 + 5 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/mod/event_handlers/mod_erlang_event/ei_helpers.c b/src/mod/event_handlers/mod_erlang_event/ei_helpers.c index 6472180a2e..74dc860cb1 100644 --- a/src/mod/event_handlers/mod_erlang_event/ei_helpers.c +++ b/src/mod/event_handlers/mod_erlang_event/ei_helpers.c @@ -64,6 +64,7 @@ void ei_link(listener_t *listener, erlang_pid * from, erlang_pid * to) char msgbuf[2048]; char *s; int index = 0; + switch_size_t send_len; int status = SWITCH_STATUS_SUCCESS; switch_socket_t *sock = NULL; switch_os_sock_put(&sock, &listener->sockdes, listener->pool); @@ -82,7 +83,8 @@ void ei_link(listener_t *listener, erlang_pid * from, erlang_pid * to) /* sum: 542 */ switch_mutex_lock(listener->sock_mutex); - status = switch_socket_send(sock, msgbuf, (switch_size_t *) &index); + send_len = (switch_size_t)index; + status = switch_socket_send(sock, msgbuf, &send_len); if (status != SWITCH_STATUS_SUCCESS) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Failed to link to process on %s\n", listener->peer_nodename); } @@ -283,8 +285,7 @@ int ei_sendto(ei_cnode * ec, int fd, struct erlang_process *process, ei_x_buff * /* convert an erlang reference to some kind of hashed string so we can store it as a hash key */ void ei_hash_ref(erlang_ref * ref, char *output) { - /* very lazy */ - sprintf(output, "%d.%d.%d@%s", ref->n[0], ref->n[1], ref->n[2], ref->node); + snprintf(output, EI_HASH_REF_LEN, "%d.%d.%d@%s", ref->n[0], ref->n[1], ref->n[2], ref->node); } diff --git a/src/mod/event_handlers/mod_erlang_event/freeswitch.erl b/src/mod/event_handlers/mod_erlang_event/freeswitch.erl index 9a08d88ae2..a82f53b651 100644 --- a/src/mod/event_handlers/mod_erlang_event/freeswitch.erl +++ b/src/mod/event_handlers/mod_erlang_event/freeswitch.erl @@ -91,7 +91,7 @@ api(Node, Cmd) -> %% sent to calling process after it is received. This function %% returns the result of the initial bgapi call or `timeout' if FreeSWITCH fails %% to respond. --spec(bgapi/3 :: (Node :: atom(), Cmd :: atom(), Args :: string()) -> {'ok', string()} | {'error', any()} | 'timeout'). +-spec bgapi(Node :: atom(), Cmd :: atom(), Args :: string()) -> {'ok', string()} | {'error', any()} | 'timeout'. bgapi(Node, Cmd, Args) -> Self = self(), % spawn a new process so that both responses go here instead of directly to @@ -128,7 +128,7 @@ bgapi(Node, Cmd, Args) -> %% passed as the argument to `Fun' after it is received. This function %% returns the result of the initial bgapi call or `timeout' if FreeSWITCH fails %% to respond. --spec(bgapi/4 :: (Node :: atom(), Cmd :: atom(), Args :: string(), Fun :: fun()) -> 'ok' | {'error', any()} | 'timeout'). +-spec bgapi(Node :: atom(), Cmd :: atom(), Args :: string(), Fun :: fun()) -> 'ok' | {'error', any()} | 'timeout'. bgapi(Node, Cmd, Args, Fun) -> Self = self(), % spawn a new process so that both responses go here instead of directly to diff --git a/src/mod/event_handlers/mod_erlang_event/handle_msg.c b/src/mod/event_handlers/mod_erlang_event/handle_msg.c index aad45a4986..a54a5123e9 100644 --- a/src/mod/event_handlers/mod_erlang_event/handle_msg.c +++ b/src/mod/event_handlers/mod_erlang_event/handle_msg.c @@ -804,13 +804,13 @@ static switch_status_t handle_msg_sendevent(listener_t *listener, int arity, ei_ } else { switch_event_types_t etype; if (switch_name_event(ename, &etype) == SWITCH_STATUS_SUCCESS) { - switch_event_t *event; + switch_event_t *event = NULL; if ((strlen(esname) && switch_event_create_subclass(&event, etype, esname) == SWITCH_STATUS_SUCCESS) || switch_event_create(&event, etype) == SWITCH_STATUS_SUCCESS) { char key[1024]; - char *value; - int type; - int size; + char *value = NULL; + int type; + int size; int i = 0; switch_bool_t fail = SWITCH_FALSE; @@ -828,14 +828,15 @@ static switch_status_t handle_msg_sendevent(listener_t *listener, int arity, ei_ value = malloc(size + 1); if (ei_decode_string(buf->buff, &buf->index, value)) { - fail = SWITCH_TRUE; + switch_safe_free(value); + fail = SWITCH_TRUE; break; } - if (!fail && !strcmp(key, "body")) { + if (!strcmp(key, "body")) { switch_safe_free(event->body); event->body = value; - } else if (!fail) { + } else { switch_event_add_header_string_nodup(event, SWITCH_STACK_BOTTOM, key, value); } @@ -896,13 +897,12 @@ static switch_status_t handle_msg_sendmsg(listener_t *listener, int arity, ei_x_ value = malloc(size + 1); if (ei_decode_string(buf->buff, &buf->index, value)) { + switch_safe_free(value); fail = SWITCH_TRUE; break; } - if (!fail) { - switch_event_add_header_string_nodup(event, SWITCH_STACK_BOTTOM, key, value); - } + switch_event_add_header_string_nodup(event, SWITCH_STACK_BOTTOM, key, value); } if (headerlength != i || fail) { @@ -1204,7 +1204,7 @@ static switch_status_t handle_ref_tuple(listener_t *listener, erlang_msg * msg, { erlang_ref ref; erlang_pid pid; - char hash[100]; + char hash[EI_HASH_REF_LEN]; int arity; const void *key; void *val; @@ -1232,7 +1232,7 @@ static switch_status_t handle_ref_tuple(listener_t *listener, erlang_msg * msg, for (iter = switch_core_hash_first(listener->sessions); iter; iter = switch_core_hash_next(&iter)) { switch_core_hash_this(iter, &key, NULL, &val); se = (session_elem_t*)val; - if (switch_test_flag(se, LFLAG_WAITING_FOR_PID) && se->spawn_reply && !strncmp(se->spawn_reply->hash, hash, 100)) { + if (switch_test_flag(se, LFLAG_WAITING_FOR_PID) && se->spawn_reply && !strncmp(se->spawn_reply->hash, hash, EI_HASH_REF_LEN)) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "found matching session for %s : %s\n", hash, se->uuid_str); @@ -1376,6 +1376,7 @@ int handle_msg(listener_t *listener, erlang_msg * msg, ei_x_buff * buf, ei_x_buf break; case ERL_REFERENCE_EXT: case ERL_NEW_REFERENCE_EXT: + case ERL_NEWER_REFERENCE_EXT: ret = handle_ref_tuple(listener, msg, buf, rbuf); break; default: diff --git a/src/mod/event_handlers/mod_erlang_event/mod_erlang_event.c b/src/mod/event_handlers/mod_erlang_event/mod_erlang_event.c index f163b2197f..3315d60fb8 100644 --- a/src/mod/event_handlers/mod_erlang_event/mod_erlang_event.c +++ b/src/mod/event_handlers/mod_erlang_event/mod_erlang_event.c @@ -968,9 +968,10 @@ static void handle_exit(listener_t *listener, erlang_pid * pid) static void listener_main_loop(listener_t *listener) { int status = 1; + int recv_erl_errno = ETIMEDOUT; int msgs_sent = 0; /* how many messages we sent in a loop */ - while ((status >= 0 || erl_errno == ETIMEDOUT || erl_errno == EAGAIN) && !prefs.done) { + while ((status >= 0 || recv_erl_errno == ETIMEDOUT || recv_erl_errno == EAGAIN) && !prefs.done) { erlang_msg msg; ei_x_buff buf; ei_x_buff rbuf; @@ -983,6 +984,9 @@ static void listener_main_loop(listener_t *listener) /* do we need the mutex when reading? */ /*switch_mutex_lock(listener->sock_mutex); */ status = ei_xreceive_msg_tmo(listener->sockdes, &msg, &buf, 1); + /* snapshot erl_errno before any outbound ei call (queue flushers below) + clobbers this thread-local slot. */ + recv_erl_errno = erl_errno; /*switch_mutex_unlock(listener->sock_mutex); */ switch (status) { @@ -1001,6 +1005,8 @@ static void listener_main_loop(listener_t *listener) if (handle_msg(listener, &msg, &buf, &rbuf)) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "handle_msg requested exit\n"); + ei_x_free(&buf); + ei_x_free(&rbuf); return; } break; @@ -1016,6 +1022,8 @@ static void listener_main_loop(listener_t *listener) if (handle_msg(listener, &msg, &buf, &rbuf)) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "handle_msg requested exit\n"); + ei_x_free(&buf); + ei_x_free(&rbuf); return; } break; @@ -1026,6 +1034,7 @@ static void listener_main_loop(listener_t *listener) switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "erl_unlink\n"); break; case ERL_EXIT: + case ERL_EXIT2: switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "erl_exit from %s <%d.%d.%d>\n", msg.from.node, msg.from.creation, msg.from.num, msg.from.serial); @@ -1037,8 +1046,8 @@ static void listener_main_loop(listener_t *listener) } break; case ERL_ERROR: - if (erl_errno != ETIMEDOUT && erl_errno != EAGAIN) { - switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "erl_error: status=%d, erl_errno=%d errno=%d\n", status, erl_errno, errno); + if (recv_erl_errno != ETIMEDOUT && recv_erl_errno != EAGAIN) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "erl_error: status=%d, erl_errno=%d errno=%d\n", status, recv_erl_errno, errno); } break; default: @@ -1069,7 +1078,7 @@ static void listener_main_loop(listener_t *listener) if (prefs.done) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "shutting down listener\n"); } else { - switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "listener exit: status=%d, erl_errno=%d errno=%d\n", status, erl_errno, errno); + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "listener exit: status=%d, erl_errno=%d errno=%d\n", status, recv_erl_errno, errno); } } @@ -1513,7 +1522,7 @@ session_elem_t *attach_call_to_spawned_process(listener_t *listener, char *modul { /* create a session list element */ session_elem_t *session_element = session_elem_create(listener, session); - char hash[100]; + char hash[EI_HASH_REF_LEN]; spawn_reply_t *p; erlang_ref ref; @@ -1720,8 +1729,6 @@ SWITCH_STANDARD_APP(erlang_sendmsg_function) ei_x_buff buf; listener_t *listener; - ei_x_new_with_version(&buf); - /* process app arguments */ if (data && (mydata = switch_core_session_strdup(session, data))) { argc = switch_separate_string(mydata, ' ', argv, 3); @@ -1737,6 +1744,7 @@ SWITCH_STANDARD_APP(erlang_sendmsg_function) /*switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "sendmsg: {%s, %s} ! %s\n", reg_name, node, argv[2]); */ + ei_x_new_with_version(&buf); ei_x_encode_tuple_header(&buf, 2); ei_x_encode_atom(&buf, "freeswitch_sendmsg"); _ei_x_encode_string(&buf, argv[2]); @@ -1754,6 +1762,8 @@ SWITCH_STANDARD_APP(erlang_sendmsg_function) switch_thread_rwlock_unlock(listener->rwlock); } + + ei_x_free(&buf); } diff --git a/src/mod/event_handlers/mod_erlang_event/mod_erlang_event.h b/src/mod/event_handlers/mod_erlang_event/mod_erlang_event.h index 2f2c0ed059..e662273208 100644 --- a/src/mod/event_handlers/mod_erlang_event/mod_erlang_event.h +++ b/src/mod/event_handlers/mod_erlang_event/mod_erlang_event.h @@ -238,6 +238,7 @@ extern prefs_t prefs; int handle_msg(listener_t *listener, erlang_msg * msg, ei_x_buff * buf, ei_x_buff * rbuf); /* ei_helpers.c */ +#define EI_HASH_REF_LEN (MAXATOMLEN_UTF8 + 64) void ei_link(listener_t *listener, erlang_pid * from, erlang_pid * to); void ei_encode_switch_event_headers(ei_x_buff * ebuf, switch_event_t *event); void ei_encode_switch_event_tag(ei_x_buff * ebuf, switch_event_t *event, char *tag); From 56cc958b2877317506eb674ea87b7720a3ca6b43 Mon Sep 17 00:00:00 2001 From: Dmitry Verenitsin Date: Tue, 26 May 2026 02:13:29 +0500 Subject: [PATCH 08/24] [core] Fix use-after-free in session thread pool worker. (#3030) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `switch_core_session_thread_pool_launch()` allocated the thread data (`td`) from the session pool. However, `switch_core_session_thread()` destroys the session pool before returning, leaving td as a dangling pointer. The worker then accesses `td->running` and `td->pool` — a use-after-free that crashes under memory pressure when the freed pool is reused. Allocate `td` with `switch_zmalloc()` and set `td->alloc = 1` so the worker frees it after the task completes. This ensures `td` outlives the session pool destruction. --- src/switch_core_session.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/switch_core_session.c b/src/switch_core_session.c index 94944faa2f..c0d3853cf1 100644 --- a/src/switch_core_session.c +++ b/src/switch_core_session.c @@ -1932,7 +1932,8 @@ SWITCH_DECLARE(switch_status_t) switch_core_session_thread_pool_launch(switch_co } else { switch_set_flag(session, SSF_THREAD_RUNNING); switch_set_flag(session, SSF_THREAD_STARTED); - td = switch_core_session_alloc(session, sizeof(*td)); + switch_zmalloc(td, sizeof(*td)); + td->alloc = 1; td->obj = session; td->func = switch_core_session_thread; status = switch_queue_push(session_manager.thread_queue, td); From 08c3fffa7cf6596a25f564d747fa7c28424a368c Mon Sep 17 00:00:00 2001 From: Dmitry Verenitsin Date: Tue, 26 May 2026 02:15:19 +0500 Subject: [PATCH 09/24] [mod_sofia] Fix use-after-free in dispatch event thread. (#3031) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sofia_process_dispatch_event_in_thread` allocated `td` from a memory pool, then `sofia_msg_thread_run_once` destroyed that same pool after processing the event — leaving `td` dangling when the thread pool worker accessed it. Allocate `td` with `switch_zmalloc` (`td->alloc = 1`) so the worker frees it safely after the function returns. Remove the now-unused `pool` field from `sofia_dispatch_event_t`. --- src/mod/endpoints/mod_sofia/mod_sofia.h | 1 - src/mod/endpoints/mod_sofia/sofia.c | 15 ++------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/mod/endpoints/mod_sofia/mod_sofia.h b/src/mod/endpoints/mod_sofia/mod_sofia.h index 8e2b1b483c..3689f82a20 100644 --- a/src/mod/endpoints/mod_sofia/mod_sofia.h +++ b/src/mod/endpoints/mod_sofia/mod_sofia.h @@ -168,7 +168,6 @@ typedef struct sofia_dispatch_event_s { int save; switch_core_session_t *session; switch_core_session_t *init_session; - switch_memory_pool_t *pool; struct sofia_dispatch_event_s *next; } sofia_dispatch_event_t; diff --git a/src/mod/endpoints/mod_sofia/sofia.c b/src/mod/endpoints/mod_sofia/sofia.c index 2901ffdd63..7579c8c03c 100644 --- a/src/mod/endpoints/mod_sofia/sofia.c +++ b/src/mod/endpoints/mod_sofia/sofia.c @@ -2199,22 +2199,15 @@ static uint32_t DE_THREAD_CNT = 0; void *SWITCH_THREAD_FUNC sofia_msg_thread_run_once(switch_thread_t *thread, void *obj) { sofia_dispatch_event_t *de = (sofia_dispatch_event_t *) obj; - switch_memory_pool_t *pool = NULL; switch_mutex_lock(mod_sofia_globals.mutex); DE_THREAD_CNT++; switch_mutex_unlock(mod_sofia_globals.mutex); if (de) { - pool = de->pool; - de->pool = NULL; sofia_process_dispatch_event(&de); } - if (pool) { - switch_core_destroy_memory_pool(&pool); - } - switch_mutex_lock(mod_sofia_globals.mutex); DE_THREAD_CNT--; switch_mutex_unlock(mod_sofia_globals.mutex); @@ -2225,16 +2218,12 @@ void *SWITCH_THREAD_FUNC sofia_msg_thread_run_once(switch_thread_t *thread, void void sofia_process_dispatch_event_in_thread(sofia_dispatch_event_t **dep) { sofia_dispatch_event_t *de = *dep; - switch_memory_pool_t *pool; - //sofia_profile_t *profile = (*dep)->profile; switch_thread_data_t *td; - switch_core_new_memory_pool(&pool); - *dep = NULL; - de->pool = pool; - td = switch_core_alloc(pool, sizeof(*td)); + switch_zmalloc(td, sizeof(*td)); + td->alloc = 1; td->func = sofia_msg_thread_run_once; td->obj = de; From 325bb3a6062220c166bc1484ef5ed5f9e2bfcf3b Mon Sep 17 00:00:00 2001 From: Dmitry Verenitsin Date: Tue, 26 May 2026 02:16:40 +0500 Subject: [PATCH 10/24] [core] Fix segments count check in clean_uri(). Add unit-test. (#3032) --- src/switch_utils.c | 3 +- tests/unit/switch_utils.c | 63 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/switch_utils.c b/src/switch_utils.c index 206f99218a..dc855c8def 100644 --- a/src/switch_utils.c +++ b/src/switch_utils.c @@ -4270,7 +4270,8 @@ switch_status_t clean_uri(char *uri) argc = switch_separate_string(uri, '/', argv, sizeof(argv) / sizeof(argv[0])); - if (argc == sizeof(argv)) { /* too deep */ + /* Intentionally using == instead of > because this way we would know that the url was fully parsed for sure */ + if (argc == (sizeof(argv) / sizeof(argv[0]))) { /* too deep */ return SWITCH_STATUS_FALSE; } diff --git a/tests/unit/switch_utils.c b/tests/unit/switch_utils.c index 391ec6e8e6..959f9f2e4f 100644 --- a/tests/unit/switch_utils.c +++ b/tests/unit/switch_utils.c @@ -124,6 +124,69 @@ FST_TEST_BEGIN(b64_pad1) } FST_TEST_END() +#define test_uri_count 6 + +/* Currently tests only clear_uri() */ +FST_TEST_BEGIN(test_switch_http_parse_header) +{ + int i = 0; + switch_status_t status = SWITCH_STATUS_SUCCESS; + switch_http_request_t request = {0}; + char bad_uris[][200] = { + "/t/o/o/_/l/o/n/g/_/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/2/3/4", + "without_a_slash/", + }; + char raw_uris[test_uri_count][200] = { + "/////////uri1", + "/././././uri2", + "/uri3/uri3_1/.//uri3_2/../../uri3_3", + "/../../../uri4", + "/uri5/uri5_1/", + "/uri6/uri6_1", + }; + const char clear_uris[test_uri_count][200] = { + "/uri1", + "/uri2", + "/uri3/uri3_3", + "/uri4", + "/uri5/uri5_1", + "/uri6/uri6_1", + }; + + for (i = 0; i < (sizeof(bad_uris) / sizeof(bad_uris[0])); i++) { + char bad_header[256]; + const char *bad_uri = bad_uris[i]; + + /* Use precision specifier to suppress false-positive "format-truncation" warning. */ + snprintf(bad_header, sizeof(bad_header), "GET %.199s HTTP/1.1\r\n\r\nBODY", bad_uri); + + fst_check((status = switch_http_parse_header(bad_header, sizeof(bad_header), &request)) == SWITCH_STATUS_FALSE); + + if (status == SWITCH_STATUS_SUCCESS) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Bad uri parsed [%d]: [%s]\n", i, request.uri); + switch_http_free_request(&request); + } + } + + for (i = 0; i < test_uri_count; i++) { + char raw_header[256]; + const char *clear_uri = clear_uris[i]; + const char *raw_uri = raw_uris[i]; + + /* Use precision specifier to suppress false-positive "format-truncation" warning. */ + snprintf(raw_header, sizeof(raw_header), "GET %.199s HTTP/1.1\r\n\r\nBODY", raw_uri); + + fst_check((status = switch_http_parse_header(raw_header, sizeof(raw_header), &request)) == SWITCH_STATUS_SUCCESS); + fst_check_string_equals(clear_uri, request.uri); + + if (status == SWITCH_STATUS_SUCCESS) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "URI [%d]: [%s] => [%s]\n", i, raw_uri, request.uri); + switch_http_free_request(&request); + } + } +} +FST_TEST_END() + FST_SUITE_END() FST_MINCORE_END() From b5c3c86aa078b0e63cdee4361f4b73f5991de0b9 Mon Sep 17 00:00:00 2001 From: Andrey Volk Date: Tue, 26 May 2026 00:25:56 +0300 Subject: [PATCH 11/24] [mod_commands, mod_verto] Add new reloadcert API and let mod_verto reload certificates on the fly without disconnects. (#3033) --- src/include/switch_types.h | 2 + .../applications/mod_commands/mod_commands.c | 18 ++ src/mod/endpoints/mod_verto/mod_verto.c | 208 +++++++++++------- src/switch_event.c | 1 + 4 files changed, 155 insertions(+), 74 deletions(-) diff --git a/src/include/switch_types.h b/src/include/switch_types.h index 294c64ee3f..a68d5bbf70 100644 --- a/src/include/switch_types.h +++ b/src/include/switch_types.h @@ -2075,6 +2075,7 @@ typedef uint32_t switch_io_flag_t; SWITCH_EVENT_CALL_DETAIL SWITCH_EVENT_DEVICE_STATE SWITCH_EVENT_SHUTDOWN_REQUESTED - Shutdown of the system has been requested + SWITCH_EVENT_CERT_RELOAD - SSL/TLS certificates reload has been requested SWITCH_EVENT_ALL - All events at once @@ -2172,6 +2173,7 @@ typedef enum { SWITCH_EVENT_DEVICE_STATE, SWITCH_EVENT_TEXT, SWITCH_EVENT_SHUTDOWN_REQUESTED, + SWITCH_EVENT_CERT_RELOAD, SWITCH_EVENT_ALL } switch_event_types_t; diff --git a/src/mod/applications/mod_commands/mod_commands.c b/src/mod/applications/mod_commands/mod_commands.c index 22e25ea8a6..b44035044a 100644 --- a/src/mod/applications/mod_commands/mod_commands.c +++ b/src/mod/applications/mod_commands/mod_commands.c @@ -2860,6 +2860,22 @@ SWITCH_STANDARD_API(reload_xml_function) return SWITCH_STATUS_SUCCESS; } +SWITCH_STANDARD_API(reload_cert_function) +{ + switch_event_t *event; + + if (switch_event_create(&event, SWITCH_EVENT_CERT_RELOAD) == SWITCH_STATUS_SUCCESS) { + switch_event_fire(&event); + stream->write_function(stream, "+OK cert reload event sent\n"); + + return SWITCH_STATUS_SUCCESS; + } + + stream->write_function(stream, "-ERR failed to create event\n"); + + return SWITCH_STATUS_FALSE; +} + #define KILL_SYNTAX " [cause]" SWITCH_STANDARD_API(kill_function) { @@ -7656,6 +7672,7 @@ SWITCH_MODULE_LOAD_FUNCTION(mod_commands_load) SWITCH_ADD_API(commands_api_interface, "reloadacl", "Reload ACL", reload_acl_function, ""); SWITCH_ADD_API(commands_api_interface, "reload", "Reload module", reload_function, UNLOAD_SYNTAX); SWITCH_ADD_API(commands_api_interface, "reloadxml", "Reload XML", reload_xml_function, ""); + SWITCH_ADD_API(commands_api_interface, "reloadcert", "Reload SSL/TLS certificates", reload_cert_function, ""); SWITCH_ADD_API(commands_api_interface, "replace", "Replace a string", replace_function, "||"); SWITCH_ADD_API(commands_api_interface, "say_string", "", say_string_function, SAY_STRING_SYNTAX); SWITCH_ADD_API(commands_api_interface, "sched_api", "Schedule an api command", sched_api_function, SCHED_SYNTAX); @@ -7831,6 +7848,7 @@ SWITCH_MODULE_LOAD_FUNCTION(mod_commands_load) switch_console_set_complete("add nat_map status"); switch_console_set_complete("add reload ::console::list_loaded_modules"); switch_console_set_complete("add reloadacl reloadxml"); + switch_console_set_complete("add reloadcert"); switch_console_set_complete("add show aliases"); switch_console_set_complete("add show api"); switch_console_set_complete("add show application"); diff --git a/src/mod/endpoints/mod_verto/mod_verto.c b/src/mod/endpoints/mod_verto/mod_verto.c index 1363416139..113a21c579 100644 --- a/src/mod/endpoints/mod_verto/mod_verto.c +++ b/src/mod/endpoints/mod_verto/mod_verto.c @@ -150,6 +150,114 @@ static void verto_deinit_ssl(verto_profile_t *profile) } } +static SSL_CTX *verto_create_ssl_ctx(verto_profile_t *profile, const char **errp) +{ + SSL_CTX *ctx = SSL_CTX_new(profile->ssl_method); + + if (!ctx) { + *errp = "Failed to create SSL context"; + + return NULL; + } + + /* Disable SSLv2 */ + SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2); + /* Disable SSLv3 */ + SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv3); + /* Disable TLSv1 */ + SSL_CTX_set_options(ctx, SSL_OP_NO_TLSv1); + /* Disable Compression CRIME (Compression Ratio Info-leak Made Easy) */ + SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION); + + if (!zstr(profile->chain)) { + if (switch_file_exists(profile->chain, NULL) != SWITCH_STATUS_SUCCESS) { + *errp = "SUPPLIED CHAIN FILE NOT FOUND"; + goto fail; + } + + if (!SSL_CTX_use_certificate_chain_file(ctx, profile->chain)) { + *errp = "CERT CHAIN FILE ERROR"; + goto fail; + } + } + + if (switch_file_exists(profile->cert, NULL) != SWITCH_STATUS_SUCCESS) { + *errp = "SUPPLIED CERT FILE NOT FOUND"; + goto fail; + } + + if (!SSL_CTX_use_certificate_file(ctx, profile->cert, SSL_FILETYPE_PEM)) { + *errp = "CERT FILE ERROR"; + goto fail; + } + + if (switch_file_exists(profile->key, NULL) != SWITCH_STATUS_SUCCESS) { + *errp = "SUPPLIED KEY FILE NOT FOUND"; + goto fail; + } + + if (!SSL_CTX_use_PrivateKey_file(ctx, profile->key, SSL_FILETYPE_PEM)) { + *errp = "PRIVATE KEY FILE ERROR"; + goto fail; + } + + if (!SSL_CTX_check_private_key(ctx)) { + *errp = "PRIVATE KEY FILE ERROR"; + goto fail; + } + + SSL_CTX_set_cipher_list(ctx, "HIGH:!DSS:!aNULL@STRENGTH"); + + return ctx; + + fail: + SSL_CTX_free(ctx); + + return NULL; +} + +static int verto_reload_ssl(verto_profile_t *profile) +{ + const char *err = NULL; + SSL_CTX *new_ctx = verto_create_ssl_ctx(profile, &err); + + if (!new_ctx) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "SSL reload failed for profile %s: %s\n", profile->name, err); + + return 0; + } + + SSL_CTX_free(profile->ssl_ctx); + + profile->ssl_ctx = new_ctx; + profile->ssl_ready = 1; + + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "SSL certificates reloaded for profile %s\n", profile->name); + + return 1; +} + +static void cert_reload_handler(switch_event_t *event) +{ + verto_profile_t *p; + + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Certificate reload event received, processing\n"); + + switch_mutex_lock(verto_globals.mutex); + + for (p = verto_globals.profile_head; p; p = p->next) { + if (p->running) { + switch_thread_rwlock_wrlock(p->rwlock); + verto_reload_ssl(p); + switch_thread_rwlock_unlock(p->rwlock); + } + } + + switch_mutex_unlock(verto_globals.mutex); + + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Certificate reload event processed\n"); +} + static void close_file(ks_socket_t *sock) { if (*sock != KS_SOCK_INVALID) { @@ -174,84 +282,30 @@ void verto_broadcast(const char *event_channel, cJSON *json, const char *key, sw static int verto_init_ssl(verto_profile_t *profile) { - const char *err; + const char *err = NULL; int i = 0; - profile->ssl_method = SSLv23_server_method(); /* create server instance */ - profile->ssl_ctx = SSL_CTX_new(profile->ssl_method); /* create context */ + profile->ssl_method = SSLv23_server_method(); + profile->ssl_ctx = verto_create_ssl_ctx(profile, &err); + + if (!profile->ssl_ctx) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "SSL ERR: %s\n", err); + + profile->ssl_ready = 0; + + for (i = 0; i < profile->i; i++) { + if (profile->ip[i].secure) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "SSL NOT READY FOR LISTENER %s:%d. USE reloadcert AFTER FIXING CERTIFICATES\n", + profile->ip[i].local_ip, profile->ip[i].local_port); + } + } + + return 0; + } + profile->ssl_ready = 1; - assert(profile->ssl_ctx); - - /* Disable SSLv2 */ - SSL_CTX_set_options(profile->ssl_ctx, SSL_OP_NO_SSLv2); - /* Disable SSLv3 */ - SSL_CTX_set_options(profile->ssl_ctx, SSL_OP_NO_SSLv3); - /* Disable TLSv1 */ - SSL_CTX_set_options(profile->ssl_ctx, SSL_OP_NO_TLSv1); - /* Disable Compression CRIME (Compression Ratio Info-leak Made Easy) */ - SSL_CTX_set_options(profile->ssl_ctx, SSL_OP_NO_COMPRESSION); - - /* set the local certificate from CertFile */ - if (!zstr(profile->chain)) { - if (switch_file_exists(profile->chain, NULL) != SWITCH_STATUS_SUCCESS) { - err = "SUPPLIED CHAIN FILE NOT FOUND\n"; - goto fail; - } - - if (!SSL_CTX_use_certificate_chain_file(profile->ssl_ctx, profile->chain)) { - err = "CERT CHAIN FILE ERROR"; - goto fail; - } - } - - if (switch_file_exists(profile->cert, NULL) != SWITCH_STATUS_SUCCESS) { - err = "SUPPLIED CERT FILE NOT FOUND\n"; - goto fail; - } - - if (!SSL_CTX_use_certificate_file(profile->ssl_ctx, profile->cert, SSL_FILETYPE_PEM)) { - err = "CERT FILE ERROR"; - goto fail; - } - - /* set the private key from KeyFile */ - - if (switch_file_exists(profile->key, NULL) != SWITCH_STATUS_SUCCESS) { - err = "SUPPLIED KEY FILE NOT FOUND\n"; - goto fail; - } - - if (!SSL_CTX_use_PrivateKey_file(profile->ssl_ctx, profile->key, SSL_FILETYPE_PEM)) { - err = "PRIVATE KEY FILE ERROR"; - goto fail; - } - - /* verify private key */ - if ( !SSL_CTX_check_private_key(profile->ssl_ctx) ) { - err = "PRIVATE KEY FILE ERROR"; - goto fail; - } - - SSL_CTX_set_cipher_list(profile->ssl_ctx, "HIGH:!DSS:!aNULL@STRENGTH"); return 1; - - fail: - switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "SSL ERR: %s\n", err); - - profile->ssl_ready = 0; - verto_deinit_ssl(profile); - - for (i = 0; i < profile->i; i++) { - if (profile->ip[i].secure) { - switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "SSL NOT ENABLED FOR LISTENER %s:%d. REVERTING TO WS\n", - profile->ip[i].local_ip, profile->ip[i].local_port); - profile->ip[i].secure = 0; - } - } - - return 0; - } @@ -1972,6 +2026,7 @@ static void client_run(jsock_t *jsock) ks_pool_open(&jsock->kpool); + switch_thread_rwlock_rdlock(jsock->profile->rwlock); #if defined(KS_VERSION_NUM) && KS_VERSION_NUM >= 20000 params = ks_json_create_object(); ks_json_add_number_to_object(params, "payload_size_max", 1000000); @@ -1979,8 +2034,10 @@ static void client_run(jsock_t *jsock) #else if (kws_init(&jsock->ws, jsock->client_socket, (jsock->ptype & PTYPE_CLIENT_SSL) ? jsock->profile->ssl_ctx : NULL, 0, flags, jsock->kpool) != KS_STATUS_SUCCESS) { #endif + switch_thread_rwlock_unlock(jsock->profile->rwlock); log_and_exit(SWITCH_LOG_NOTICE, "%s WS SETUP FAILED\n", jsock->name); } + switch_thread_rwlock_unlock(jsock->profile->rwlock); if (kws_test_flag(jsock->ws, KWS_HTTP)) { http_run(jsock); @@ -4690,7 +4747,7 @@ static int start_jsock(verto_profile_t *profile, ks_socket_t sock, int family) for (i = 0; i < profile->i; i++) { if ( profile->server_socket[i] == sock ) { - if (profile->ip[i].secure) { + if (profile->ip[i].secure && profile->ssl_ready) { ptype = PTYPE_CLIENT_SSL; } break; @@ -6887,6 +6944,8 @@ SWITCH_MODULE_LOAD_FUNCTION(mod_verto_load) switch_core_register_secondary_recover_callback(modname, verto_recover_callback); + switch_event_bind(modname, SWITCH_EVENT_CERT_RELOAD, SWITCH_EVENT_SUBCLASS_ANY, cert_reload_handler, NULL); + if (verto_globals.enable_presence) { switch_event_bind(modname, SWITCH_EVENT_CHANNEL_CALLSTATE, SWITCH_EVENT_SUBCLASS_ANY, presence_event_handler, NULL); } @@ -6922,6 +6981,7 @@ SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_verto_shutdown) switch_core_hash_destroy(&json_GLOBALS.store_hash); switch_event_channel_unbind(NULL, verto_broadcast, NULL); + switch_event_unbind_callback(cert_reload_handler); switch_event_unbind_callback(presence_event_handler); switch_event_unbind_callback(event_handler); diff --git a/src/switch_event.c b/src/switch_event.c index 8a8c8d6c35..25bf961512 100644 --- a/src/switch_event.c +++ b/src/switch_event.c @@ -227,6 +227,7 @@ static char *EVENT_NAMES[] = { "DEVICE_STATE", "TEXT", "SHUTDOWN_REQUESTED", + "CERT_RELOAD", "ALL" }; From 2bd6f0116bc704f36a839c9149739b33f8af29cf Mon Sep 17 00:00:00 2001 From: Andrey Volk Date: Tue, 26 May 2026 01:12:37 +0300 Subject: [PATCH 12/24] [mod_sofia] Reload certificates on the fly without disconnects using reloadcert API. (#3034) --- src/mod/endpoints/mod_sofia/mod_sofia.c | 43 +++++++++++++++++++++++++ w32/download_sofia-sip.props | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/mod/endpoints/mod_sofia/mod_sofia.c b/src/mod/endpoints/mod_sofia/mod_sofia.c index 4fd5d57c1f..0bf07b57be 100644 --- a/src/mod/endpoints/mod_sofia/mod_sofia.c +++ b/src/mod/endpoints/mod_sofia/mod_sofia.c @@ -6528,6 +6528,42 @@ char *sofia_stir_shaken_as_create_identity_header(switch_core_session_t *session } +#ifdef HAVE_NUA_RELOAD_TLS +static void sofia_cert_reload_handler(switch_event_t *event) +{ + switch_hash_index_t *hi; + const void *vvar; + void *val; + + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Certificate reload event received, processing\n"); + + switch_mutex_lock(mod_sofia_globals.hash_mutex); + + for (hi = switch_core_hash_first(mod_sofia_globals.profile_hash); hi; hi = switch_core_hash_next(&hi)) { + sofia_profile_t *profile; + + switch_core_hash_this(hi, &vvar, NULL, &val); + profile = (sofia_profile_t *) val; + + if (!sofia_test_pflag(profile, PFLAG_RUNNING) || !profile->nua || !profile->tls_cert_dir) { + continue; + } + + if (strcmp(vvar, profile->name)) { + continue; + } + + nua_reload_tls(profile->nua, profile->tls_cert_dir); + + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "TLS certificate reload signaled for sofia profile %s\n", profile->name); + } + + switch_mutex_unlock(mod_sofia_globals.hash_mutex); + + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Certificate reload event processed\n"); +} +#endif + SWITCH_MODULE_LOAD_FUNCTION(mod_sofia_load) { switch_chat_interface_t *chat_interface; @@ -6694,6 +6730,10 @@ SWITCH_MODULE_LOAD_FUNCTION(mod_sofia_load) switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "Waiting for profiles to start\n"); switch_yield(1500000); +#ifdef HAVE_NUA_RELOAD_TLS + switch_event_bind(modname, SWITCH_EVENT_CERT_RELOAD, SWITCH_EVENT_SUBCLASS_ANY, sofia_cert_reload_handler, NULL); +#endif + if (switch_event_bind(modname, SWITCH_EVENT_CUSTOM, MULTICAST_EVENT, event_handler, NULL) != SWITCH_STATUS_SUCCESS) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Couldn't bind!\n"); switch_goto_status(SWITCH_STATUS_TERM, err); @@ -6876,6 +6916,9 @@ void mod_sofia_shutdown_cleanup(void) { } switch_mutex_unlock(mod_sofia_globals.mutex); +#ifdef HAVE_NUA_RELOAD_TLS + switch_event_unbind_callback(sofia_cert_reload_handler); +#endif switch_event_unbind_callback(sofia_presence_event_handler); switch_event_unbind_callback(general_queue_event_handler); diff --git a/w32/download_sofia-sip.props b/w32/download_sofia-sip.props index 52c1e6fe27..a4cdf6a2d9 100644 --- a/w32/download_sofia-sip.props +++ b/w32/download_sofia-sip.props @@ -29,7 +29,7 @@ Date: Tue, 26 May 2026 18:42:52 +0500 Subject: [PATCH 13/24] [mod_sofia] capture SIP reason header on INVITE failure (#3036) Co-authored-by: Chris Rienzo --- src/mod/endpoints/mod_sofia/sofia.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/mod/endpoints/mod_sofia/sofia.c b/src/mod/endpoints/mod_sofia/sofia.c index 7579c8c03c..62b4963e78 100644 --- a/src/mod/endpoints/mod_sofia/sofia.c +++ b/src/mod/endpoints/mod_sofia/sofia.c @@ -6645,12 +6645,19 @@ static void sofia_handle_sip_r_invite(switch_core_session_t *session, int status } if (status >= 400) { + char *reason_header = NULL; char status_str[5]; switch_snprintf(status_str, sizeof(status_str), "%d", status); switch_channel_set_variable(channel, "sip_invite_failure_status", status_str); switch_channel_set_variable(channel, "sip_invite_failure_phrase", phrase); switch_channel_set_variable_partner(channel, "sip_invite_failure_status", status_str); switch_channel_set_variable_partner(channel, "sip_invite_failure_phrase", phrase); + + reason_header = sip_header_as_string(nua_handle_get_home(nh), (void *) sip->sip_reason); + if (!zstr(reason_header)) { + switch_channel_set_variable(channel, "sip_reason", reason_header); + switch_channel_set_variable_partner(channel, "sip_reason", reason_header); + } } else { switch_channel_set_variable_partner(channel, "sip_invite_failure_status", NULL); switch_channel_set_variable_partner(channel, "sip_invite_failure_phrase", NULL); From 9da537a19f881b67f35bd98895f5939983ccff73 Mon Sep 17 00:00:00 2001 From: Dmitry Verenitsin Date: Tue, 26 May 2026 19:33:23 +0500 Subject: [PATCH 14/24] [mod_sofia] Add SIP 603+ detection and passthrough control. Add unit-tests. (#3035) Implement SIP 603+ (ATIS-1000099) support for FCC analytics-based call blocking compliance. Detection: - Detect incoming 603+ responses by checking "Network Blocked" phrase and "v=analytics1;" in the `Reason` header text - Set `sip_603plus_reason` channel variable on both legs for CDR visibility Passthrough control: - `sip_603plus_passthrough=true`: forward 603+ phrase and Reason header - `sip_603plus_passthrough=false`: strip `Reason` header, send clean `603 Decline` - Not set: existing behavior preserved - Works independently of `disable_q850_reason` for selective forwarding --- conf/vanilla/vars.xml | 7 + src/mod/endpoints/mod_sofia/Makefile.am | 10 +- src/mod/endpoints/mod_sofia/mod_sofia.c | 23 + src/mod/endpoints/mod_sofia/sofia.c | 14 + .../mod_sofia/test/conf/freeswitch.xml | 84 +++ .../endpoints/mod_sofia/test/test_603plus.c | 490 ++++++++++++++++++ 6 files changed, 626 insertions(+), 2 deletions(-) create mode 100644 src/mod/endpoints/mod_sofia/test/test_603plus.c diff --git a/conf/vanilla/vars.xml b/conf/vanilla/vars.xml index 54a15a2534..ae7c355f99 100644 --- a/conf/vanilla/vars.xml +++ b/conf/vanilla/vars.xml @@ -430,4 +430,11 @@ + + + diff --git a/src/mod/endpoints/mod_sofia/Makefile.am b/src/mod/endpoints/mod_sofia/Makefile.am index 3a5d295ddb..378ba06c73 100644 --- a/src/mod/endpoints/mod_sofia/Makefile.am +++ b/src/mod/endpoints/mod_sofia/Makefile.am @@ -15,7 +15,7 @@ mod_sofia_la_SOURCES = mod_sofia_la_LIBADD = $(switch_builddir)/libfreeswitch.la libsofiamod.la mod_sofia_la_LDFLAGS = -avoid-version -module -no-undefined -shared $(SOFIA_SIP_LIBS) $(STIRSHAKEN_LIBS) -noinst_PROGRAMS = test/test_sofia_funcs test/test_nuafail test/sipp-based-tests +noinst_PROGRAMS = test/test_sofia_funcs test/test_nuafail test/sipp-based-tests test/test_603plus test_test_sofia_funcs_SOURCES = test/test_sofia_funcs.c test_test_sofia_funcs_CFLAGS = $(AM_CFLAGS) $(SOFIA_SIP_CFLAGS) $(STIRSHAKEN_CFLAGS) -DSWITCH_TEST_BASE_DIR_FOR_CONF=\"${abs_builddir}/test\" -DSWITCH_TEST_BASE_DIR_OVERRIDE=\"${abs_builddir}/test\" @@ -25,6 +25,11 @@ endif test_test_sofia_funcs_LDFLAGS = $(AM_LDFLAGS) -avoid-version -no-undefined $(freeswitch_LDFLAGS) $(switch_builddir)/libfreeswitch.la $(CORE_LIBS) $(APR_LIBS) $(STIRSHAKEN_LIBS) test_test_sofia_funcs_LDADD = libsofiamod.la $(SOFIA_SIP_LIBS) $(STIRSHAKEN_LIBS) +test_test_603plus_SOURCES = test/test_603plus.c +test_test_603plus_CFLAGS = $(AM_CFLAGS) $(SOFIA_SIP_CFLAGS) -DSWITCH_TEST_BASE_DIR_FOR_CONF=\"${abs_builddir}/test\" -DSWITCH_TEST_BASE_DIR_OVERRIDE=\"${abs_builddir}/test\" +test_test_603plus_LDFLAGS = $(AM_LDFLAGS) -avoid-version -no-undefined $(freeswitch_LDFLAGS) $(switch_builddir)/libfreeswitch.la $(CORE_LIBS) $(APR_LIBS) +test_test_603plus_LDADD = libsofiamod.la $(SOFIA_SIP_LIBS) + test_test_nuafail_SOURCES = test/test_nuafail.c test_test_nuafail_CFLAGS = $(AM_CFLAGS) $(SOFIA_SIP_CFLAGS) $(STIRSHAKEN_CFLAGS) -DSWITCH_TEST_BASE_DIR_FOR_CONF=\"${abs_builddir}/test\" -DSWITCH_TEST_BASE_DIR_OVERRIDE=\"${abs_builddir}/test\" if HAVE_STIRSHAKEN @@ -38,13 +43,14 @@ test_sipp_based_tests_CFLAGS = $(AM_CFLAGS) $(SOFIA_SIP_CFLAGS) -DSWITCH_TEST_BA test_sipp_based_tests_LDFLAGS = $(AM_LDFLAGS) -avoid-version -no-undefined $(freeswitch_LDFLAGS) $(switch_builddir)/libfreeswitch.la $(CORE_LIBS) $(APR_LIBS) test_sipp_based_tests_LDADD = libsofiamod.la $(SOFIA_SIP_LIBS) -TESTS = test/test_sofia_funcs.sh test/test_nuafail +TESTS = test/test_sofia_funcs.sh test/test_nuafail test/test_603plus #TESTS += test/test_run_sipp.sh if ISMAC mod_sofia_la_LDFLAGS += -framework CoreFoundation -framework SystemConfiguration test_test_sofia_funcs_LDFLAGS += -framework CoreFoundation -framework SystemConfiguration test_test_nuafail_LDFLAGS += -framework CoreFoundation -framework SystemConfiguration +test_test_603plus_LDFLAGS += -framework CoreFoundation -framework SystemConfiguration test_sipp_based_tests_LDFLAGS += -framework CoreFoundation -framework SystemConfiguration endif diff --git a/src/mod/endpoints/mod_sofia/mod_sofia.c b/src/mod/endpoints/mod_sofia/mod_sofia.c index 0bf07b57be..47b2d0681e 100644 --- a/src/mod/endpoints/mod_sofia/mod_sofia.c +++ b/src/mod/endpoints/mod_sofia/mod_sofia.c @@ -493,6 +493,7 @@ switch_status_t sofia_on_hangup(switch_core_session_t *session) const char *val = NULL; const char *max_forwards = switch_channel_get_variable(channel, SWITCH_MAX_FORWARDS_VARIABLE); const char *call_info = switch_channel_get_variable(channel, "presence_call_info_full"); + const char *passthrough_603plus = switch_channel_get_variable(channel, "sip_603plus_passthrough"); const char *session_id_header = sofia_glue_session_id_header(session, tech_pvt->profile); val = switch_channel_get_variable(tech_pvt->channel, "disable_q850_reason"); @@ -512,6 +513,23 @@ switch_status_t sofia_on_hangup(switch_core_session_t *session) } } + /* 603+ (ATIS-1000099) Reason header override — applied after standard reason construction. + * + * passthrough=true: Restore 603+ Reason even if disable_q850_reason suppressed it. + * Allows selective forwarding of 603+ while suppressing other Reason headers. + * passthrough=false: Strip Reason header entirely — send clean 603 Decline with no Reason. */ + if (passthrough_603plus) { + const char *reason_603plus = switch_channel_get_variable(channel, "sip_603plus_reason"); + + if (!zstr(reason_603plus)) { + if (switch_true(passthrough_603plus)) { + reason = switch_core_session_sprintf(session, "%s", reason_603plus); + } else if (switch_false(passthrough_603plus)) { + reason = switch_core_session_sprintf(session, ""); + } + } + } + if (switch_channel_test_flag(channel, CF_INTERCEPT) || cause == SWITCH_CAUSE_PICKED_OFF || cause == SWITCH_CAUSE_LOSE_RACE) { switch_channel_set_variable(channel, "call_completed_elsewhere", "true"); } @@ -557,6 +575,11 @@ switch_status_t sofia_on_hangup(switch_core_session_t *session) if (tech_pvt->respond_phrase) { //phrase = su_strdup(nua_handle_home(tech_pvt->nh), tech_pvt->respond_phrase); phrase = tech_pvt->respond_phrase; + } else if (sip_cause == 603 + && !zstr(reason) + && switch_true(passthrough_603plus) + && !zstr(switch_channel_get_variable(channel, "sip_603plus_reason"))) { + phrase = "Network Blocked"; } else { phrase = sip_status_phrase(sip_cause); } diff --git a/src/mod/endpoints/mod_sofia/sofia.c b/src/mod/endpoints/mod_sofia/sofia.c index 62b4963e78..8d82e9d3fd 100644 --- a/src/mod/endpoints/mod_sofia/sofia.c +++ b/src/mod/endpoints/mod_sofia/sofia.c @@ -6658,9 +6658,23 @@ static void sofia_handle_sip_r_invite(switch_core_session_t *session, int status switch_channel_set_variable(channel, "sip_reason", reason_header); switch_channel_set_variable_partner(channel, "sip_reason", reason_header); } + + /* 603+ (ATIS-1000099) detection: clear stale state from serial forking, then check */ + switch_channel_set_variable(channel, "sip_603plus_reason", NULL); + switch_channel_set_variable_partner(channel, "sip_603plus_reason", NULL); + + if (status == 603 && phrase && !strcasecmp(phrase, "Network Blocked") + && sip->sip_reason && sip->sip_reason->re_text + && !strncmp(sip->sip_reason->re_text, "\"v=analytics1;", 14) + && !zstr(reason_header)) { + + switch_channel_set_variable(channel, "sip_603plus_reason", reason_header); + switch_channel_set_variable_partner(channel, "sip_603plus_reason", reason_header); + } } else { switch_channel_set_variable_partner(channel, "sip_invite_failure_status", NULL); switch_channel_set_variable_partner(channel, "sip_invite_failure_phrase", NULL); + switch_channel_set_variable_partner(channel, "sip_603plus_reason", NULL); } if (status >= 400 && sip->sip_reason && sip->sip_reason->re_protocol && (!strcasecmp(sip->sip_reason->re_protocol, "Q.850") diff --git a/src/mod/endpoints/mod_sofia/test/conf/freeswitch.xml b/src/mod/endpoints/mod_sofia/test/conf/freeswitch.xml index fa626af93e..6a2959918e 100644 --- a/src/mod/endpoints/mod_sofia/test/conf/freeswitch.xml +++ b/src/mod/endpoints/mod_sofia/test/conf/freeswitch.xml @@ -140,6 +140,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/mod/endpoints/mod_sofia/test/test_603plus.c b/src/mod/endpoints/mod_sofia/test/test_603plus.c new file mode 100644 index 0000000000..658881c884 --- /dev/null +++ b/src/mod/endpoints/mod_sofia/test/test_603plus.c @@ -0,0 +1,490 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2005-2026, Anthony Minessale II + * + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * + * The Initial Developer of the Original Code is + * Anthony Minessale II + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Dmitry Verenitsin + * + * + * test_603plus.c -- Tests for SIP 603+ (ATIS-1000099) detection and passthrough + * + * Detection requires BOTH: + * 1. SIP status 603 with phrase "Network Blocked" (case-insensitive) + * 2. Reason header text starts with "v=analytics1;" (ATIS version AVP) + * + * Test approach: originate via loopback gateway (same FS instance). + * The responding extension sends a crafted 603 with/without Reason header. + * We bind to CHANNEL_HANGUP_COMPLETE to capture sip_603plus_reason from + * the outbound leg before it is destroyed. + * + * Passthrough tests use a bridge scenario: originate -> middle extension + * (sets passthrough) -> bridges to 603+ target. The originate leg receives + * the response FROM the middle box, letting us verify what was actually sent. + */ + +#include +#include + +/* Event capture state */ + +static struct { + char sip_603plus_reason[1024]; + char sip_invite_failure_phrase[256]; + char sip_reason[1024]; + switch_bool_t received; +} capture; + +static void reset_capture(void) +{ + memset(capture.sip_603plus_reason, 0, sizeof(capture.sip_603plus_reason)); + memset(capture.sip_invite_failure_phrase, 0, sizeof(capture.sip_invite_failure_phrase)); + memset(capture.sip_reason, 0, sizeof(capture.sip_reason)); + capture.received = SWITCH_FALSE; +} + +static void on_hangup_complete(switch_event_t *event) +{ + const char *direction, *val; + + /* Only capture from outbound legs (the originating call, not the responder). + * In bridge tests, multiple outbound legs hang up (bridge B-leg, then originate O-leg). + * Reset on every outbound event so the last one (O-leg) wins cleanly. */ + direction = switch_event_get_header(event, "Call-Direction"); + if (zstr(direction) || strcmp(direction, "outbound")) return; + + reset_capture(); + + val = switch_event_get_header(event, "variable_sip_603plus_reason"); + if (!zstr(val)) { + switch_snprintf(capture.sip_603plus_reason, sizeof(capture.sip_603plus_reason), "%s", val); + } + + val = switch_event_get_header(event, "variable_sip_invite_failure_phrase"); + if (!zstr(val)) { + switch_snprintf(capture.sip_invite_failure_phrase, sizeof(capture.sip_invite_failure_phrase), "%s", val); + } + + val = switch_event_get_header(event, "variable_sip_reason"); + if (!zstr(val)) { + switch_snprintf(capture.sip_reason, sizeof(capture.sip_reason), "%s", val); + } + + capture.received = SWITCH_TRUE; +} + +static void originate_and_wait(const char *dest, switch_call_cause_t *cause) +{ + switch_core_session_t *session = NULL; + + switch_ivr_originate(NULL, &session, cause, + dest, 2, NULL, NULL, NULL, NULL, NULL, SOF_NONE, NULL, NULL); + + if (session) { + switch_channel_hangup(switch_core_session_get_channel(session), SWITCH_CAUSE_NORMAL_CLEARING); + switch_core_session_rwunlock(session); + } + + /* Let event dispatch thread deliver CHANNEL_HANGUP_COMPLETE */ + switch_yield(1000000); +} + +/* Test suite */ + +FST_CORE_EX_BEGIN("./conf", SCF_VG | SCF_USE_SQL) +{ +FST_MODULE_BEGIN(mod_sofia, sofia) +{ + FST_SETUP_BEGIN() + { + } + FST_SETUP_END() + + FST_TEARDOWN_BEGIN() + { + } + FST_TEARDOWN_END() + + /* Detection: positive cases */ + + FST_TEST_BEGIN(detect_valid_603plus_sip) + { + /* + * Extension +15553336050 sends: + * 603 Network Blocked + * Reason: SIP;cause=603;text="v=analytics1;url=https://example.com/redress";location=TN + * + * Both conditions met -> sip_603plus_reason MUST be set. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336050", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(!zstr_buf(capture.sip_603plus_reason), "sip_603plus_reason must be set for valid 603+"); + fst_xcheck(!!strstr(capture.sip_603plus_reason, "v=analytics1"), "sip_603plus_reason must contain v=analytics1"); + fst_xcheck(!strcasecmp(capture.sip_invite_failure_phrase, "Network Blocked"), "Failure phrase must be 'Network Blocked'"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(detect_valid_603plus_q850) + { + /* + * Extension +15553336051 sends: + * 603 Network Blocked + * Reason: Q.850;cause=21;text="v=analytics1;url=https://example.com/redress";location=LN + * + * Q.850 protocol is equally valid per ATIS-1000099 section 4.1.1. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336051", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(!zstr_buf(capture.sip_603plus_reason), "sip_603plus_reason must be set for Q.850 ATIS Reason"); + fst_xcheck(!!strstr(capture.sip_603plus_reason, "v=analytics1"), "sip_603plus_reason must contain v=analytics1"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(detect_603plus_after_180) + { + /* + * Extension +15553336056 sends 180 Ringing, waits 500ms, then: + * 603 Network Blocked + * Reason: SIP;cause=603;text="v=analytics1;url=https://example.com/redress";location=TN + * + * Detection must work after provisional responses. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336056", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(!zstr_buf(capture.sip_603plus_reason), "sip_603plus_reason must be set after 180+603"); + fst_xcheck(!!strstr(capture.sip_603plus_reason, "v=analytics1"), "sip_603plus_reason must contain v=analytics1"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + /* Detection: negative cases */ + + FST_TEST_BEGIN(detect_wrong_phrase) + { + /* + * Extension +15553336052 sends: + * 603 Decline <- wrong phrase + * Reason: SIP;cause=603;text="v=analytics1;url=https://example.com/redress";location=TN + * + * Phrase is "Decline", not "Network Blocked". Detection must NOT fire. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336052", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(zstr_buf(capture.sip_603plus_reason), "sip_603plus_reason must NOT be set when phrase is 'Decline'"); + /* sip_reason should still be set (existing behavior for any Reason header) */ + fst_xcheck(!zstr_buf(capture.sip_reason), "sip_reason should be set regardless of phrase"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(detect_no_analytics_in_reason) + { + /* + * Extension +15553336053 sends: + * 603 Network Blocked + * Reason: Q.850;cause=21;text="Call Rejected" <- no v=analytics1 + * + * Reason header lacks v=analytics1. Detection must NOT fire. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336053", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(zstr_buf(capture.sip_603plus_reason), "sip_603plus_reason must NOT be set without v=analytics1"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(detect_no_reason_header) + { + /* + * Extension +15553336054 sends: + * 603 Network Blocked + * (no Reason header -- disable_q850_reason=true suppresses it) + * + * No Reason header -> sip->sip_reason is NULL. Detection must NOT fire. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336054", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(zstr_buf(capture.sip_603plus_reason), "sip_603plus_reason must NOT be set without Reason header"); + fst_xcheck(zstr_buf(capture.sip_reason), "sip_reason should not be set when Reason header is suppressed"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(detect_non_603_status) + { + /* + * Extension +15553336055 sends: + * 486 Busy Here <- not 603 + * Reason: SIP;cause=603;text="v=analytics1;url=https://example.com/redress";location=TN + * + * Status code is 486, not 603. Detection must NOT fire. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336055", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_USER_BUSY, "Expected USER_BUSY for 486"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(zstr_buf(capture.sip_603plus_reason), "sip_603plus_reason must NOT be set for non-603 status"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + /* + * Passthrough behavior. + * + * Bridge scenario: originate -> middle extension (sets passthrough) -> bridges to 603+ target. + * The originate leg receives the response FROM the middle box. We capture its + * sip_invite_failure_phrase and sip_reason to verify what was actually sent. + */ + + FST_TEST_BEGIN(passthrough_true) + { + /* + * Extension +15553336060 sets sip_603plus_passthrough=true, bridges to 603+ target. + * The middle box should forward both "Network Blocked" phrase and ATIS Reason header. + * Our originate leg should see a valid 603+. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336060", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(!strcasecmp(capture.sip_invite_failure_phrase, "Network Blocked"), + "passthrough=true must preserve 'Network Blocked' phrase"); + fst_xcheck(!zstr_buf(capture.sip_603plus_reason), + "passthrough=true must result in valid 603+ on originate leg"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(passthrough_false) + { + /* + * Extension +15553336061 sets sip_603plus_passthrough=false, bridges to 603+ target. + * The middle box should strip the ATIS Reason and use default phrase "Decline". + * Our originate leg should NOT see a 603+. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336061", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(strcasecmp(capture.sip_invite_failure_phrase, "Network Blocked") != 0, + "passthrough=false must NOT send 'Network Blocked' phrase"); + fst_xcheck(zstr_buf(capture.sip_603plus_reason), + "passthrough=false must strip ATIS Reason (no 603+ on originate leg)"); + fst_xcheck(zstr_buf(capture.sip_reason), + "passthrough=false must suppress Reason header entirely"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(passthrough_default) + { + /* + * Extension +15553336062 does NOT set sip_603plus_passthrough, bridges to 603+ target. + * Default: phrase is "Decline" (existing behavior), but ATIS Reason leaks through. + * This is the backward-compatible state. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336062", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(strcasecmp(capture.sip_invite_failure_phrase, "Network Blocked") != 0, + "default passthrough must NOT change phrase (stays 'Decline')"); + /* ATIS Reason leaks through via sip_reason -- this is existing behavior */ + fst_xcheck(!zstr_buf(capture.sip_reason), + "default passthrough: sip_reason should still be set (existing behavior)"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + /* + * disable_q850_reason + passthrough combinations. + * + * Tests that disable_q850_reason and sip_603plus_passthrough work independently. + * disable_q850_reason suppresses standard Reason headers; + * sip_603plus_passthrough controls 603+ ATIS Reason forwarding. + */ + + FST_TEST_BEGIN(disable_reason_passthrough_true) + { + /* + * Extension +15553336063: disable_q850_reason=true + sip_603plus_passthrough=true. + * Standard Reason suppressed, but ATIS 603+ Reason restored. + * The customer use case: suppress all Reason headers except FCC-required 603+. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336063", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(!strcasecmp(capture.sip_invite_failure_phrase, "Network Blocked"), + "disable_q850+passthrough=true must preserve 'Network Blocked' phrase"); + fst_xcheck(!zstr_buf(capture.sip_603plus_reason), + "disable_q850+passthrough=true must restore ATIS Reason"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(disable_reason_passthrough_false) + { + /* + * Extension +15553336064: disable_q850_reason=true + sip_603plus_passthrough=false. + * Both suppress -- no Reason header at all. + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336064", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(strcasecmp(capture.sip_invite_failure_phrase, "Network Blocked") != 0, + "disable_q850+passthrough=false must NOT send 'Network Blocked' phrase"); + fst_xcheck(zstr_buf(capture.sip_reason), + "disable_q850+passthrough=false must suppress Reason header entirely"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + + FST_TEST_BEGIN(disable_reason_passthrough_default) + { + /* + * Extension +15553336065: disable_q850_reason=true, passthrough not set. + * disable_q850_reason suppresses everything, passthrough not set = no override. + * No Reason header, phrase is "Decline". + */ + switch_call_cause_t cause; + + switch_event_bind("test_603plus", SWITCH_EVENT_CHANNEL_HANGUP_COMPLETE, + SWITCH_EVENT_SUBCLASS_ANY, on_hangup_complete, NULL); + + reset_capture(); + originate_and_wait("sofia/gateway/test/+15553336065", &cause); + + fst_xcheck(cause == SWITCH_CAUSE_CALL_REJECTED, "Expected CALL_REJECTED for 603"); + fst_xcheck(capture.received == SWITCH_TRUE, "Should have received outbound hangup event"); + fst_xcheck(strcasecmp(capture.sip_invite_failure_phrase, "Network Blocked") != 0, + "disable_q850+default must NOT send 'Network Blocked' phrase"); + fst_xcheck(zstr_buf(capture.sip_reason), + "disable_q850+default must suppress Reason header"); + + switch_event_unbind_callback(on_hangup_complete); + } + FST_TEST_END() + +} +FST_MODULE_END() +} +FST_CORE_END() From bf9c95e8909b33ecbede63fa4649edf4f3434257 Mon Sep 17 00:00:00 2001 From: Dmitry Verenitsin Date: Tue, 26 May 2026 20:11:11 +0500 Subject: [PATCH 15/24] [core] Use switch_stun_ipv6_t for STUN IPv6 write paths. (#3037) Route IPv6 writes in `switch_stun_packet_attribute_add_binded_address` and `switch_stun_packet_attribute_add_xor_binded_address` through `switch_stun_ipv6_t` (16-byte `address[]`) instead of `switch_stun_ip_t` (4-byte `uint32_t address`). Add IPv4/IPv6 unit tests for both encoders. Co-authored-by: Andrey Volk --- src/switch_stun.c | 52 +++------ tests/unit/Makefile.am | 1 + tests/unit/conf_stun/freeswitch.xml | 73 ++++++++++++ tests/unit/switch_stun.c | 172 ++++++++++++++++++++++++++++ 4 files changed, 265 insertions(+), 33 deletions(-) create mode 100644 tests/unit/conf_stun/freeswitch.xml create mode 100644 tests/unit/switch_stun.c diff --git a/src/switch_stun.c b/src/switch_stun.c index 35c9daed91..1c0027e1a1 100644 --- a/src/switch_stun.c +++ b/src/switch_stun.c @@ -485,31 +485,24 @@ SWITCH_DECLARE(switch_stun_packet_t *) switch_stun_packet_build_header(switch_st SWITCH_DECLARE(uint8_t) switch_stun_packet_attribute_add_binded_address(switch_stun_packet_t *packet, char *ipstr, uint16_t port, int family) { switch_stun_packet_attribute_t *attribute; - switch_stun_ip_t *ip; attribute = (switch_stun_packet_attribute_t *) ((uint8_t *) & packet->first_attribute + ntohs(packet->header.length)); attribute->type = htons(SWITCH_STUN_ATTR_XOR_MAPPED_ADDRESS); if (family == AF_INET6) { + switch_stun_ipv6_t *ipv6 = (switch_stun_ipv6_t *) attribute->value; + attribute->length = htons(20); + ipv6->family = 2; + ipv6->port = htons(port ^ (STUN_MAGIC_COOKIE >> 16)); + inet_pton(AF_INET6, ipstr, ipv6->address); } else { + switch_stun_ip_t *ip = (switch_stun_ip_t *) attribute->value; + attribute->length = htons(8); - } - - ip = (switch_stun_ip_t *) attribute->value; - - ip->port = htons(port ^ (STUN_MAGIC_COOKIE >> 16)); - - if (family == AF_INET6) { - ip->family = 2; - } else { ip->family = 1; - } - - if (family == AF_INET6) { - inet_pton(AF_INET6, ipstr, (struct in6_addr *) &ip->address); - } else { - inet_pton(AF_INET, ipstr, (int *) &ip->address); + ip->port = htons(port ^ (STUN_MAGIC_COOKIE >> 16)); + inet_pton(AF_INET, ipstr, &ip->address); } packet->header.length += htons(sizeof(switch_stun_packet_attribute_t)) + attribute->length; @@ -519,32 +512,25 @@ SWITCH_DECLARE(uint8_t) switch_stun_packet_attribute_add_binded_address(switch_s SWITCH_DECLARE(uint8_t) switch_stun_packet_attribute_add_xor_binded_address(switch_stun_packet_t *packet, char *ipstr, uint16_t port, int family) { switch_stun_packet_attribute_t *attribute; - switch_stun_ip_t *ip; attribute = (switch_stun_packet_attribute_t *) ((uint8_t *) & packet->first_attribute + ntohs(packet->header.length)); attribute->type = htons(SWITCH_STUN_ATTR_XOR_MAPPED_ADDRESS); if (family == AF_INET6) { + switch_stun_ipv6_t *ipv6 = (switch_stun_ipv6_t *) attribute->value; + attribute->length = htons(20); + ipv6->family = 2; + ipv6->port = htons(port ^ (STUN_MAGIC_COOKIE >> 16)); + inet_pton(AF_INET6, ipstr, ipv6->address); + v6_xor(ipv6->address, (uint8_t *)packet->header.id); } else { + switch_stun_ip_t *ip = (switch_stun_ip_t *) attribute->value; + attribute->length = htons(8); - } - - ip = (switch_stun_ip_t *) attribute->value; - - ip->port = htons(port ^ (STUN_MAGIC_COOKIE >> 16)); - - if (family == AF_INET6) { - ip->family = 2; - } else { ip->family = 1; - } - - if (family == AF_INET6) { - inet_pton(AF_INET6, ipstr, (struct in6_addr *) &ip->address); - v6_xor((uint8_t *)&ip->address, (uint8_t *)packet->header.id); - } else { - inet_pton(AF_INET, ipstr, (int *) &ip->address); + ip->port = htons(port ^ (STUN_MAGIC_COOKIE >> 16)); + inet_pton(AF_INET, ipstr, &ip->address); ip->address = htonl(ntohl(ip->address) ^ STUN_MAGIC_COOKIE); } diff --git a/tests/unit/Makefile.am b/tests/unit/Makefile.am index 719152d6df..2f83bca68f 100644 --- a/tests/unit/Makefile.am +++ b/tests/unit/Makefile.am @@ -3,6 +3,7 @@ include $(top_srcdir)/build/modmake.rulesam noinst_PROGRAMS = switch_event switch_hash switch_ivr_originate switch_utils switch_core switch_console switch_vpx switch_core_file \ switch_ivr_play_say switch_core_codec switch_rtp switch_xml noinst_PROGRAMS += switch_core_video switch_core_db switch_vad switch_packetizer switch_core_session test_sofia switch_ivr_async switch_core_asr switch_log +noinst_PROGRAMS += switch_stun noinst_PROGRAMS += test_tts_format noinst_PROGRAMS+= switch_hold switch_sip diff --git a/tests/unit/conf_stun/freeswitch.xml b/tests/unit/conf_stun/freeswitch.xml new file mode 100644 index 0000000000..c9ad71b4df --- /dev/null +++ b/tests/unit/conf_stun/freeswitch.xml @@ -0,0 +1,73 @@ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + +
+
diff --git a/tests/unit/switch_stun.c b/tests/unit/switch_stun.c new file mode 100644 index 0000000000..675333c029 --- /dev/null +++ b/tests/unit/switch_stun.c @@ -0,0 +1,172 @@ +/* +* FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application +* Copyright (C) 2005-2026, Anthony Minessale II +* +* Version: MPL 1.1 +* +* The contents of this file are subject to the Mozilla Public License Version +* 1.1 (the "License"); you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* http://www.mozilla.org/MPL/ +* +* Software distributed under the License is distributed on an "AS IS" basis, +* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +* for the specific language governing rights and limitations under the +* License. +* +* The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application +* +* The Initial Developer of the Original Code is +* Anthony Minessale II +* Portions created by the Initial Developer are Copyright (C) +* the Initial Developer. All Rights Reserved. +* +* Contributor(s): +* Dmitry Verenitsin +* +* switch_stun.c -- tests STUN (https://www.rfc-editor.org/rfc/rfc5389). +*/ + + +#include +#include +#include + +FST_CORE_BEGIN("./conf_stun") +{ +FST_SUITE_BEGIN(switch_stun) +{ +FST_SETUP_BEGIN() +{ +} +FST_SETUP_END() + +FST_TEARDOWN_BEGIN() +{ +} +FST_TEARDOWN_END() + + FST_TEST_BEGIN(test_stun_add_binded_address_ipv6) + { + /* + * Encode an IPv6 XOR-MAPPED-ADDRESS attribute and verify the + * attribute type, length, address family, and the raw 16-byte + * address payload at its expected offset inside the value. + */ + uint8_t buf[512]; + switch_stun_packet_t *packet; + switch_stun_packet_attribute_t *attr; + const char *ipv6_str = "2001:db8::dead:beef"; + uint8_t expected[16]; + uint8_t *value_bytes; + + memset(buf, 0, sizeof(buf)); + packet = switch_stun_packet_build_header(SWITCH_STUN_BINDING_RESPONSE, NULL, buf); + fst_xcheck(inet_pton(AF_INET6, ipv6_str, expected) == 1, "test IPv6 literal parses"); + + switch_stun_packet_attribute_add_binded_address(packet, (char *)ipv6_str, 12345, AF_INET6); + + attr = (switch_stun_packet_attribute_t *)packet->first_attribute; + fst_xcheck(ntohs(attr->type) == SWITCH_STUN_ATTR_XOR_MAPPED_ADDRESS, "attribute type is XOR_MAPPED_ADDRESS"); + fst_xcheck(ntohs(attr->length) == 20, "attribute length is 20 for IPv6"); + + /* Attribute value layout: wasted(1) + family(1) + port(2) + address(16). */ + value_bytes = (uint8_t *)attr->value; + fst_xcheck(value_bytes[1] == 2, "attribute family byte is 2 for IPv6"); + fst_xcheck(memcmp(value_bytes + 4, expected, 16) == 0, "16-byte IPv6 address written at offset 4 of attribute value"); + } + FST_TEST_END() + + FST_TEST_BEGIN(test_stun_add_xor_binded_address_ipv6) + { + /* + * Encode then decode an IPv6 XOR-MAPPED-ADDRESS attribute and + * confirm the round-trip recovers the original IPv6 string — + * the write path must XOR the address with the transaction ID + * symmetrically to the read path. + */ + uint8_t buf[512]; + switch_stun_packet_t *packet; + switch_stun_packet_attribute_t *attr; + const char *ipv6_str = "2001:db8::dead:beef"; + char out_ip[64] = { 0 }; + uint16_t out_port = 0; + + memset(buf, 0, sizeof(buf)); + packet = switch_stun_packet_build_header(SWITCH_STUN_BINDING_RESPONSE, NULL, buf); + + switch_stun_packet_attribute_add_xor_binded_address(packet, (char *)ipv6_str, 12345, AF_INET6); + + attr = (switch_stun_packet_attribute_t *)packet->first_attribute; + fst_xcheck(ntohs(attr->type) == SWITCH_STUN_ATTR_XOR_MAPPED_ADDRESS, "attribute type is XOR_MAPPED_ADDRESS"); + fst_xcheck(ntohs(attr->length) == 20, "attribute length is 20 for IPv6"); + + switch_stun_packet_attribute_get_xor_mapped_address(attr, &packet->header, out_ip, sizeof(out_ip), &out_port); + fst_check_string_equals(out_ip, ipv6_str); + } + FST_TEST_END() + + FST_TEST_BEGIN(test_stun_add_binded_address_ipv4) + { + /* + * Encode an IPv4 XOR-MAPPED-ADDRESS attribute and verify the + * attribute type, length, address family, and the raw 4-byte + * address payload at its expected offset inside the value. + */ + uint8_t buf[512]; + switch_stun_packet_t *packet; + switch_stun_packet_attribute_t *attr; + const char *ipv4_str = "192.0.2.42"; + uint8_t expected[4]; + uint8_t *value_bytes; + + memset(buf, 0, sizeof(buf)); + packet = switch_stun_packet_build_header(SWITCH_STUN_BINDING_RESPONSE, NULL, buf); + fst_xcheck(inet_pton(AF_INET, ipv4_str, expected) == 1, "test IPv4 literal parses"); + + switch_stun_packet_attribute_add_binded_address(packet, (char *)ipv4_str, 12345, AF_INET); + + attr = (switch_stun_packet_attribute_t *)packet->first_attribute; + fst_xcheck(ntohs(attr->type) == SWITCH_STUN_ATTR_XOR_MAPPED_ADDRESS, "attribute type is XOR_MAPPED_ADDRESS"); + fst_xcheck(ntohs(attr->length) == 8, "attribute length is 8 for IPv4"); + + /* Attribute value layout: wasted(1) + family(1) + port(2) + address(4). */ + value_bytes = (uint8_t *)attr->value; + fst_xcheck(value_bytes[1] == 1, "attribute family byte is 1 for IPv4"); + fst_xcheck(memcmp(value_bytes + 4, expected, 4) == 0, "4-byte IPv4 address written at offset 4 of attribute value"); + } + FST_TEST_END() + + FST_TEST_BEGIN(test_stun_add_xor_binded_address_ipv4) + { + /* + * Encode then decode an IPv4 XOR-MAPPED-ADDRESS attribute and + * confirm the round-trip recovers the original IPv4 string — + * the write path must XOR the address with the magic cookie + * symmetrically to the read path. + */ + uint8_t buf[512]; + switch_stun_packet_t *packet; + switch_stun_packet_attribute_t *attr; + const char *ipv4_str = "192.0.2.42"; + char out_ip[64] = { 0 }; + uint16_t out_port = 0; + + memset(buf, 0, sizeof(buf)); + packet = switch_stun_packet_build_header(SWITCH_STUN_BINDING_RESPONSE, NULL, buf); + + switch_stun_packet_attribute_add_xor_binded_address(packet, (char *)ipv4_str, 12345, AF_INET); + + attr = (switch_stun_packet_attribute_t *)packet->first_attribute; + fst_xcheck(ntohs(attr->type) == SWITCH_STUN_ATTR_XOR_MAPPED_ADDRESS, "attribute type is XOR_MAPPED_ADDRESS"); + fst_xcheck(ntohs(attr->length) == 8, "attribute length is 8 for IPv4"); + + switch_stun_packet_attribute_get_xor_mapped_address(attr, &packet->header, out_ip, sizeof(out_ip), &out_port); + fst_check_string_equals(out_ip, ipv4_str); + } + FST_TEST_END() +} +FST_SUITE_END() +} +FST_CORE_END() + From e3dc9950fd68c10389df40c87bbd1c7ea9d562ce Mon Sep 17 00:00:00 2001 From: MarioG-X <20360699+MarioG-X@users.noreply.github.com> Date: Tue, 26 May 2026 09:29:55 -0700 Subject: [PATCH 16/24] [GHA] Update ffmpeg and libpq in macos.yml ffmpeg@5 changed to ffmpeg@7 Note: tested ffmpeg@8 but it causes missing ft2build.h in truetype include library. libpq@16 changed to libpq@18 Co-authored-by: Andrey Volk --- .github/workflows/macos.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 24546bfbb3..e51ff574a8 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -35,11 +35,11 @@ jobs: autoconf \ automake \ curl \ - ffmpeg@5 \ + ffmpeg@7 \ gnu-sed \ jpeg \ ldns \ - libpq@16 \ + libpq@18 \ libsndfile \ libtool \ lua \ @@ -60,8 +60,8 @@ jobs: signalwire/homebrew-signalwire/spandsp \ && \ brew link --force --overwrite \ - ffmpeg@5 \ - libpq@16 + ffmpeg@7 \ + libpq@18 - name: Bootstrap FreeSWITCH run: ./bootstrap.sh -j From 33ee3663bbe634ebc7a909bec787dcaf2434f37f Mon Sep 17 00:00:00 2001 From: Dmitry Verenitsin Date: Wed, 27 May 2026 00:02:42 +0500 Subject: [PATCH 17/24] Merge commit from fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cap `Content-Length` at `HTTP_POST_MAX_BODY` (10 MiB) and size the allocation to the actual body length (`content_length + 1` for the trailing NUL). Also fix `WS_BLOCK` units — `kws_raw_read` takes ms, set to 10000. --- src/mod/endpoints/mod_verto/mod_verto.c | 9 +- tests/unit/Makefile.am | 1 + tests/unit/conf_verto/freeswitch.xml | 47 ++++ tests/unit/conf_verto/verto.conf.xml | 36 +++ tests/unit/test_mod_verto.c | 316 ++++++++++++++++++++++++ 5 files changed, 405 insertions(+), 4 deletions(-) create mode 100644 tests/unit/conf_verto/freeswitch.xml create mode 100644 tests/unit/conf_verto/verto.conf.xml create mode 100644 tests/unit/test_mod_verto.c diff --git a/src/mod/endpoints/mod_verto/mod_verto.c b/src/mod/endpoints/mod_verto/mod_verto.c index 113a21c579..e4a9dc3fda 100644 --- a/src/mod/endpoints/mod_verto/mod_verto.c +++ b/src/mod/endpoints/mod_verto/mod_verto.c @@ -41,6 +41,7 @@ SWITCH_MODULE_RUNTIME_FUNCTION(mod_verto_runtime); SWITCH_MODULE_DEFINITION(mod_verto, mod_verto_load, mod_verto_shutdown, mod_verto_runtime); #define HTTP_CHUNK_SIZE 1024 * 32 +#define HTTP_POST_MAX_BODY (10 * 1024 * 1024) /* max accepted Content-Length for form-urlencoded POST */ #define EP_NAME "verto.rtc" //#define WSS_STANDALONE 1 #include "libks/ks.h" @@ -1824,7 +1825,7 @@ new_req: char *buffer = NULL; switch_ssize_t len = 0, bytes = 0; - if (request->content_length && request->content_length > 10 * 1024 * 1024 - 1) { + if (request->content_length && request->content_length >= HTTP_POST_MAX_BODY) { char *data = "HTTP/1.1 413 Request Entity Too Large\r\n" "Content-Length: 0\r\n\r\n"; kws_raw_write(jsock->ws, data, strlen(data)); @@ -1832,16 +1833,16 @@ new_req: goto done; } - if (!(buffer = malloc(2 * 1024 * 1024))) { + if (!(buffer = malloc(request->content_length + 1))) { goto request_err; } while(bytes < (switch_ssize_t)request->content_length) { len = request->content_length - bytes; -#define WS_BLOCK 1 +#define WS_BLOCK 10000 /* ms; matches libks's internal default */ - if ((len = kws_raw_read(jsock->ws, buffer + bytes, len, WS_BLOCK)) < 0) { + if ((len = kws_raw_read(jsock->ws, buffer + bytes, len, WS_BLOCK)) <= 0) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Read error %" SWITCH_SSIZE_T_FMT"\n", len); goto done; } diff --git a/tests/unit/Makefile.am b/tests/unit/Makefile.am index 2f83bca68f..47542e34ea 100644 --- a/tests/unit/Makefile.am +++ b/tests/unit/Makefile.am @@ -6,6 +6,7 @@ noinst_PROGRAMS += switch_core_video switch_core_db switch_vad switch_packetizer noinst_PROGRAMS += switch_stun noinst_PROGRAMS += test_tts_format noinst_PROGRAMS+= switch_hold switch_sip +noinst_PROGRAMS += test_mod_verto if HAVE_PCAP noinst_PROGRAMS += switch_rtp_pcap diff --git a/tests/unit/conf_verto/freeswitch.xml b/tests/unit/conf_verto/freeswitch.xml new file mode 100644 index 0000000000..fb316ffde4 --- /dev/null +++ b/tests/unit/conf_verto/freeswitch.xml @@ -0,0 +1,47 @@ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/tests/unit/conf_verto/verto.conf.xml b/tests/unit/conf_verto/verto.conf.xml new file mode 100644 index 0000000000..ece12290ae --- /dev/null +++ b/tests/unit/conf_verto/verto.conf.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/unit/test_mod_verto.c b/tests/unit/test_mod_verto.c new file mode 100644 index 0000000000..c3f414af07 --- /dev/null +++ b/tests/unit/test_mod_verto.c @@ -0,0 +1,316 @@ +/* + * FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * Copyright (C) 2026, Anthony Minessale II + * + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application + * + * The Initial Developer of the Original Code is + * Anthony Minessale II + * Portions created by the Initial Developer are Copyright (C) + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dmitry Verenitsin + * + * + * test_mod_verto.c -- Tests for mod_verto + * + */ + +#include +#include + +#define VERTO_TEST_HOST "127.0.0.1" +#define VERTO_TEST_PORT 33081 + +/* Must match HTTP_POST_MAX_BODY in src/mod/endpoints/mod_verto/mod_verto.c */ +#define VERTO_POST_MAX_BODY (10 * 1024 * 1024) + +static switch_status_t verto_connect(switch_socket_t **sock_out, switch_memory_pool_t *pool) +{ + switch_sockaddr_t *addr = NULL; + switch_socket_t *sock = NULL; + int attempts; + + if (switch_sockaddr_info_get(&addr, VERTO_TEST_HOST, SWITCH_UNSPEC, + VERTO_TEST_PORT, 0, pool) != SWITCH_STATUS_SUCCESS) { + return SWITCH_STATUS_FALSE; + } + + for (attempts = 0; attempts < 50; attempts++) { + if (switch_socket_create(&sock, switch_sockaddr_get_family(addr), + SOCK_STREAM, SWITCH_PROTO_TCP, pool) != SWITCH_STATUS_SUCCESS) { + return SWITCH_STATUS_FALSE; + } + switch_socket_opt_set(sock, SWITCH_SO_TCP_NODELAY, 1); + + if (switch_socket_connect(sock, addr) == SWITCH_STATUS_SUCCESS) { + *sock_out = sock; + return SWITCH_STATUS_SUCCESS; + } + + switch_socket_close(sock); + sock = NULL; + switch_yield(100000); + } + + return SWITCH_STATUS_FALSE; +} + +static switch_status_t send_all(switch_socket_t *sock, const char *buf, switch_size_t len) +{ + switch_size_t remaining = len; + const char *p = buf; + + while (remaining > 0) { + switch_size_t n = remaining; + if (switch_socket_send(sock, p, &n) != SWITCH_STATUS_SUCCESS) { + return SWITCH_STATUS_FALSE; + } + if (n == 0) { + return SWITCH_STATUS_FALSE; + } + p += n; + remaining -= n; + } + return SWITCH_STATUS_SUCCESS; +} + +static switch_size_t read_status_line(switch_socket_t *sock, char *out, switch_size_t cap) +{ + switch_size_t got = 0; + + while (got < cap - 1) { + switch_size_t want = cap - 1 - got; + if (switch_socket_recv(sock, out + got, &want) != SWITCH_STATUS_SUCCESS || want == 0) { + break; + } + got += want; + if (memchr(out, '\n', got)) break; + } + out[got] = '\0'; + return got; +} + +FST_CORE_DB_BEGIN("./conf_verto") +{ + FST_SUITE_BEGIN(test_mod_verto) + { + FST_SETUP_BEGIN() + { + fst_requires_module("mod_verto"); + switch_yield(500000); + } + FST_SETUP_END() + + FST_TEARDOWN_BEGIN() + { + } + FST_TEARDOWN_END() + + FST_TEST_BEGIN(post_at_cap_returns_413) + { + switch_memory_pool_t *pool = NULL; + switch_socket_t *sock = NULL; + char req[256]; + char resp[64] = { 0 }; + switch_size_t req_len; + + do { + if (switch_core_new_memory_pool(&pool) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not allocate memory pool"); + break; + } + if (verto_connect(&sock, pool) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not connect to verto listener"); + break; + } + + req_len = switch_snprintf(req, sizeof(req), + "POST / HTTP/1.1\r\n" + "Host: " VERTO_TEST_HOST "\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-Length: %d\r\n" + "\r\n", + VERTO_POST_MAX_BODY); + + if (send_all(sock, req, req_len) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not send request"); + break; + } + + read_status_line(sock, resp, sizeof(resp)); + fst_check_string_starts_with(resp, "HTTP/1.1 413"); + } while (0); + + if (sock) switch_socket_close(sock); + if (pool) switch_core_destroy_memory_pool(&pool); + } + FST_TEST_END() + + FST_TEST_BEGIN(post_small_body_parsed) + { + switch_memory_pool_t *pool = NULL; + switch_socket_t *sock = NULL; + const switch_size_t body_len = 32 * 1024; + char *body = NULL; + char req[256]; + char resp[64] = { 0 }; + switch_size_t req_len; + + do { + if (switch_core_new_memory_pool(&pool) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not allocate memory pool"); + break; + } + if (verto_connect(&sock, pool) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not connect to verto listener"); + break; + } + + body = malloc(body_len); + if (!body) { + fst_fail("could not allocate body buffer"); + break; + } + memset(body, 'x', body_len); + + req_len = switch_snprintf(req, sizeof(req), + "POST / HTTP/1.1\r\n" + "Host: " VERTO_TEST_HOST "\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-Length: %" SWITCH_SIZE_T_FMT "\r\n" + "\r\n", + body_len); + + if (send_all(sock, req, req_len) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not send headers"); + break; + } + if (send_all(sock, body, body_len) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not send body"); + break; + } + + read_status_line(sock, resp, sizeof(resp)); + fst_check_string_starts_with(resp, "HTTP/1.1 "); + fst_xcheck(strncmp(resp, "HTTP/1.1 413", 12) != 0, + "server returned 413 below cap"); + } while (0); + + free(body); + if (sock) switch_socket_close(sock); + if (pool) switch_core_destroy_memory_pool(&pool); + } + FST_TEST_END() + + FST_TEST_BEGIN(post_large_body_no_overflow) + { + switch_memory_pool_t *pool = NULL; + switch_socket_t *sock = NULL; + const switch_size_t body_len = 8 * 1024 * 1024; + char *body = NULL; + char req[256]; + char resp[64] = { 0 }; + switch_size_t req_len; + + do { + if (switch_core_new_memory_pool(&pool) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not allocate memory pool"); + break; + } + if (verto_connect(&sock, pool) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not connect to verto listener"); + break; + } + + body = malloc(body_len); + if (!body) { + fst_fail("could not allocate body buffer"); + break; + } + memset(body, 'x', body_len); + + req_len = switch_snprintf(req, sizeof(req), + "POST / HTTP/1.1\r\n" + "Host: " VERTO_TEST_HOST "\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-Length: %" SWITCH_SIZE_T_FMT "\r\n" + "\r\n", + body_len); + + if (send_all(sock, req, req_len) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not send headers"); + break; + } + if (send_all(sock, body, body_len) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not send body"); + break; + } + + read_status_line(sock, resp, sizeof(resp)); + fst_check_string_starts_with(resp, "HTTP/1.1 "); + fst_xcheck(strncmp(resp, "HTTP/1.1 413", 12) != 0, + "server returned 413 below cap"); + } while (0); + + free(body); + if (sock) switch_socket_close(sock); + if (pool) switch_core_destroy_memory_pool(&pool); + } + FST_TEST_END() + + FST_TEST_BEGIN(post_overflow_length_returns_413) + { + switch_memory_pool_t *pool = NULL; + switch_socket_t *sock = NULL; + char req[256]; + char resp[64] = { 0 }; + switch_size_t req_len; + + do { + if (switch_core_new_memory_pool(&pool) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not allocate memory pool"); + break; + } + if (verto_connect(&sock, pool) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not connect to verto listener"); + break; + } + + req_len = switch_snprintf(req, sizeof(req), + "POST / HTTP/1.1\r\n" + "Host: " VERTO_TEST_HOST "\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-Length: 9999999999\r\n" + "\r\n"); + + if (send_all(sock, req, req_len) != SWITCH_STATUS_SUCCESS) { + fst_fail("could not send request"); + break; + } + + read_status_line(sock, resp, sizeof(resp)); + fst_check_string_starts_with(resp, "HTTP/1.1 413"); + } while (0); + + if (sock) switch_socket_close(sock); + if (pool) switch_core_destroy_memory_pool(&pool); + } + FST_TEST_END() + } + FST_SUITE_END() +} +FST_CORE_END() From 67b62fb969a65662b8ae178fb4a41dae67ce0eb6 Mon Sep 17 00:00:00 2001 From: Dmitry Verenitsin Date: Wed, 27 May 2026 00:23:35 +0500 Subject: [PATCH 18/24] Merge commit from fork Unchecked `atoi()` on declared payload size let a client request up to `INT_MAX`, forcing the server to write ~20 GB per request via the download phase. Short `#` frames also triggered OOB reads on `s[1..3]`. - Gate `#` branch on `JPFLAG_AUTHED`. - Cap declared size at 10 MiB (`VERTO_SPEED_TEST_MAX_SIZE`). - Replace `atoi()` with bounded `strtol()`. - Require `bytes >= 4` before indexing `s[1..3]`. --- src/mod/endpoints/mod_verto/mod_verto.c | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/mod/endpoints/mod_verto/mod_verto.c b/src/mod/endpoints/mod_verto/mod_verto.c index e4a9dc3fda..c2464d9023 100644 --- a/src/mod/endpoints/mod_verto/mod_verto.c +++ b/src/mod/endpoints/mod_verto/mod_verto.c @@ -43,6 +43,7 @@ SWITCH_MODULE_DEFINITION(mod_verto, mod_verto_load, mod_verto_shutdown, mod_vert #define HTTP_CHUNK_SIZE 1024 * 32 #define HTTP_POST_MAX_BODY (10 * 1024 * 1024) /* max accepted Content-Length for form-urlencoded POST */ #define EP_NAME "verto.rtc" +#define VERTO_SPEED_TEST_MAX_SIZE (10 * 1024 * 1024) //#define WSS_STANDALONE 1 #include "libks/ks.h" @@ -2112,16 +2113,26 @@ static void client_run(jsock_t *jsock) char repl[2048] = ""; switch_time_t a, b; + if (!switch_test_flag(jsock, JPFLAG_AUTHED)) { + die("%s Speed-test request before authentication\n", jsock->name); + } + + if (bytes < 4) { + continue; + } + if (s[1] == 'S' && s[2] == 'P') { if (s[3] == 'U') { - int i, size = 0; + int i; + long size; char *p = s+4; int loops = 0; int rem = 0; int dur = 0, j = 0; - if ((size = atoi(p)) <= 0) { + size = strtol(p, NULL, 10); + if (size <= 0 || size > VERTO_SPEED_TEST_MAX_SIZE) { continue; } @@ -2129,7 +2140,7 @@ static void client_run(jsock_t *jsock) do { bytes = kws_read_frame(jsock->ws, &oc, &data); s = (char *) data; - } while (bytes && data && s[0] == '#' && s[3] == 'B'); + } while (bytes >= 4 && data && s[0] == '#' && s[3] == 'B'); b = switch_time_now(); if (!bytes || !data) continue; From 693f7dc6aad2f2c17499571806da99ec63ce0a79 Mon Sep 17 00:00:00 2001 From: Dmitry Verenitsin Date: Wed, 27 May 2026 00:24:48 +0500 Subject: [PATCH 19/24] Merge commit from fork `process_jrpc()` called `set_session_id()` before `check_auth()`, so an unauthenticated client could insert its jsock into `jsock_hash` under a foreign `sessid` and have `attach_jsock()` evict the prior owner (`verto.punt` + `detach_calls()` + `drop=1`) with no identity check. Move the bind past the auth gate; `JPFLAG_INIT` now means "jsock is bound", not "first frame seen". Additionally, `attach_jsock()` refuses the bind when prior and new jsock are authed under different `uid`s, replying `CODE_AUTH_FAILED` "Session in use". Same-uid reconnect and no-auth profile binds are unchanged. --- src/mod/endpoints/mod_verto/mod_verto.c | 38 ++++++++++++++++++------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/mod/endpoints/mod_verto/mod_verto.c b/src/mod/endpoints/mod_verto/mod_verto.c index c2464d9023..f2233df73a 100644 --- a/src/mod/endpoints/mod_verto/mod_verto.c +++ b/src/mod/endpoints/mod_verto/mod_verto.c @@ -1272,10 +1272,11 @@ static jsock_t *get_jsock(const char *uuid) static void tech_reattach(verto_pvt_t *tech_pvt, jsock_t *jsock); -static void attach_jsock(jsock_t *jsock) +static switch_bool_t attach_jsock(jsock_t *jsock) { jsock_t *jp; int proceed = 1; + switch_bool_t result = SWITCH_TRUE; switch_mutex_lock(verto_globals.jsock_mutex); @@ -1284,6 +1285,17 @@ static void attach_jsock(jsock_t *jsock) if ((jp = switch_core_hash_find(verto_globals.jsock_hash, jsock->uuid_str))) { if (jp == jsock) { proceed = 0; + } else if (!zstr(jp->uid) && !zstr(jsock->uid) && strcmp(jp->uid, jsock->uid)) { + /* Refuse cross-identity takeover when both jsocks are authenticated under different uids. + * Clear uuid_str and set nodelete to prevent any uuid_str-keyed teardown + * (detach_jsock, del_jsock, detach_calls) from touching jp. */ + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, + "User %s blocked from taking over session %s owned by %s\n", + jsock->uid, jsock->uuid_str, jp->uid); + jsock->nodelete = 1; + jsock->uuid_str[0] = '\0'; + proceed = 0; + result = SWITCH_FALSE; } else { cJSON *params = NULL; cJSON *msg = NULL; @@ -1304,6 +1316,7 @@ static void attach_jsock(jsock_t *jsock) } switch_mutex_unlock(verto_globals.jsock_mutex); + return result; } static void detach_jsock(jsock_t *jsock) @@ -1482,10 +1495,8 @@ static void process_jrpc_response(jsock_t *jsock, cJSON *json) { } -static void set_session_id(jsock_t *jsock, const char *uuid) +static switch_bool_t set_session_id(jsock_t *jsock, const char *uuid) { - //cJSON *params, *msg = jrpc_new(0); - if (!zstr(uuid)) { switch_set_string(jsock->uuid_str, uuid); switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "%s re-connecting session %s\n", jsock->name, jsock->uuid_str); @@ -1494,8 +1505,7 @@ static void set_session_id(jsock_t *jsock, const char *uuid) switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "%s new RPC session %s\n", jsock->name, jsock->uuid_str); } - attach_jsock(jsock); - + return attach_jsock(jsock); } static cJSON *process_jrpc(jsock_t *jsock, cJSON *json) @@ -1515,11 +1525,6 @@ static cJSON *process_jrpc(jsock_t *jsock, cJSON *json) sessid = cJSON_GetObjectCstr(params, "sessid"); } - if (!switch_test_flag(jsock, JPFLAG_INIT)) { - set_session_id(jsock, sessid); - switch_set_flag(jsock, JPFLAG_INIT); - } - if (zstr(version) || strcmp(version, "2.0")) { reply = jrpc_new(0); jrpc_add_error(reply, CODE_INVALID, "Invalid message", id); @@ -1546,6 +1551,17 @@ static cJSON *process_jrpc(jsock_t *jsock, cJSON *json) switch_set_flag(jsock, JPFLAG_AUTHED); } + /* Bind only after the auth gate — attach_jsock()'s eviction + * must not be reachable pre-auth. */ + if (!switch_test_flag(jsock, JPFLAG_INIT)) { + if (!set_session_id(jsock, sessid)) { + jrpc_add_error(reply, CODE_AUTH_FAILED, "Session in use", id); + jsock->drop = 1; + goto end; + } + switch_set_flag(jsock, JPFLAG_INIT); + } + if (!method || !(func = jrpc_get_func(jsock, method))) { jrpc_add_error(reply, -32601, "Invalid Method, Missing Method or Permission Denied", id); } else { From 74d320834bd429a59bd72a6f5a2ec1ad0785a7d4 Mon Sep 17 00:00:00 2001 From: Dmitry Verenitsin Date: Wed, 27 May 2026 00:26:29 +0500 Subject: [PATCH 20/24] Merge commit from fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In `check_auth()` the userauth branch committed request `userVariables`, `JPFLAG_RESUME_CALL`, identity fields, ``/``, `dialplan`, and `context` to `jsock` *before* the password compare. On mismatch only `jsock->uid` was reverted; the rest persisted on the socket and leaked into outbound/inbound INVITE setup and `jsapi`/event publishes. Restructure so the gate runs first: pre-scan `` into locals, compare, and on mismatch return FALSE with no `jsock` writes. Identity/vars commits and ``/`` persistence move past the gate. Blind-reg short-circuit and `req_params`/`x_user` ownership preserved on every exit; success-path writes are bit-for-bit equivalent. Side cleanups: - "Login sucessful" → "Login successful" typo; - success log WARNING → NOTICE; - the spurious WARNING "Login sucessful" no longer fires on bad-password attempts that located the user in the directory; --- src/mod/endpoints/mod_verto/mod_verto.c | 117 ++++++++++++++---------- 1 file changed, 67 insertions(+), 50 deletions(-) diff --git a/src/mod/endpoints/mod_verto/mod_verto.c b/src/mod/endpoints/mod_verto/mod_verto.c index f2233df73a..784a1379cb 100644 --- a/src/mod/endpoints/mod_verto/mod_verto.c +++ b/src/mod/endpoints/mod_verto/mod_verto.c @@ -1057,7 +1057,7 @@ static switch_bool_t check_auth(jsock_t *jsock, cJSON *params, int *code, char * if (jsock->profile->chop_domain && (domain = strchr(id, '@'))) { *domain++ = '\0'; } - + } if (jsock->profile->register_domain) { @@ -1087,27 +1087,10 @@ static switch_bool_t check_auth(jsock_t *jsock, cJSON *params, int *code, char * } } - - if ((json_ptr = cJSON_GetObjectItem(params, "userVariables"))) { - cJSON * i; - - switch_mutex_lock(jsock->flag_mutex); - for(i = json_ptr->child; i; i = i->next) { - if (i->type == cJSON_True) { - switch_event_add_header_string(jsock->user_vars, SWITCH_STACK_BOTTOM, i->string, "true"); - } else if (i->type == cJSON_False) { - switch_event_add_header_string(jsock->user_vars, SWITCH_STACK_BOTTOM, i->string, "false"); - } else if (!zstr(i->string) && !zstr(i->valuestring)) { - switch_event_add_header_string(jsock->user_vars, SWITCH_STACK_BOTTOM, i->string, i->valuestring); - } - } - switch_mutex_unlock(jsock->flag_mutex); - } - if (jsock->profile->send_passwd || verto_globals.send_passwd) { switch_event_add_header_string(req_params, SWITCH_STACK_BOTTOM, "user_supplied_pass", passwd); } - + switch_event_add_header_string(req_params, SWITCH_STACK_BOTTOM, "action", "jsonrpc-authenticate"); if (switch_xml_locate_user_merged("id", id, domain, NULL, &x_user, req_params) != SWITCH_STATUS_SUCCESS && !jsock->profile->blind_reg) { @@ -1120,20 +1103,8 @@ static switch_bool_t check_auth(jsock_t *jsock, cJSON *params, int *code, char * const char *use_passwd = NULL, *verto_context = NULL, *verto_dialplan = NULL; time_t now = switch_epoch_time_now(NULL); - switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Login sucessful for user: %s domain: %s\n", id, domain); - - jsock->logintime = now; - jsock->id = switch_core_strdup(jsock->pool, id); - jsock->domain = switch_core_strdup(jsock->pool, domain); - jsock->uid = switch_core_sprintf(jsock->pool, "%s@%s", id, domain); - jsock->ready = 1; - - if (!x_user) { - switch_event_destroy(&req_params); - r = SWITCH_TRUE; - goto end; - } - + /* Pre-scan : extract credentials and verto-context/dialplan + * into locals only. No jsock writes here. */ if ((x_params = switch_xml_child(x_user, "params"))) { for (x_param = switch_xml_child(x_params, "param"); x_param; x_param = x_param->next) { const char *var = switch_xml_attr_soft(x_param, "name"); @@ -1155,8 +1126,63 @@ static switch_bool_t check_auth(jsock_t *jsock, cJSON *params, int *code, char * } else if (!strcasecmp(var, "verto-dialplan")) { verto_dialplan = val; } + } + } - switch_event_add_header_string(jsock->params, SWITCH_STACK_BOTTOM, var, val); + /* Password gate. blind_reg with no x_user passes by config. */ + if (x_user && (zstr(use_passwd) || strcmp(a1_hash ? a1_hash : passwd, use_passwd))) { + *code = CODE_AUTH_FAILED; + switch_snprintf(message, mlen, "Authentication Failure"); + login_fire_custom_event(jsock, params, 0, "Authentication Failure"); + switch_xml_clear_user_cache("id", id, domain); + switch_xml_free(x_user); + switch_event_destroy(&req_params); + goto end; + } + + /* Commit jsock state — reachable only post-gate. */ + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "Login successful for user: %s domain: %s\n", id, domain); + + jsock->logintime = now; + jsock->id = switch_core_strdup(jsock->pool, id); + jsock->domain = switch_core_strdup(jsock->pool, domain); + jsock->uid = switch_core_sprintf(jsock->pool, "%s@%s", id, domain); + + if ((json_ptr = cJSON_GetObjectItem(params, "userVariables"))) { + cJSON *i; + + switch_mutex_lock(jsock->flag_mutex); + for (i = json_ptr->child; i; i = i->next) { + if (i->type == cJSON_True) { + switch_event_add_header_string(jsock->user_vars, SWITCH_STACK_BOTTOM, i->string, "true"); + } else if (i->type == cJSON_False) { + switch_event_add_header_string(jsock->user_vars, SWITCH_STACK_BOTTOM, i->string, "false"); + } else if (!zstr(i->string) && !zstr(i->valuestring)) { + switch_event_add_header_string(jsock->user_vars, SWITCH_STACK_BOTTOM, i->string, i->valuestring); + } + } + switch_mutex_unlock(jsock->flag_mutex); + } + + /* blind_reg path: no XML user located — jsock state already committed above; + * skip directory persistence (params/variables/dialplan/context) and return. */ + if (!x_user) { + switch_event_destroy(&req_params); + /* ready=1 is the last state write so cross-thread readers that + * gate on `ready && !zstr(uid)` see a fully populated jsock. */ + jsock->ready = 1; + r = SWITCH_TRUE; + goto end; + } + + /* Second pass over : persist every entry into jsock->params. + * Pre-scan above only read credentials/verto-context/dialplan into locals. + * Must run post-gate — these headers feed channel variables on later calls. */ + if ((x_params = switch_xml_child(x_user, "params"))) { + for (x_param = switch_xml_child(x_params, "param"); x_param; x_param = x_param->next) { + switch_event_add_header_string(jsock->params, SWITCH_STACK_BOTTOM, + switch_xml_attr_soft(x_param, "name"), + switch_xml_attr_soft(x_param, "value")); } } @@ -1171,7 +1197,7 @@ static switch_bool_t check_auth(jsock_t *jsock, cJSON *params, int *code, char * switch_mutex_unlock(jsock->flag_mutex); switch_clear_flag(jsock, JPFLAG_AUTH_EXPIRED); - + if (!strcmp(var, "login-expires")) { uint32_t tmp = atol(val); @@ -1194,21 +1220,12 @@ static switch_bool_t check_auth(jsock_t *jsock, cJSON *params, int *code, char * jsock->context = switch_core_strdup(jsock->pool, verto_context); } - - if (!use_passwd || zstr(use_passwd) || strcmp(a1_hash ? a1_hash : passwd, use_passwd)) { - r = SWITCH_FALSE; - *code = CODE_AUTH_FAILED; - switch_snprintf(message, mlen, "Authentication Failure"); - jsock->uid = NULL; - login_fire_custom_event(jsock, params, 0, "Authentication Failure"); - switch_xml_clear_user_cache("id", id, domain); - } else { - switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,"auth using %s\n",a1_hash ? "a1-hash" : "username & password"); - r = SWITCH_TRUE; - check_permissions(jsock, x_user, params); - } - - + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,"auth using %s\n",a1_hash ? "a1-hash" : "username & password"); + check_permissions(jsock, x_user, params); + /* ready=1 is the last state write so cross-thread readers that + * gate on `ready && !zstr(uid)` see a fully populated jsock. */ + jsock->ready = 1; + r = SWITCH_TRUE; switch_xml_free(x_user); } From 02ac36bb11975f4382fdb566ccf3dad21986a6ef Mon Sep 17 00:00:00 2001 From: Dmitry Verenitsin Date: Wed, 27 May 2026 00:27:05 +0500 Subject: [PATCH 21/24] Merge commit from fork Lower `CJSON_NESTING_LIMIT` from upstream default 1000 to 64 via `SWITCH_AM_CFLAGS` / `SWITCH_AM_CXXFLAGS`. The mutually recursive `parse_value`/`parse_array`/`parse_object` chain in cJSON consumes ~2 stack frames per nesting level, which can overflow worker threads running on `SWITCH_THREAD_STACKSIZE` (240 KB). --- configure.ac | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/configure.ac b/configure.ac index a9bc51a1a9..bf6421e4e7 100644 --- a/configure.ac +++ b/configure.ac @@ -308,6 +308,11 @@ SWITCH_AM_CXXFLAGS="-I${switch_srcdir}/src/include -I${switch_builddir}/src/incl SWITCH_AM_CPPFLAGS="-I${switch_srcdir}/src/include -I${switch_builddir}/src/include -I${switch_srcdir}/libs/libteletone/src" SWITCH_AM_LDFLAGS="-lm" +# Cap cJSON parser recursion depth. Default upstream limit (1000) can overflow +# small thread stacks; both vendored cJSON copies (src/ and libs/esl/) honor this. +APR_ADDTO(SWITCH_AM_CFLAGS, [-DCJSON_NESTING_LIMIT=64]) +APR_ADDTO(SWITCH_AM_CXXFLAGS, [-DCJSON_NESTING_LIMIT=64]) + #set SOLINK variable based on compiler and host if test "x${ax_cv_c_compiler_vendor}" = "xsun" ; then SOLINK="-Bdynamic -dy -G" From 22de26cc7ca5e05c5300306a285024c221ba9d27 Mon Sep 17 00:00:00 2001 From: Dmitry Verenitsin Date: Wed, 27 May 2026 00:28:23 +0500 Subject: [PATCH 22/24] Merge commit from fork * [libesl] Validate `Content-Length` in `esl_recv_event`. `atol()` accepted negative values, allowing a remote ESL peer to cause a one-byte heap underwrite (`Content-Length: -1`) or NULL-pointer dereference (`Content-Length: -2`, since `esl_assert` compiles out under `NDEBUG`). Reject negative and oversized values, and check `malloc` failure instead of relying on `assert`. Cap at `ESL_MAX_CONTENT_LENGTH` (16 MiB). * [libesl] Add test_recv_event. --- .github/workflows/unit-test.yml | 7 +++ configure.ac | 1 + libs/esl/Makefile.am | 2 +- libs/esl/src/esl.c | 21 +++++-- libs/esl/src/include/esl.h | 2 + libs/esl/tests/Makefile.am | 11 ++++ libs/esl/tests/test_recv_event.c | 96 ++++++++++++++++++++++++++++++++ 7 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 libs/esl/tests/Makefile.am create mode 100644 libs/esl/tests/test_recv_event.c diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 32c1b2c580..5e414ab835 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -107,6 +107,13 @@ jobs: run: | ./run-tests.sh ${{ inputs.total-groups }} ${{ inputs.current-group }} --output-dir logs || exit 1 + - name: Run libesl tests + if: ${{ inputs.current-group == 1 }} + shell: bash + working-directory: ${{ inputs.working-directory }}/../../libs/esl + run: | + make check + - name: Collect unit test logs if: always() shell: bash diff --git a/configure.ac b/configure.ac index bf6421e4e7..1866ee5ca5 100644 --- a/configure.ac +++ b/configure.ac @@ -2145,6 +2145,7 @@ AC_CONFIG_FILES([Makefile build/standalone_module/freeswitch.pc build/modmake.rules libs/esl/Makefile + libs/esl/tests/Makefile libs/esl/perl/Makefile libs/esl/php/Makefile libs/xmlrpc-c/include/xmlrpc-c/config.h diff --git a/libs/esl/Makefile.am b/libs/esl/Makefile.am index 03f530d0bf..34ce19c53a 100644 --- a/libs/esl/Makefile.am +++ b/libs/esl/Makefile.am @@ -1,5 +1,5 @@ AUTOMAKE_OPTIONS = foreign subdir-objects -SUBDIRS = . perl +SUBDIRS = . perl tests MYLIB=./.libs/libesl.a LIBS=-lncurses -lpthread -lm LDFLAGS=-L. $(SYSTEM_LDFLAGS) diff --git a/libs/esl/src/esl.c b/libs/esl/src/esl.c index 6f085e26db..383b981a08 100644 --- a/libs/esl/src/esl.c +++ b/libs/esl/src/esl.c @@ -1349,12 +1349,22 @@ ESL_DECLARE(esl_status_t) esl_recv_event(esl_handle_t *handle, int check_q, esl_ if ((cl = esl_event_get_header(revent, "content-length"))) { char *body; esl_ssize_t sofar = 0; - + len = atol(cl); - body = malloc(len+1); - esl_assert(body); - *(body + len) = '\0'; - + + if (len < 0 || len > ESL_MAX_CONTENT_LENGTH) { + esl_event_destroy(&revent); + goto fail; + } + + body = malloc(len + 1); + if (!body) { + esl_event_destroy(&revent); + goto fail; + } + + body[len] = '\0'; + do { esl_ssize_t r,s = esl_buffer_inuse(handle->packet_buf); @@ -1367,6 +1377,7 @@ ESL_DECLARE(esl_status_t) esl_recv_event(esl_handle_t *handle, int check_q, esl_ if (!(strerror_r(handle->errnum, handle->err, sizeof(handle->err)))) *(handle->err)=0; free(body); + esl_event_destroy(&revent); goto fail; } else if (r == 0) { continue; diff --git a/libs/esl/src/include/esl.h b/libs/esl/src/include/esl.h index 4d2baac871..ab1742be65 100644 --- a/libs/esl/src/include/esl.h +++ b/libs/esl/src/include/esl.h @@ -217,6 +217,8 @@ typedef enum { #define esl_strlen_zero_buf(s) (*(s) == '\0') #define end_of(_s) *(*_s == '\0' ? _s : _s + strlen(_s) - 1) +#define ESL_MAX_CONTENT_LENGTH (16 * 1024 * 1024) + #ifdef WIN32 #include #include diff --git a/libs/esl/tests/Makefile.am b/libs/esl/tests/Makefile.am new file mode 100644 index 0000000000..40b6c7b6fc --- /dev/null +++ b/libs/esl/tests/Makefile.am @@ -0,0 +1,11 @@ +AUTOMAKE_OPTIONS = foreign + +if BUILD_TESTS +noinst_PROGRAMS = test_recv_event +TESTS = $(noinst_PROGRAMS) + +test_recv_event_SOURCES = test_recv_event.c +test_recv_event_CFLAGS = $(AM_CFLAGS) -I$(switch_srcdir)/libs/esl/src/include +test_recv_event_LDADD = $(top_builddir)/libs/esl/libesl.la +test_recv_event_LDFLAGS = $(AM_LDFLAGS) -lpthread -lm +endif diff --git a/libs/esl/tests/test_recv_event.c b/libs/esl/tests/test_recv_event.c new file mode 100644 index 0000000000..568e2a467b --- /dev/null +++ b/libs/esl/tests/test_recv_event.c @@ -0,0 +1,96 @@ +/* + * test_recv_event.c + * + * Verifies that esl_recv_event() rejects out-of-range Content-Length + * values: negative numbers and values above ESL_MAX_CONTENT_LENGTH must + * cause the function to return ESL_FAIL and mark the handle as + * disconnected, leaving no allocated state behind. + * + * POSIX-only: uses socketpair(2). Returns 77 on Windows so automake + * marks the test as skipped. + */ + +#include +#include +#include + +#ifdef _WIN32 + +int main(void) +{ + return 77; +} + +#else + +#include +#include +#include + +#include + +#define TEST_ASSERT(cond) do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL %s:%d: %s\n", \ + __FILE__, __LINE__, #cond); \ + exit(1); \ + } \ +} while (0) + +static void prepare_handle(esl_handle_t *h, esl_socket_t s) +{ + memset(h, 0, sizeof(*h)); + h->sock = s; + h->connected = 1; + TEST_ASSERT(esl_mutex_create(&h->mutex) == ESL_SUCCESS); + TEST_ASSERT(esl_buffer_create(&h->packet_buf, + BUF_CHUNK, BUF_START, 0) == ESL_SUCCESS); +} + +static void expect_rejected(const char *frame, const char *desc) +{ + int sv[2]; + esl_handle_t h; + size_t n = strlen(frame); + ssize_t w; + + fprintf(stderr, " case: %s\n", desc); + + TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); + + prepare_handle(&h, sv[0]); + + w = write(sv[1], frame, n); + TEST_ASSERT(w == (ssize_t) n); + close(sv[1]); + + TEST_ASSERT(esl_recv_event(&h, 0, NULL) == ESL_FAIL); + TEST_ASSERT(h.connected == 0); + + esl_disconnect(&h); +} + +int main(void) +{ + fprintf(stderr, "test_recv_event: invalid Content-Length is rejected\n"); + + expect_rejected( + "Content-Type: text/event-plain\n" + "Content-Length: -1\n\n", + "negative Content-Length: -1"); + + expect_rejected( + "Content-Type: text/event-plain\n" + "Content-Length: -2\n\n", + "negative Content-Length: -2"); + + expect_rejected( + "Content-Type: text/event-plain\n" + "Content-Length: 99999999999\n\n", + "Content-Length above ESL_MAX_CONTENT_LENGTH"); + + fprintf(stderr, "OK\n"); + return 0; +} + +#endif From dc5c802627700ebfd85070e4add8e7006615464b Mon Sep 17 00:00:00 2001 From: Dmitry Verenitsin Date: Wed, 27 May 2026 01:11:19 +0500 Subject: [PATCH 23/24] [libesl] Fix build of tests (#3038) --- libs/esl/tests/Makefile.am | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/esl/tests/Makefile.am b/libs/esl/tests/Makefile.am index 40b6c7b6fc..8e4b096e54 100644 --- a/libs/esl/tests/Makefile.am +++ b/libs/esl/tests/Makefile.am @@ -1,6 +1,5 @@ AUTOMAKE_OPTIONS = foreign -if BUILD_TESTS noinst_PROGRAMS = test_recv_event TESTS = $(noinst_PROGRAMS) @@ -8,4 +7,3 @@ test_recv_event_SOURCES = test_recv_event.c test_recv_event_CFLAGS = $(AM_CFLAGS) -I$(switch_srcdir)/libs/esl/src/include test_recv_event_LDADD = $(top_builddir)/libs/esl/libesl.la test_recv_event_LDFLAGS = $(AM_LDFLAGS) -lpthread -lm -endif From 0f25e294bc8cc1905e2ee1477958397b96b95dc6 Mon Sep 17 00:00:00 2001 From: Andrey Volk Date: Tue, 26 May 2026 23:37:13 +0300 Subject: [PATCH 24/24] swigall (#3039) --- src/mod/languages/mod_managed/managed/swig.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mod/languages/mod_managed/managed/swig.cs b/src/mod/languages/mod_managed/managed/swig.cs index 9bced266e1..acd3aa0f12 100644 --- a/src/mod/languages/mod_managed/managed/swig.cs +++ b/src/mod/languages/mod_managed/managed/swig.cs @@ -39455,6 +39455,7 @@ public enum switch_event_types_t { SWITCH_EVENT_DEVICE_STATE, SWITCH_EVENT_TEXT, SWITCH_EVENT_SHUTDOWN_REQUESTED, + SWITCH_EVENT_CERT_RELOAD, SWITCH_EVENT_ALL }