Commit 2bd999ae5f for asterisk.org
commit 2bd999ae5f9b8f13af5b7ad6db5647f0fb228ccd
Author: Maximilian Fridrich <m.fridrich@commend.com>
Date: Fri Nov 7 12:26:45 2025 +0100
res_pjsip: Introduce redirect module for handling 3xx responses
This commit introduces a new redirect handling module that provides
infrastructure for following SIP 3xx redirect responses. The redirect
functionality respects the endpoint's redirect_method setting and only
follows redirects when set to 'uri_pjsip'. This infrastructure can be
used by any PJSIP module that needs to handle 3xx redirect responses.
diff --git a/configs/samples/pjsip.conf.sample b/configs/samples/pjsip.conf.sample
index f3619909b6..319a7d5806 100644
--- a/configs/samples/pjsip.conf.sample
+++ b/configs/samples/pjsip.conf.sample
@@ -703,6 +703,9 @@
; (default: "username,ip")
;redirect_method=user ; How redirects received from an endpoint are handled
; (default: "user")
+;follow_redirect_methods= ; A comma-separated list of SIP methods for which redirects are followed.
+ ; Currently, only "message" is supported.
+ ; (default: "")
;mailboxes= ; NOTIFY the endpoint when state changes for any of the specified mailboxes.
; Asterisk will send unsolicited MWI NOTIFY messages to the endpoint when state
; changes happen for any of the specified mailboxes. (default: "")
diff --git a/contrib/ast-db-manage/config/versions/bb6d54e22913_add_follow_redirect_methods_to_ps_.py b/contrib/ast-db-manage/config/versions/bb6d54e22913_add_follow_redirect_methods_to_ps_.py
new file mode 100644
index 0000000000..d73c697fec
--- /dev/null
+++ b/contrib/ast-db-manage/config/versions/bb6d54e22913_add_follow_redirect_methods_to_ps_.py
@@ -0,0 +1,22 @@
+"""add follow_redirect_methods to ps_endpoints
+
+Revision ID: bb6d54e22913
+Revises: dc7c357dc178
+Create Date: 2025-12-12 11:26:36.591932
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = 'bb6d54e22913'
+down_revision = 'dc7c357dc178'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.add_column('ps_endpoints', sa.Column('follow_redirect_methods', sa.String(95)))
+
+
+def downgrade():
+ op.drop_column('ps_endpoints', 'follow_redirect_methods')
diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h
index b46312bced..e122c6e5a4 100644
--- a/include/asterisk/res_pjsip.h
+++ b/include/asterisk/res_pjsip.h
@@ -750,6 +750,16 @@ enum ast_sip_session_redirect {
AST_SIP_REDIRECT_URI_PJSIP,
};
+/*!
+ * \brief SIP methods that are allowed to follow 3xx redirects.
+ *
+ * Used as bit flags in follow_redirect_methods field.
+ */
+enum ast_sip_redirect_method {
+ /*! Allow MESSAGE method to follow redirects */
+ AST_SIP_REDIRECT_METHOD_MESSAGE = (1 << 0),
+};
+
/*!
* \brief Incoming/Outgoing call offer/answer joint codec preference.
*
@@ -1114,6 +1124,8 @@ struct ast_sip_endpoint {
unsigned int allowtransfer;
/*! Method used when handling redirects */
enum ast_sip_session_redirect redirect_method;
+ /*! SIP methods allowed to follow 3xx redirects */
+ struct ast_flags follow_redirect_methods;
/*! Variables set on channel creation */
struct ast_variable *channel_vars;
/*! Whether to place a 'user=phone' parameter into the request URI if user is a number */
@@ -4392,4 +4404,15 @@ const int ast_sip_hangup_sip2cause(int cause);
*/
int ast_sip_str2rc(const char *name);
+/*!
+ * \brief Parses a string representing a q_value to a float.
+ *
+ * Valid q values must be in the range from 0.0 to 1.0 inclusively.
+ *
+ * \param q_value String representing a floating point value
+ *
+ * \retval The parsed qvalue or -1.0 on failure.
+ */
+float ast_sip_parse_qvalue(const char *q_value);
+
#endif /* _RES_PJSIP_H */
diff --git a/include/asterisk/res_pjsip_redirect.h b/include/asterisk/res_pjsip_redirect.h
new file mode 100644
index 0000000000..9de40344c9
--- /dev/null
+++ b/include/asterisk/res_pjsip_redirect.h
@@ -0,0 +1,143 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2025, Commend International
+ *
+ * Maximilian Fridrich <m.fridrich@commend.com>
+ *
+ * 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.
+ */
+
+#ifndef _RES_PJSIP_REDIRECT_H
+#define _RES_PJSIP_REDIRECT_H
+
+#include <pjsip.h>
+
+/*!
+ * \brief Maximum number of redirect hops allowed
+ */
+#define AST_SIP_MAX_REDIRECT_HOPS 5
+
+/*!
+ * \brief Maximum number of redirect contacts to process
+ */
+#define AST_SIP_MAX_REDIRECT_CONTACTS 20
+
+/*!
+ * \brief Opaque structure for redirect state
+ *
+ * This structure encapsulates all state needed for handling
+ * SIP 3xx redirects, including visited URIs for loop detection,
+ * pending contacts for retry logic, and hop counting.
+ */
+struct ast_sip_redirect_state;
+
+/*!
+ * \brief Create a new redirect state
+ *
+ * \param endpoint The SIP endpoint
+ * \param initial_uri The initial URI being contacted (for loop detection)
+ *
+ * \retval NULL on failure
+ * \retval A new redirect state on success
+ *
+ * \note The caller must call ast_sip_redirect_state_destroy() when done
+ */
+struct ast_sip_redirect_state *ast_sip_redirect_state_create(
+ struct ast_sip_endpoint *endpoint,
+ const char *initial_uri);
+
+/*!
+ * \brief Check if redirect should be followed based on endpoint configuration
+ *
+ * \param endpoint The SIP endpoint
+ * \param rdata The redirect response data containing the 3xx response
+ *
+ * \retval 0 if redirect should not be followed
+ * \retval 1 if redirect should be followed
+ *
+ * \note This checks if the status code is 3xx and if the SIP method
+ * (extracted from the CSeq header) is allowed to follow redirects
+ * based on the endpoint's follow_redirect_methods configuration
+ */
+int ast_sip_should_redirect(struct ast_sip_endpoint *endpoint, pjsip_rx_data *rdata);
+
+/*!
+ * \brief Parse a 3xx redirect response and extract contacts
+ *
+ * This function parses all Contact headers from a 3xx response,
+ * extracts q-values, sorts contacts by priority (highest q-value first),
+ * and filters out URIs that would create loops.
+ *
+ * \param rdata The redirect response data
+ * \param state The redirect state
+ *
+ * \retval -1 on failure (hop limit reached, no valid contacts, etc.)
+ * \retval 0 on success (at least one valid contact available)
+ *
+ * \note After calling this, use ast_sip_redirect_get_next_uri() to retrieve URIs
+ */
+int ast_sip_redirect_parse_3xx(pjsip_rx_data *rdata, struct ast_sip_redirect_state *state);
+
+/*!
+ * \brief Get the next redirect URI to try
+ *
+ * This function returns the next contact URI from the redirect response,
+ * ordered by q-value (highest first). It also marks the URI as visited
+ * to prevent loops on subsequent redirects.
+ *
+ * \param state The redirect state
+ * \param uri_out Pointer to store the URI string (caller must free)
+ *
+ * \retval -1 if no more URIs available
+ * \retval 0 on success
+ *
+ * \note The caller must ast_free() the returned URI string
+ */
+int ast_sip_redirect_get_next_uri(struct ast_sip_redirect_state *state, char **uri_out);
+
+/*!
+ * \brief Check if a URI would create a redirect loop
+ *
+ * \param state The redirect state
+ * \param uri The URI to check
+ *
+ * \retval 0 if URI is safe (not visited)
+ * \retval 1 if URI would create a loop (already visited)
+ */
+int ast_sip_redirect_check_loop(const struct ast_sip_redirect_state *state, const char *uri);
+
+/*!
+ * \brief Get the current hop count
+ *
+ * \param state The redirect state
+ *
+ * \return The current hop count
+ */
+int ast_sip_redirect_get_hop_count(const struct ast_sip_redirect_state *state);
+
+/*!
+ * \brief Get the endpoint from the redirect state
+ *
+ * \param state The redirect state
+ *
+ * \return The endpoint (borrowed reference, do not cleanup)
+ */
+struct ast_sip_endpoint *ast_sip_redirect_get_endpoint(const struct ast_sip_redirect_state *state);
+
+/*!
+ * \brief Destroy a redirect state and free all resources
+ *
+ * \param state The redirect state to destroy
+ */
+void ast_sip_redirect_state_destroy(struct ast_sip_redirect_state *state);
+
+#endif /* _RES_PJSIP_REDIRECT_H */
diff --git a/res/res_pjsip.c b/res/res_pjsip.c
index b8460fb939..f0b0e48ebf 100644
--- a/res/res_pjsip.c
+++ b/res/res_pjsip.c
@@ -3498,6 +3498,25 @@ struct pjsip_param *ast_sip_pjsip_uri_get_other_param(pjsip_uri *uri, const pj_s
return NULL;
}
+float ast_sip_parse_qvalue(const char *q_value) {
+ char *end = NULL;
+ float ret = strtof(q_value, &end);
+
+ if (end == q_value) {
+ return -1.0f; /* Not a number. */
+ }
+ if ('\0' != *end) {
+ return -1.0f; /* Extra characters at end of input. */
+ }
+ if (!isfinite(ret)) {
+ return -1.0f; /* NaN or Infinity. */
+ }
+ if (ret > 1.0f || ret < 0.0f) {
+ return -1.0f; /* Out of valid range. */
+ }
+ return ret;
+}
+
/*! \brief Convert SIP hangup causes to Asterisk hangup causes */
const int ast_sip_hangup_sip2cause(int cause)
{
diff --git a/res/res_pjsip/pjsip_config.xml b/res/res_pjsip/pjsip_config.xml
index a36005a2f7..270f79049c 100644
--- a/res/res_pjsip/pjsip_config.xml
+++ b/res/res_pjsip/pjsip_config.xml
@@ -689,6 +689,21 @@
</enumlist>
</description>
</configOption>
+ <configOption name="follow_redirect_methods">
+ <since>
+ <version>20.18.0</version>
+ <version>22.8.0</version>
+ <version>23.2.0</version>
+ </since>
+ <synopsis>Follow 3XX redirect responses for the defined SIP methods.</synopsis>
+ <description><para>
+ This is a comma-delimited, case-insensitive list of SIP methods for which redirects are followed.
+ When a redirect response is received for a SIP request, the redirect is only followed if the
+ method is listed in this config option. For example <literal>MESSAGE</literal>. Currently, only
+ <literal>MESSAGE</literal> is supported.
+ </para>
+ </description>
+ </configOption>
<configOption name="mailboxes">
<since>
<version>12.0.0</version>
diff --git a/res/res_pjsip/pjsip_configuration.c b/res/res_pjsip/pjsip_configuration.c
index 614e55f5b8..e23cc71916 100644
--- a/res/res_pjsip/pjsip_configuration.c
+++ b/res/res_pjsip/pjsip_configuration.c
@@ -601,6 +601,80 @@ static int redirect_method_to_str(const void *obj, const intptr_t *args, char **
return 0;
}
+/*!
+ * \brief Mapping of SIP method names to their corresponding redirect flags
+ */
+struct redirect_method_map {
+ const char *method_name;
+ enum ast_sip_redirect_method flag;
+};
+
+static const struct redirect_method_map redirect_method_mappings[] = {
+ { "message", AST_SIP_REDIRECT_METHOD_MESSAGE },
+};
+
+static int follow_redirect_methods_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)
+{
+ struct ast_sip_endpoint *endpoint = obj;
+ char *methods;
+ char *method;
+ int i;
+
+ /* Clear any existing flags */
+ ast_clear_flag(&endpoint->follow_redirect_methods, ~0);
+
+ if (ast_strlen_zero(var->value)) {
+ return 0;
+ }
+
+ methods = ast_strdupa(var->value);
+ while ((method = ast_strsep(&methods, ',', AST_STRSEP_TRIM))) {
+ int found = 0;
+
+ /* Look up the method in our mapping table */
+ for (i = 0; i < ARRAY_LEN(redirect_method_mappings); i++) {
+ if (!strcasecmp(method, redirect_method_mappings[i].method_name)) {
+ ast_set_flag(&endpoint->follow_redirect_methods, redirect_method_mappings[i].flag);
+ found = 1;
+ break;
+ }
+ }
+
+ if (!found) {
+ ast_log(LOG_ERROR, "Unrecognized SIP method '%s' for follow_redirect_methods on endpoint %s\n",
+ method, ast_sorcery_object_get_id(endpoint));
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+static int follow_redirect_methods_to_str(const void *obj, const intptr_t *args, char **buf)
+{
+ const struct ast_sip_endpoint *endpoint = obj;
+ struct ast_str *str = ast_str_create(64);
+ int first = 1;
+ int i;
+
+ if (!str) {
+ return -1;
+ }
+
+ /* Iterate through all supported methods and append if flag is set */
+ for (i = 0; i < ARRAY_LEN(redirect_method_mappings); i++) {
+ if (ast_test_flag(&endpoint->follow_redirect_methods, redirect_method_mappings[i].flag)) {
+ ast_str_append(&str, 0, "%s%s", first ? "" : ",", redirect_method_mappings[i].method_name);
+ first = 0;
+ }
+ }
+
+ *buf = ast_strdup(ast_str_buffer(str));
+ ast_free(str);
+
+ return 0;
+}
+
static int direct_media_method_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)
{
struct ast_sip_endpoint *endpoint = obj;
@@ -2253,6 +2327,7 @@ int ast_res_pjsip_initialize_configuration(void)
ast_sorcery_object_field_register(sip_sorcery, "endpoint", "media_encryption_optimistic", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, media.rtp.encryption_optimistic));
ast_sorcery_object_field_register(sip_sorcery, "endpoint", "g726_non_standard", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, media.g726_non_standard));
ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "redirect_method", "user", redirect_method_handler, redirect_method_to_str, NULL, 0, 0);
+ ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "follow_redirect_methods", "", follow_redirect_methods_handler, follow_redirect_methods_to_str, NULL, 0, 0);
ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "set_var", "", set_var_handler, set_var_to_str, set_var_to_vl, 0, 0);
ast_sorcery_object_field_register(sip_sorcery, "endpoint", "message_context", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, message_context));
ast_sorcery_object_field_register(sip_sorcery, "endpoint", "accountcode", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, accountcode));
diff --git a/res/res_pjsip/redirect.c b/res/res_pjsip/redirect.c
new file mode 100644
index 0000000000..aa15e6ecf6
--- /dev/null
+++ b/res/res_pjsip/redirect.c
@@ -0,0 +1,464 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2025, Commend International
+ *
+ * Maximilian Fridrich <m.fridrich@commend.com>
+ *
+ * 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.
+ */
+
+#include "asterisk.h"
+
+#include <pjsip.h>
+
+#include "asterisk/linkedlists.h"
+#include "asterisk/res_pjsip.h"
+#include "asterisk/res_pjsip_redirect.h"
+
+/*!
+ * \internal
+ * \brief Visited URI tracking for redirect loop detection
+ */
+struct visited_uri {
+ char uri[PJSIP_MAX_URL_SIZE];
+ AST_LIST_ENTRY(visited_uri) list;
+};
+
+/*!
+ * \internal
+ * \brief Redirect contact with q-value for prioritization
+ */
+struct redirect_contact {
+ char uri[PJSIP_MAX_URL_SIZE];
+ float q_value; /* q-value from Contact header, default 1.0 if not present */
+ AST_LIST_ENTRY(redirect_contact) list;
+};
+
+/*! \brief List of redirect contacts */
+AST_LIST_HEAD_NOLOCK(redirect_contact_list, redirect_contact);
+
+/*!
+ * \brief Redirect state structure
+ */
+struct ast_sip_redirect_state {
+ struct ast_sip_endpoint *endpoint;
+ int hop_count;
+ AST_LIST_HEAD_NOLOCK(, visited_uri) visited_uris;
+ struct redirect_contact_list pending_contacts;
+};
+
+struct ast_sip_redirect_state *ast_sip_redirect_state_create(
+ struct ast_sip_endpoint *endpoint,
+ const char *initial_uri)
+{
+ struct ast_sip_redirect_state *state;
+ struct visited_uri *visited;
+
+ state = ast_calloc(1, sizeof(*state));
+ if (!state) {
+ return NULL;
+ }
+
+ state->endpoint = ao2_bump(endpoint);
+ state->hop_count = 0;
+ AST_LIST_HEAD_INIT_NOLOCK(&state->visited_uris);
+ AST_LIST_HEAD_INIT_NOLOCK(&state->pending_contacts);
+
+ /* Add the initial URI to visited list */
+ if (initial_uri) {
+ visited = ast_calloc(1, sizeof(*visited));
+ if (visited) {
+ ast_copy_string(visited->uri, initial_uri, sizeof(visited->uri));
+ AST_LIST_INSERT_HEAD(&state->visited_uris, visited, list);
+ } else {
+ ast_log(LOG_WARNING, "Redirect: Memory allocation failed for endpoint '%s'. "
+ "Redirect loop detection may be impaired.\n", ast_sorcery_object_get_id(state->endpoint));
+ }
+ }
+
+ return state;
+}
+
+/*!
+ * \brief Mapping of SIP method names to their corresponding redirect flags
+ */
+struct redirect_method_map {
+ const char *method_name;
+ enum ast_sip_redirect_method flag;
+};
+
+static const struct redirect_method_map redirect_methods[] = {
+ { "MESSAGE", AST_SIP_REDIRECT_METHOD_MESSAGE },
+};
+
+/*!
+ * \internal
+ * \brief Check if a SIP method is allowed to follow redirects
+ *
+ * \param endpoint The SIP endpoint
+ * \param method_name The SIP method name from the CSeq header
+ *
+ * \retval 0 if method is not allowed to follow redirects
+ * \retval 1 if method is allowed to follow redirects
+ */
+static int method_allowed_for_redirect(struct ast_sip_endpoint *endpoint, const pj_str_t *method_name)
+{
+ int i;
+
+ /* Look up the method in our mapping table */
+ for (i = 0; i < ARRAY_LEN(redirect_methods); i++) {
+ if (pj_stricmp2(method_name, redirect_methods[i].method_name) == 0) {
+ /* Method is recognized, check if it's allowed */
+ if (ast_test_flag(&endpoint->follow_redirect_methods, redirect_methods[i].flag)) {
+ return 1;
+ } else {
+ ast_log(LOG_NOTICE, "Received redirect for %s to endpoint '%s', "
+ "but %s is not in follow_redirect_methods. Not following redirect.\n",
+ redirect_methods[i].method_name,
+ ast_sorcery_object_get_id(endpoint), redirect_methods[i].method_name);
+ return 0;
+ }
+ }
+ }
+
+ /* Method not recognized/supported for redirects */
+ ast_log(LOG_NOTICE, "Received redirect for method %.*s to endpoint '%s', "
+ "but this method is not supported in follow_redirect_methods. Not following redirect.\n",
+ (int)method_name->slen, method_name->ptr, ast_sorcery_object_get_id(endpoint));
+ return 0;
+}
+
+int ast_sip_should_redirect(struct ast_sip_endpoint *endpoint, pjsip_rx_data *rdata)
+{
+ pjsip_msg *msg;
+ pjsip_cseq_hdr *cseq;
+ int status_code;
+
+ if (!rdata || !rdata->msg_info.msg || rdata->msg_info.msg->type != PJSIP_RESPONSE_MSG) {
+ return 0;
+ }
+
+ msg = rdata->msg_info.msg;
+ status_code = msg->line.status.code;
+
+ /* Check if it's a 3xx response */
+ if (!PJSIP_IS_STATUS_IN_CLASS(status_code, 300)) {
+ return 0;
+ }
+
+ /* Extract the method from the CSeq header */
+ cseq = rdata->msg_info.cseq;
+ if (!cseq) {
+ ast_log(LOG_WARNING, "Received %d redirect for endpoint '%s', but no CSeq header found\n",
+ status_code, ast_sorcery_object_get_id(endpoint));
+ return 0;
+ }
+
+ /* Check if this method is allowed to follow redirects */
+ return method_allowed_for_redirect(endpoint, &cseq->method.name);
+}
+
+/*!
+ * \internal
+ * \brief Check if a URI has already been visited (loop detection)
+ */
+static int is_uri_visited(const struct ast_sip_redirect_state *state, const char *uri)
+{
+ struct visited_uri *visited;
+
+ AST_LIST_TRAVERSE(&state->visited_uris, visited, list) {
+ if (!strcmp(visited->uri, uri)) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+/*!
+ * \internal
+ * \brief Add a URI to the visited list
+ */
+static int add_visited_uri(struct ast_sip_redirect_state *state, const char *uri)
+{
+ struct visited_uri *visited;
+
+ visited = ast_calloc(1, sizeof(*visited));
+ if (!visited) {
+ return -1;
+ }
+
+ ast_copy_string(visited->uri, uri, sizeof(visited->uri));
+ AST_LIST_INSERT_TAIL(&state->visited_uris, visited, list);
+
+ return 0;
+}
+
+/*!
+ * \internal
+ * \brief Extract q-value from a Contact header
+ *
+ * \param contact The Contact header
+ * \return The q-value (default 1.0 if not present or invalid)
+ */
+static float extract_q_value(const pjsip_contact_hdr *contact)
+{
+ pjsip_param *param;
+ static const pj_str_t Q_STR = { "q", 1 };
+
+ /* Search for q parameter in the contact header */
+ param = pjsip_param_find(&contact->other_param, &Q_STR);
+ if (!param) {
+ /* No q parameter, use default */
+ return 1.0f;
+ }
+
+ /* Parse the q value */
+ if (param->value.slen > 0) {
+ char q_buf[16];
+ float q_val;
+ int len = param->value.slen < sizeof(q_buf) - 1 ? param->value.slen : sizeof(q_buf) - 1;
+ memcpy(q_buf, param->value.ptr, len);
+ q_buf[len] = '\0';
+
+ q_val = ast_sip_parse_qvalue(q_buf);
+
+ return q_val < 0.0f ? 1.0f : q_val;
+ }
+
+ /* Invalid q value, use default */
+ return 1.0f;
+}
+
+/*!
+ * \internal
+ * \brief Insert a contact into the sorted list by q-value (highest first)
+ *
+ * \param list The list to insert into
+ * \param new_contact The contact to insert
+ */
+static void insert_contact_sorted(struct redirect_contact_list *list, struct redirect_contact *new_contact)
+{
+ struct redirect_contact *contact;
+
+ /* Find the insertion point - contacts with higher q values come first */
+ AST_LIST_TRAVERSE_SAFE_BEGIN(list, contact, list) {
+ if (new_contact->q_value > contact->q_value) {
+ /* Insert before this contact */
+ AST_LIST_INSERT_BEFORE_CURRENT(new_contact, list);
+ return;
+ }
+ }
+ AST_LIST_TRAVERSE_SAFE_END;
+
+ /* If we get here, insert at the end */
+ AST_LIST_INSERT_TAIL(list, new_contact, list);
+}
+
+/*!
+ * \internal
+ * \brief Parse all Contact headers from a 3xx response and create a sorted list
+ *
+ * \param rdata The redirect response data
+ * \param contacts List to populate with parsed contacts
+ * \return Number of valid contacts found
+ */
+static int parse_redirect_contacts(pjsip_rx_data *rdata, struct redirect_contact_list *contacts, const struct ast_sip_redirect_state *state)
+{
+ pjsip_contact_hdr *contact_hdr;
+ pjsip_uri *contact_uri;
+ int count = 0;
+ void *start = NULL;
+
+ /* Iterate through all Contact headers */
+ while ((contact_hdr = (pjsip_contact_hdr *) pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_CONTACT, start))) {
+ struct redirect_contact *redirect_contact;
+ int len;
+
+ start = contact_hdr->next;
+
+ /* Enforce maximum contact limit to prevent resource exhaustion */
+ if (count >= AST_SIP_MAX_REDIRECT_CONTACTS) {
+ ast_log(LOG_WARNING, "Redirect: maximum Contact header limit (%d) reached for endpoint '%s'. Ignoring additional contacts\n",
+ AST_SIP_MAX_REDIRECT_CONTACTS, ast_sorcery_object_get_id(state->endpoint));
+ break;
+ }
+
+ if (!contact_hdr->uri) {
+ continue;
+ }
+
+ contact_uri = (pjsip_uri *)pjsip_uri_get_uri(contact_hdr->uri);
+
+ /* Verify it's a SIP URI */
+ if (!PJSIP_URI_SCHEME_IS_SIP(contact_uri) && !PJSIP_URI_SCHEME_IS_SIPS(contact_uri)) {
+ ast_debug(1, "Skipping non-SIP/SIPS Contact URI in redirect for endpoint '%s'\n", ast_sorcery_object_get_id(state->endpoint));
+ continue;
+ }
+
+ /* Allocate a new redirect_contact structure */
+ redirect_contact = ast_calloc(1, sizeof(*redirect_contact));
+ if (!redirect_contact) {
+ ast_log(LOG_ERROR, "Failed to allocate memory for redirect contact for endpoint '%s'.\n", ast_sorcery_object_get_id(state->endpoint));
+ continue;
+ }
+
+ /* Print the URI */
+ len = pjsip_uri_print(PJSIP_URI_IN_REQ_URI, contact_uri,
+ redirect_contact->uri, sizeof(redirect_contact->uri) - 1);
+ if (len < 1) {
+ ast_debug(1, "Contact URI too long for redirect on endpoint '%s'. Skipping.\n", ast_sorcery_object_get_id(state->endpoint));
+ ast_free(redirect_contact);
+ continue;
+ }
+ redirect_contact->uri[len] = '\0';
+
+ /* Extract q-value */
+ redirect_contact->q_value = extract_q_value(contact_hdr);
+
+ ast_debug(1, "Found redirect Contact: %s (q=%f) for endpoint '%s'.\n", redirect_contact->uri, redirect_contact->q_value,
+ ast_sorcery_object_get_id(state->endpoint));
+
+ /* Insert into sorted list */
+ insert_contact_sorted(contacts, redirect_contact);
+ count++;
+ }
+
+ return count;
+}
+
+int ast_sip_redirect_parse_3xx(pjsip_rx_data *rdata, struct ast_sip_redirect_state *state)
+{
+ struct redirect_contact_list redirect_contacts;
+ struct redirect_contact *contact;
+ int contact_count;
+ int status_code = rdata->msg_info.msg->line.status.code;
+
+ ast_debug(1, "Received %d redirect response on endpoint '%s'.\n", status_code, ast_sorcery_object_get_id(state->endpoint));
+
+ /* Check if redirect should be followed based on endpoint configuration */
+ if (!ast_sip_should_redirect(state->endpoint, rdata)) {
+ return -1;
+ }
+
+ /* Check hop limit */
+ if (state->hop_count >= AST_SIP_MAX_REDIRECT_HOPS) {
+ ast_log(LOG_WARNING, "Redirect hop limit (%d) reached for endpoint '%s'. Not following redirect.\n",
+ AST_SIP_MAX_REDIRECT_HOPS, ast_sorcery_object_get_id(state->endpoint));
+ return -1;
+ }
+
+ /* Parse all Contact headers and sort by q-value */
+ AST_LIST_HEAD_INIT_NOLOCK(&redirect_contacts);
+ contact_count = parse_redirect_contacts(rdata, &redirect_contacts, state);
+
+ if (contact_count == 0) {
+ ast_log(LOG_WARNING, "Received %d redirect without valid Contact headers for endpoint '%s'. Cannot follow redirect.\n",
+ status_code, ast_sorcery_object_get_id(state->endpoint));
+ return -1;
+ }
+
+ /* Filter out contacts that would create loops */
+ AST_LIST_TRAVERSE_SAFE_BEGIN(&redirect_contacts, contact, list) {
+ if (is_uri_visited(state, contact->uri)) {
+ ast_log(LOG_WARNING, "Redirect: skipping Contact '%s' for endpoint '%s' (would create loop)\n", contact->uri,
+ ast_sorcery_object_get_id(state->endpoint));
+ AST_LIST_REMOVE_CURRENT(list);
+ ast_free(contact);
+ contact_count--;
+ }
+ }
+ AST_LIST_TRAVERSE_SAFE_END;
+
+ if (contact_count == 0) {
+ ast_log(LOG_WARNING, "Redirect: all Contact URIs would create loops for endpoint '%s'. Not following redirect.\n",
+ ast_sorcery_object_get_id(state->endpoint));
+ return -1;
+ }
+
+ /* Move all contacts to pending_contacts list */
+ while ((contact = AST_LIST_REMOVE_HEAD(&redirect_contacts, list))) {
+ AST_LIST_INSERT_TAIL(&state->pending_contacts, contact, list);
+ }
+
+ /* Increment hop count */
+ state->hop_count++;
+
+ return 0;
+}
+
+int ast_sip_redirect_get_next_uri(struct ast_sip_redirect_state *state, char **uri_out)
+{
+ struct redirect_contact *contact;
+
+ if (!uri_out) {
+ return -1;
+ }
+
+ /* Get the first contact from the pending list */
+ contact = AST_LIST_REMOVE_HEAD(&state->pending_contacts, list);
+ if (!contact) {
+ return -1;
+ }
+
+ /* Allocate and return the URI string */
+ *uri_out = ast_strdup(contact->uri);
+ if (!*uri_out) {
+ ast_free(contact);
+ return -1;
+ }
+
+ /* Add to visited list to prevent loops */
+ if (add_visited_uri(state, contact->uri)) {
+ ast_log(LOG_WARNING, "Failed to add URI to visited list for endpoint '%s'. Loop detection may be impaired.\n",
+ ast_sorcery_object_get_id(state->endpoint));
+ }
+
+ ast_free(contact);
+ return 0;
+}
+
+int ast_sip_redirect_check_loop(const struct ast_sip_redirect_state *state, const char *uri)
+{
+ return is_uri_visited(state, uri);
+}
+
+int ast_sip_redirect_get_hop_count(const struct ast_sip_redirect_state *state)
+{
+ return state->hop_count;
+}
+
+struct ast_sip_endpoint *ast_sip_redirect_get_endpoint(const struct ast_sip_redirect_state *state)
+{
+ return state->endpoint;
+}
+
+void ast_sip_redirect_state_destroy(struct ast_sip_redirect_state *state)
+{
+ struct visited_uri *visited;
+ struct redirect_contact *contact;
+
+ if (!state) {
+ return;
+ }
+
+ ao2_cleanup(state->endpoint);
+
+ while ((visited = AST_LIST_REMOVE_HEAD(&state->visited_uris, list))) {
+ ast_free(visited);
+ }
+
+ while ((contact = AST_LIST_REMOVE_HEAD(&state->pending_contacts, list))) {
+ ast_free(contact);
+ }
+
+ ast_free(state);
+}
diff --git a/res/res_pjsip/security_agreements.c b/res/res_pjsip/security_agreements.c
index 9f65c35bb7..7c37bd2899 100644
--- a/res/res_pjsip/security_agreements.c
+++ b/res/res_pjsip/security_agreements.c
@@ -213,32 +213,6 @@ void ast_sip_remove_headers_by_name_and_value(pjsip_msg *msg, const pj_str_t *hd
}
}
-/*!
- * \internal
- * \brief Parses a string representing a q_value to a float.
- *
- * Valid q values must be in the range from 0.0 to 1.0 inclusively.
- *
- * \param q_value
- * \retval The parsed qvalue or -1.0 on failure.
- */
-static float parse_qvalue(const char *q_value) {
- char *end;
- float ret = strtof(q_value, &end);
-
- if (end == q_value) {
- /* Not a number. */
- return -1.0;
- } else if ('\0' != *end) {
- /* Extra character at end of input. */
- return -1.0;
- } else if (ret > 1.0 || ret < 0.0) {
- /* Out of valid range. */
- return -1.0;
- }
- return ret;
-}
-
int ast_sip_str_to_security_mechanism(struct ast_sip_security_mechanism **security_mechanism, const char *value) {
struct ast_sip_security_mechanism *mech;
char *param;
@@ -267,7 +241,7 @@ int ast_sip_str_to_security_mechanism(struct ast_sip_security_mechanism **securi
goto out;
}
if (!strncmp(param, "q=", 2)) {
- mech->qvalue = parse_qvalue(¶m[2]);
+ mech->qvalue = ast_sip_parse_qvalue(¶m[2]);
if (mech->qvalue < 0.0) {
err = EINVAL;
goto out;