From b3728145b2727c018597ddb97f05bbe3a3c35d7a Mon Sep 17 00:00:00 2001 From: Daniel Donoghue Date: Tue, 10 Mar 2026 14:49:30 +0100 Subject: [PATCH] res_pjsip_maintenance: Add PJSIP endpoint maintenance mode Introduces res_pjsip_maintenance, a loadable module that allows operators to place individual PJSIP endpoints into maintenance mode at runtime without unregistering or disabling them. While an endpoint is in maintenance mode: * New inbound INVITE and SUBSCRIBE dialogs are rejected with 503 Service Unavailable and a Retry-After: 300 header. * In-progress dialogs (re-INVITE, UPDATE, BYE, etc.) are unaffected and complete normally. * Outbound originations via Dial() or ARI originate are refused before any SIP session is created. State is held in-memory only and is cleared on module unload or Asterisk restart. This module was developed with AI assistance (Claude). All code has been reviewed and tested by the author, who takes full responsibility for the submission. CLI interface: pjsip set maintenance pjsip show maintenance [endpoint] AMI interface: Action: PJSIPSetMaintenance Endpoint: |all State: on|off Action: PJSIPShowMaintenance Endpoint: (optional; omit to list all) Emits PJSIPMaintenanceStatus events per result, followed by PJSIPMaintenanceStatusComplete. State changes also emit an unsolicited PJSIPMaintenanceStatus event. To support outbound blocking, a new session_create callback is added to ast_sip_session_supplement. Supplements that set this callback are invoked at the start of ast_sip_session_create_outgoing() in res_pjsip_session, before any dialog or invite session resources are allocated. res_pjsip_maintenance registers itself as a session supplement and uses this callback to gate outbound session creation on a per-endpoint basis. MODULEINFO: pjproject res_pjsip res_pjsip_session UserNote: New module res_pjsip_maintenance adds runtime maintenance mode for PJSIP endpoints. Use "pjsip set maintenance " to enable or disable, and "pjsip show maintenance" to list affected endpoints. AMI actions PJSIPSetMaintenance and PJSIPShowMaintenance provide programmatic access. No configuration file changes required. DeveloperNote: ast_sip_session_supplement gains a new optional callback - int (*session_create)(struct ast_sip_endpoint *endpoint, const char *destination). It is called from the global supplement list (not per-session) at the start of ast_sip_session_create_outgoing() via ast_sip_session_check_supplement_create(). Returning non-zero blocks the outgoing session. Modules that need to gate outbound SIP session creation should register a supplement with this callback set rather than hooking into chan_pjsip directly. --- include/asterisk/res_pjsip_session.h | 52 ++ res/res_pjsip/pjsip_session.c | 17 + res/res_pjsip_maintenance.c | 754 +++++++++++++++++++++++++++ res/res_pjsip_session.c | 6 + 4 files changed, 829 insertions(+) create mode 100644 res/res_pjsip_maintenance.c diff --git a/include/asterisk/res_pjsip_session.h b/include/asterisk/res_pjsip_session.h index 7279d0d448..f2e741d76f 100644 --- a/include/asterisk/res_pjsip_session.h +++ b/include/asterisk/res_pjsip_session.h @@ -364,6 +364,31 @@ struct ast_sip_session_supplement { * Defaults to AST_SIP_SESSION_BEFORE_MEDIA */ enum ast_sip_session_response_priority response_priority; + /*! + * \brief Called before an outgoing session is created + * + * This is called before the session dialog is created and can be used to + * block the creation of the session entirely. A non-zero return value + * prevents the session from being created. The callback is called from + * the global supplement list, not per-session, so the session does not + * yet exist when this is called. + * + * \since 20.20.0 + * \since 22.10.0 + * \since 23.4.0 + * + * \param endpoint The endpoint the outgoing session would be created for + * \param contact The contact to use for the outgoing session, or NULL + * \param location Name of the location to call, be it named location or explicit URI, or NULL + * \param request_user Optional request user to place in the request URI, or NULL + * \param req_topology The requested stream capabilities + * + * \retval non-zero Block session creation + * \retval 0 Allow session creation + */ + int (*session_create)(struct ast_sip_endpoint *endpoint, + struct ast_sip_contact *contact, const char *location, + const char *request_user, struct ast_stream_topology *req_topology); }; enum ast_sip_session_sdp_stream_defer { @@ -625,6 +650,33 @@ void ast_sip_session_register_supplement_with_module(struct ast_module *module, */ void ast_sip_session_unregister_supplement(struct ast_sip_session_supplement *supplement); +/*! + * \brief Check registered supplements for permission to create an outgoing session + * + * Iterates the global supplement list and calls any registered \c session_create + * callbacks. The first callback to return a non-zero value stops the iteration + * and causes this function to return -1, blocking the session creation. + * + * This is called at the beginning of ast_sip_session_create_outgoing() before + * any dialog or invite session resources are allocated. + * + * \since 20.20.0 + * \since 22.10.0 + * \since 23.4.0 + * + * \param endpoint The endpoint the outgoing session would be created for + * \param contact The contact to use for the outgoing session, or NULL + * \param location Name of the location to call, be it named location or explicit URI, or NULL + * \param request_user Optional request user to place in the request URI, or NULL + * \param req_topology The requested stream capabilities + * + * \retval 0 Session creation is allowed + * \retval -1 Session creation is blocked by a supplement + */ +int ast_sip_session_check_supplement_create(struct ast_sip_endpoint *endpoint, + struct ast_sip_contact *contact, const char *location, + const char *request_user, struct ast_stream_topology *req_topology); + /*! * \brief Add supplements to a SIP session * diff --git a/res/res_pjsip/pjsip_session.c b/res/res_pjsip/pjsip_session.c index 429fc6de25..c40c110a0e 100644 --- a/res/res_pjsip/pjsip_session.c +++ b/res/res_pjsip/pjsip_session.c @@ -108,6 +108,23 @@ int ast_sip_session_add_supplements(struct ast_sip_session *session) return 0; } +int ast_sip_session_check_supplement_create(struct ast_sip_endpoint *endpoint, + struct ast_sip_contact *contact, const char *location, + const char *request_user, struct ast_stream_topology *req_topology) +{ + struct ast_sip_session_supplement *iter; + SCOPED_LOCK(lock, &session_supplements, AST_RWLIST_RDLOCK, AST_RWLIST_UNLOCK); + + AST_RWLIST_TRAVERSE(&session_supplements, iter, next) { + if (iter->session_create && iter->session_create(endpoint, contact, location, + request_user, req_topology)) { + return -1; + } + } + + return 0; +} + void ast_sip_session_remove_supplements(struct ast_sip_session *session) { struct ast_sip_session_supplement *iter; diff --git a/res/res_pjsip_maintenance.c b/res/res_pjsip_maintenance.c new file mode 100644 index 0000000000..455aba83f3 --- /dev/null +++ b/res/res_pjsip_maintenance.c @@ -0,0 +1,754 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2026, Aurora Innovation + * + * Daniel Donoghue + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! + * \file + * \brief PJSIP Endpoint Maintenance Mode + * + * Provides a runtime toggle to place individual PJSIP endpoints into + * maintenance mode. While an endpoint is in maintenance mode: + * + * - New \b inbound out-of-dialog requests are rejected with + * "503 Service Unavailable" and a Retry-After: 300 header + * (except SUBSCRIBE/REGISTER with Expires: 0). + * - \b Outbound originations (Dial, ARI originate) are refused before + * any SIP session or Asterisk channel is created. + * - Active in-progress dialogs (BYE, re-INVITE, UPDATE, etc.) are + * completely unaffected. + * - Existing presence/BLF subscriptions are left to expire naturally. + * + * CLI: + * pjsip set maintenance + * pjsip show maintenance [endpoint] + * + * AMI actions: PJSIPSetMaintenance, PJSIPShowMaintenance + * + * \ingroup res_pjsip + */ + +/*** MODULEINFO + pjproject + res_pjsip + res_pjsip_session + extended + ***/ + +/*** DOCUMENTATION + + + 20.20.0 + 22.10.0 + 23.4.0 + + + Enable or disable maintenance mode for a PJSIP endpoint. + + + + + The PJSIP endpoint name, or all to + toggle maintenance mode for every configured endpoint. + + + Desired maintenance state. + + + + + + + + Enables or disables maintenance mode for the specified PJSIP + endpoint. While in maintenance mode, new inbound out-of-dialog + requests are rejected with 503 Service Unavailable (except + SUBSCRIBE/REGISTER with Expires: 0), and outbound originations via + Dial() or ARI are refused before any SIP session or channel is + created. In-progress dialogs are unaffected. + A PJSIPMaintenanceStatus event is emitted + when the state changes. + + + + + 20.20.0 + 22.10.0 + 23.4.0 + + + Show maintenance mode status for PJSIP endpoints. + + + + + If specified, show the status for this endpoint only. + If omitted, list all endpoints currently in maintenance + mode. + + + + Emits one PJSIPMaintenanceStatus event + per result, followed by a + PJSIPMaintenanceStatusComplete event. + + + + + + 20.20.0 + 22.10.0 + 23.4.0 + + + Reports the maintenance mode state of a PJSIP endpoint. + + + + The PJSIP endpoint name. + + + Current maintenance state. + + + + + + + + Emitted when an endpoint enters or leaves maintenance + mode, and as a list entry in response to + PJSIPShowMaintenance. + + + + ***/ + +#include "asterisk.h" + +#include + +#include "asterisk/res_pjsip.h" +#include "asterisk/res_pjsip_session.h" +#include "asterisk/manager.h" +#include "asterisk/module.h" +#include "asterisk/logger.h" +#include "asterisk/cli.h" +#include "asterisk/sorcery.h" +#include "asterisk/astobj2.h" +#include "asterisk/strings.h" + +enum { + MAINT_HASH_BUCKETS = 53, +}; + +/*! Endpoints currently in maintenance mode. + * Protected by the container's own internal RWLOCK. + * No other locks are ever held simultaneously with this container. + */ +static struct ao2_container *maintenance_set; + +/*! + * \internal + * \brief Add an endpoint to the maintenance set. + * \retval 1 Added successfully. + * \retval 0 Already in maintenance (no-op). + * \retval -1 Allocation failure. + */ +static int maint_set_add(const char *endpoint_name) +{ + char *entry; + + entry = ao2_find(maintenance_set, endpoint_name, OBJ_SEARCH_KEY); + if (entry) { + ao2_ref(entry, -1); + return 0; /* already in maintenance */ + } + return ast_str_container_add(maintenance_set, endpoint_name) ? -1 : 1; +} + +/*! + * \internal + * \brief Remove an endpoint from the maintenance set. + * \retval 1 Removed successfully. + * \retval 0 Was not in maintenance (no-op). + */ +static int maint_set_remove(const char *endpoint_name) +{ + char *entry; + + entry = ao2_find(maintenance_set, endpoint_name, OBJ_SEARCH_KEY | OBJ_UNLINK); + if (!entry) { + return 0; + } + ao2_ref(entry, -1); + return 1; +} + +/*! + * \internal + * \brief Apply a maintenance state change to the maintenance set. + * + * Does not validate endpoint existence; callers are responsible for that. + * Callers are also responsible for emitting log messages and AMI events. + * + * \retval 1 State changed. + * \retval 0 Already in requested state (no-op). + * \retval -1 Allocation failure (enable path only). + */ +static int apply_maintenance_state(const char *endpoint_name, int enable) +{ + return enable ? maint_set_add(endpoint_name) : maint_set_remove(endpoint_name); +} + +/* Session supplement: block outgoing session creation when endpoint is in maintenance. */ + +/*! + * \internal + * \brief Session supplement session_create callback: block outgoing sessions to + * endpoints currently in maintenance mode. + * \retval 1 Endpoint is in maintenance; session creation blocked. + * \retval 0 Endpoint is not in maintenance; session creation allowed. + */ +static int maint_session_create(struct ast_sip_endpoint *endpoint, + struct ast_sip_contact *contact, const char *location, + const char *request_user, struct ast_stream_topology *req_topology) +{ + const char *endpoint_name = ast_sorcery_object_get_id(endpoint); + char *entry = ao2_find(maintenance_set, endpoint_name, OBJ_SEARCH_KEY); + + if (entry) { + ao2_ref(entry, -1); + ast_log(LOG_NOTICE, "PJSIP: Refusing outbound call to endpoint '%s': maintenance mode active\n", + endpoint_name); + return 1; + } + return 0; +} + +static struct ast_sip_session_supplement maintenance_session_supplement = { + .session_create = maint_session_create, + .priority = AST_SIP_SUPPLEMENT_PRIORITY_FIRST, +}; + +/* Inbound request hook for maintenance_pjsip_mod. + * + * For endpoints in maintenance mode, blocks new out-of-dialog requests + * with 503 + Retry-After: 300. Any in-dialog request is passed through + * unmodified. SUBSCRIBE and REGISTER with Expires: 0 are also passed + * through, allowing un-subscribe and de-registration. */ + +static pj_bool_t maintenance_on_rx_request(pjsip_rx_data *rdata) +{ + pjsip_msg *msg = rdata->msg_info.msg; + const pjsip_method *method = &msg->line.req.method; + pjsip_to_hdr *to; + pjsip_expires_hdr *expires_hdr; + struct ast_sip_endpoint *endpoint; + char *entry; + pjsip_hdr hdr_list; + pjsip_generic_int_hdr *retry_after; + static const pj_str_t str_retry_after = { "Retry-After", 11 }; + int is_subscribe; + int is_register; + + is_subscribe = pjsip_method_cmp(method, pjsip_get_subscribe_method()) == 0; + is_register = pjsip_method_cmp(method, pjsip_get_register_method()) == 0; + + /* Any in-dialog request is always allowed through. */ + to = rdata->msg_info.to; + if (to->tag.slen > 0) { + return PJ_FALSE; + } + + /* SUBSCRIBE or REGISTER with Expires: 0: allow un-subscribe / de-register. */ + if (is_subscribe || is_register) { + expires_hdr = pjsip_msg_find_hdr(msg, PJSIP_H_EXPIRES, NULL); + if (expires_hdr && expires_hdr->ivalue == 0) { + return PJ_FALSE; + } + } + + endpoint = ast_pjsip_rdata_get_endpoint(rdata); + if (!endpoint) { + return PJ_FALSE; + } + + entry = ao2_find(maintenance_set, ast_sorcery_object_get_id(endpoint), OBJ_SEARCH_KEY); + if (!entry) { + ao2_ref(endpoint, -1); + return PJ_FALSE; + } + ao2_ref(entry, -1); + + ast_log(LOG_NOTICE, "PJSIP: Endpoint '%s' is in maintenance mode; rejecting new %.*s from %s\n", + ast_sorcery_object_get_id(endpoint), + (int)method->name.slen, method->name.ptr, + rdata->pkt_info.src_name); + + ao2_ref(endpoint, -1); + + pj_list_init(&hdr_list); + retry_after = pjsip_generic_int_hdr_create(rdata->tp_info.pool, + &str_retry_after, 300); + if (retry_after) { + pj_list_push_back(&hdr_list, retry_after); + } + + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 503, NULL, + retry_after ? &hdr_list : NULL, NULL); + + return PJ_TRUE; +} + +static struct pjsip_module maintenance_pjsip_mod = { + .name = { "Maintenance Module", 18 }, + /* + * Run after endpoint identification (endpoint_mod, + * PJSIP_MOD_PRIORITY_TSX_LAYER - 3) so that + * ast_pjsip_rdata_get_endpoint() returns the identified endpoint, + * but before the request authenticator + * (PJSIP_MOD_PRIORITY_APPLICATION - 2) so that a maintenance + * endpoint receives 503 rather than a 401 challenge. + */ + .priority = PJSIP_MOD_PRIORITY_APPLICATION - 3, + .on_rx_request = maintenance_on_rx_request, +}; + +/* Sorcery observer - clean up stale entries when an endpoint is deleted. */ + +static void maint_endpoint_deleted(const void *obj) +{ + maint_set_remove(ast_sorcery_object_get_id(obj)); +} + +static const struct ast_sorcery_observer endpoint_observer = { + .deleted = maint_endpoint_deleted, +}; + +/* CLI helpers */ + +/*! + * \internal + * \brief Tab-complete a PJSIP endpoint name. + */ +static char *cli_complete_endpoint(const char *word) +{ + int wordlen = strlen(word); + struct ao2_container *endpoints; + struct ast_sip_endpoint *endpoint; + struct ao2_iterator i; + + endpoints = ast_sorcery_retrieve_by_prefix(ast_sip_get_sorcery(), + "endpoint", word, wordlen); + if (!endpoints) { + return NULL; + } + + i = ao2_iterator_init(endpoints, 0); + while ((endpoint = ao2_iterator_next(&i))) { + ast_cli_completion_add(ast_strdup(ast_sorcery_object_get_id(endpoint))); + ao2_cleanup(endpoint); + } + ao2_iterator_destroy(&i); + ao2_ref(endpoints, -1); + + return NULL; +} + +/* CLI: pjsip set maintenance */ + +static char *handle_cli_pjsip_set_maintenance(struct ast_cli_entry *e, int cmd, + struct ast_cli_args *a) +{ + struct ast_sip_endpoint *endpoint; + struct ao2_container *all_endpoints; + struct ao2_iterator it; + const char *endpoint_name; + int enable; + int rc; + int count; + int failed; + + switch (cmd) { + case CLI_INIT: + e->command = "pjsip set maintenance"; + e->usage = + "Usage: pjsip set maintenance \n" + " Place a PJSIP endpoint into or out of maintenance mode.\n" + " Use 'all' to toggle maintenance mode for every endpoint.\n" + " While in maintenance mode new inbound out-of-dialog requests\n" + " to that endpoint are rejected with 503 (except SUBSCRIBE/\n" + " REGISTER with Expires: 0), and outbound originations are\n" + " refused.\n"; + return NULL; + case CLI_GENERATE: + if (a->pos == 3) { + static const char * const opts[] = { "on", "off", NULL }; + return ast_cli_complete(a->word, opts, a->n); + } + if (a->pos == 4) { + if (!strncasecmp("all", a->word, strlen(a->word))) { + ast_cli_completion_add(ast_strdup("all")); + } + return cli_complete_endpoint(a->word); + } + return NULL; + } + + if (a->argc != 5) { + return CLI_SHOWUSAGE; + } + + if (!strcasecmp(a->argv[3], "on")) { + enable = 1; + } else if (!strcasecmp(a->argv[3], "off")) { + enable = 0; + } else { + return CLI_SHOWUSAGE; + } + + endpoint_name = a->argv[4]; + + if (!strcasecmp(endpoint_name, "all")) { + all_endpoints = ast_sip_get_endpoints(); + if (!all_endpoints) { + ast_cli(a->fd, "Failed to retrieve endpoint list\n"); + return CLI_SUCCESS; + } + count = 0; + failed = 0; + it = ao2_iterator_init(all_endpoints, 0); + while ((endpoint = ao2_iterator_next(&it))) { + rc = apply_maintenance_state(ast_sorcery_object_get_id(endpoint), enable); + if (rc > 0) { + count++; + } else if (rc < 0) { + failed++; + } + ao2_ref(endpoint, -1); + } + ao2_iterator_destroy(&it); + ao2_ref(all_endpoints, -1); + if (count > 0) { + manager_event(EVENT_FLAG_SYSTEM, "PJSIPMaintenanceStatus", + "Endpoint: all\r\n" + "Status: %s\r\n", + enable ? "enabled" : "disabled"); + ast_log(LOG_NOTICE, "PJSIP: Maintenance mode %s for all endpoints " + "(%d endpoint%s affected)\n", + enable ? "enabled" : "disabled", + count, count == 1 ? "" : "s"); + } + ast_cli(a->fd, "Maintenance mode %s for %d endpoint%s%s\n", + enable ? "ENABLED" : "DISABLED", + count, count == 1 ? "" : "s", + failed ? " (some failed)" : ""); + return CLI_SUCCESS; + } + + endpoint = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", endpoint_name); + if (!endpoint) { + ast_cli(a->fd, "Endpoint '%s' not found\n", endpoint_name); + return CLI_SUCCESS; + } + ao2_ref(endpoint, -1); + + rc = apply_maintenance_state(endpoint_name, enable); + if (rc > 0) { + manager_event(EVENT_FLAG_SYSTEM, "PJSIPMaintenanceStatus", + "Endpoint: %s\r\n" + "Status: %s\r\n", + endpoint_name, enable ? "enabled" : "disabled"); + ast_log(LOG_NOTICE, "PJSIP: Maintenance mode %s for endpoint '%s'\n", + enable ? "enabled" : "disabled", endpoint_name); + ast_cli(a->fd, "Maintenance mode %s for endpoint '%s'\n", + enable ? "ENABLED" : "DISABLED", endpoint_name); + } else if (rc == 0 && enable) { + ast_cli(a->fd, "Endpoint '%s' is already in maintenance mode\n", endpoint_name); + } else if (rc == 0) { + ast_cli(a->fd, "Endpoint '%s' was not in maintenance mode\n", endpoint_name); + } else { + ast_cli(a->fd, "Failed to %s maintenance mode for endpoint '%s'\n", + enable ? "enable" : "disable", endpoint_name); + } + + return CLI_SUCCESS; +} + +/* CLI: pjsip show maintenance [endpoint] */ + +/*! \brief ao2_callback used to print one maintenance entry to the CLI */ +static int cli_maint_entry_cb(void *obj, void *arg, int flags) +{ + const char *name = obj; + int fd = *(int *)arg; + ast_cli(fd, " %-40s ON\n", name); + return 0; +} + +static char *handle_cli_pjsip_show_maintenance(struct ast_cli_entry *e, int cmd, + struct ast_cli_args *a) +{ + const char *endpoint_name; + char *entry; + int fd; + int count; + + switch (cmd) { + case CLI_INIT: + e->command = "pjsip show maintenance"; + e->usage = + "Usage: pjsip show maintenance [endpoint]\n" + " Display endpoints currently in maintenance mode.\n" + " If [endpoint] is given, show the status for that endpoint only.\n"; + return NULL; + case CLI_GENERATE: + if (a->pos == 3) { + return cli_complete_endpoint(a->word); + } + return NULL; + } + + if (a->argc == 4) { + endpoint_name = a->argv[3]; + entry = ao2_find(maintenance_set, endpoint_name, OBJ_SEARCH_KEY); + if (entry) { + ast_cli(a->fd, "Endpoint '%s' is in maintenance mode\n", endpoint_name); + ao2_ref(entry, -1); + } else { + ast_cli(a->fd, "Endpoint '%s' is NOT in maintenance mode\n", endpoint_name); + } + return CLI_SUCCESS; + } + + if (a->argc != 3) { + return CLI_SHOWUSAGE; + } + + ast_cli(a->fd, "\n"); + ast_cli(a->fd, " %-40s %s\n", "Endpoint", "State"); + ast_cli(a->fd, " %-40s -----\n", "----------------------------------------"); + fd = a->fd; + ao2_callback(maintenance_set, OBJ_NODATA, cli_maint_entry_cb, &fd); + + count = ao2_container_count(maintenance_set); + ast_cli(a->fd, "\n %d endpoint%s in maintenance mode\n\n", + count, count == 1 ? "" : "s"); + + return CLI_SUCCESS; +} + +static struct ast_cli_entry cli_maintenance[] = { + AST_CLI_DEFINE(handle_cli_pjsip_set_maintenance, "Set PJSIP endpoint maintenance mode"), + AST_CLI_DEFINE(handle_cli_pjsip_show_maintenance, "Show PJSIP endpoint maintenance status"), +}; + +/* AMI: PJSIPSetMaintenance, PJSIPShowMaintenance */ + +static int ami_set_maintenance(struct mansession *s, const struct message *m) +{ + const char *endpoint_name; + const char *state_str; + struct ast_sip_endpoint *endpoint; + struct ao2_container *all_endpoints; + struct ao2_iterator it; + int enable; + int rc; + + endpoint_name = astman_get_header(m, "Endpoint"); + state_str = astman_get_header(m, "State"); + + if (ast_strlen_zero(endpoint_name)) { + astman_send_error(s, m, "Endpoint parameter missing"); + return 0; + } + if (ast_strlen_zero(state_str)) { + astman_send_error(s, m, "State parameter missing"); + return 0; + } + + if (!strcasecmp(state_str, "on")) { + enable = 1; + } else if (!strcasecmp(state_str, "off")) { + enable = 0; + } else { + astman_send_error(s, m, "State must be 'on' or 'off'"); + return 0; + } + + if (!strcasecmp(endpoint_name, "all")) { + int count = 0; + + all_endpoints = ast_sip_get_endpoints(); + if (!all_endpoints) { + astman_send_error(s, m, "Failed to retrieve endpoint list"); + return 0; + } + it = ao2_iterator_init(all_endpoints, 0); + while ((endpoint = ao2_iterator_next(&it))) { + if (apply_maintenance_state(ast_sorcery_object_get_id(endpoint), enable) > 0) { + count++; + } + ao2_ref(endpoint, -1); + } + ao2_iterator_destroy(&it); + ao2_ref(all_endpoints, -1); + if (count > 0) { + manager_event(EVENT_FLAG_SYSTEM, "PJSIPMaintenanceStatus", + "Endpoint: all\r\n" + "Status: %s\r\n", + enable ? "enabled" : "disabled"); + ast_log(LOG_NOTICE, "PJSIP: Maintenance mode %s for all endpoints " + "(%d endpoint%s affected)\n", + enable ? "enabled" : "disabled", + count, count == 1 ? "" : "s"); + } + astman_send_ack(s, m, + enable ? "Maintenance mode enabled for all endpoints" + : "Maintenance mode disabled for all endpoints"); + return 0; + } + + endpoint = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", endpoint_name); + if (!endpoint) { + astman_send_error_va(s, m, "Endpoint '%s' not found", endpoint_name); + return 0; + } + ao2_ref(endpoint, -1); + + rc = apply_maintenance_state(endpoint_name, enable); + if (rc < 0) { + astman_send_error_va(s, m, "Failed to %s maintenance mode for endpoint '%s'", + enable ? "enable" : "disable", endpoint_name); + } else { + if (rc > 0) { + manager_event(EVENT_FLAG_SYSTEM, "PJSIPMaintenanceStatus", + "Endpoint: %s\r\n" + "Status: %s\r\n", + endpoint_name, enable ? "enabled" : "disabled"); + ast_log(LOG_NOTICE, "PJSIP: Maintenance mode %s for endpoint '%s'\n", + enable ? "enabled" : "disabled", endpoint_name); + } + astman_send_ack(s, m, + enable ? "Maintenance mode enabled" : "Maintenance mode disabled"); + } + + return 0; +} + +/*! \brief ao2_callback used to emit one PJSIPMaintenanceStatus AMI list entry */ +static int ami_maint_entry_cb(void *obj, void *arg, int flags) +{ + const char *name = obj; + struct ast_sip_ami *ami = arg; + struct ast_str *buf; + + buf = ast_sip_create_ami_event("PJSIPMaintenanceStatus", ami); + if (!buf) { + return 0; + } + ast_str_append(&buf, 0, "Endpoint: %s\r\nStatus: enabled\r\n", name); + astman_append(ami->s, "%s\r\n", ast_str_buffer(buf)); + ast_free(buf); + ++ami->count; + + return 0; +} + +static int ami_show_maintenance(struct mansession *s, const struct message *m) +{ + const char *endpoint_name; + struct ast_sip_ami ami; + char *entry; + struct ast_str *buf; + + endpoint_name = astman_get_header(m, "Endpoint"); + + ami.s = s; + ami.m = m; + ami.action_id = astman_get_header(m, "ActionID"); + ami.arg = NULL; + ami.count = 0; + + astman_send_listack(s, m, "Maintenance status events follow", "start"); + + if (!ast_strlen_zero(endpoint_name)) { + buf = ast_sip_create_ami_event("PJSIPMaintenanceStatus", &ami); + if (buf) { + entry = ao2_find(maintenance_set, endpoint_name, OBJ_SEARCH_KEY); + ast_str_append(&buf, 0, "Endpoint: %s\r\nStatus: %s\r\n", + endpoint_name, entry ? "enabled" : "disabled"); + if (entry) { + ao2_ref(entry, -1); + } + astman_append(s, "%s\r\n", ast_str_buffer(buf)); + ast_free(buf); + } + ami.count = 1; + } else { + ao2_callback(maintenance_set, OBJ_NODATA, ami_maint_entry_cb, &ami); + } + + astman_send_list_complete_start(s, m, "PJSIPMaintenanceStatusComplete", ami.count); + astman_send_list_complete_end(s); + + return 0; +} + +/* Module load / unload */ + +static int load_module(void) +{ + maintenance_set = ast_str_container_alloc_options(AO2_ALLOC_OPT_LOCK_RWLOCK, + MAINT_HASH_BUCKETS); + if (!maintenance_set) { + ast_log(LOG_ERROR, "res_pjsip_maintenance: failed to allocate maintenance set\n"); + return AST_MODULE_LOAD_DECLINE; + } + + ast_sorcery_observer_add(ast_sip_get_sorcery(), "endpoint", &endpoint_observer); + ast_sip_register_service(&maintenance_pjsip_mod); + ast_sip_session_register_supplement(&maintenance_session_supplement); + ast_manager_register_xml("PJSIPSetMaintenance", + EVENT_FLAG_SYSTEM, ami_set_maintenance); + ast_manager_register_xml("PJSIPShowMaintenance", + EVENT_FLAG_SYSTEM | EVENT_FLAG_REPORTING, ami_show_maintenance); + ast_cli_register_multiple(cli_maintenance, ARRAY_LEN(cli_maintenance)); + + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + ast_cli_unregister_multiple(cli_maintenance, ARRAY_LEN(cli_maintenance)); + ast_manager_unregister("PJSIPShowMaintenance"); + ast_manager_unregister("PJSIPSetMaintenance"); + ast_sip_session_unregister_supplement(&maintenance_session_supplement); + ast_sip_unregister_service(&maintenance_pjsip_mod); + ast_sorcery_observer_remove(ast_sip_get_sorcery(), "endpoint", &endpoint_observer); + ao2_cleanup(maintenance_set); + maintenance_set = NULL; + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "PJSIP Endpoint Maintenance Mode", + .support_level = AST_MODULE_SUPPORT_EXTENDED, + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_APP_DEPEND, + .requires = "res_pjsip,res_pjsip_session", +); diff --git a/res/res_pjsip_session.c b/res/res_pjsip_session.c index 04e6444689..4a1fb56bfa 100644 --- a/res/res_pjsip_session.c +++ b/res/res_pjsip_session.c @@ -3248,6 +3248,12 @@ struct ast_sip_session *ast_sip_session_create_outgoing(struct ast_sip_endpoint SCOPE_ENTER(1, "%s %s Topology: %s\n", ast_sorcery_object_get_id(endpoint), request_user, ast_str_tmp(256, ast_stream_topology_to_str(req_topology, &STR_TMP))); + if (ast_sip_session_check_supplement_create(endpoint, contact, location, + request_user, req_topology)) { + SCOPE_EXIT_RTN_VALUE(NULL, "%s: Session creation blocked by supplement\n", + ast_sorcery_object_get_id(endpoint)); + } + /* If no location has been provided use the AOR list from the endpoint itself */ if (location || !contact) { location = S_OR(location, endpoint->aors);