From: Maximilian Fridrich Date: Fri, 7 Nov 2025 11:26:45 +0000 (+0100) Subject: res_pjsip: Introduce redirect module for handling 3xx responses X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=569b3ed99503c91fbcd90a3fcc1481e58755b07c;p=thirdparty%2Fasterisk.git 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 + * + * 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 + +/*! + * \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 efabc8ac45..342daba820 100644 --- a/res/res_pjsip/pjsip_config.xml +++ b/res/res_pjsip/pjsip_config.xml @@ -691,6 +691,21 @@ + + + 20.18.0 + 22.8.0 + 23.2.0 + + Follow 3XX redirect responses for the defined SIP methods. + + 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 MESSAGE. Currently, only + MESSAGE is supported. + + + 12.0.0 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 + * + * 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 + +#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;