Commit d8caa664a8 for asterisk.org

commit d8caa664a8e1be4b9b85c7a0eac17ba498dbcc6a
Author: Maximilian Fridrich <m.fridrich@commend.com>
Date:   Fri Nov 7 12:27:11 2025 +0100

    res_pjsip_messaging: Add support for following 3xx redirects

    This commit integrates the redirect module into res_pjsip_messaging
    to enable following 3xx redirect responses for outgoing SIP MESSAGEs.

    When follow_redirect_methods contains 'message' on an endpoint, Asterisk
    will now follow 3xx redirect responses for MESSAGEs, similar to how
    it behaves for INVITE responses.

    Resolves: #1576

    UserNote: A new pjsip endpoint option follow_redirect_methods was added.
    This option is a comma-delimited, case-insensitive list of SIP methods
    for which SIP 3XX redirect responses are followed. An alembic upgrade
    script has been added for adding this new option to the Asterisk
    database.

diff --git a/res/res_pjsip_messaging.c b/res/res_pjsip_messaging.c
index a00e481d1a..f9ab8717c6 100644
--- a/res/res_pjsip_messaging.c
+++ b/res/res_pjsip_messaging.c
@@ -126,6 +126,7 @@
 #include "asterisk/module.h"
 #include "asterisk/pbx.h"
 #include "asterisk/res_pjsip.h"
+#include "asterisk/res_pjsip_redirect.h"
 #include "asterisk/res_pjsip_session.h"
 #include "asterisk/taskprocessor.h"
 #include "asterisk/test.h"
@@ -580,6 +581,69 @@ static struct msg_data *msg_data_create(const struct ast_msg *msg, const char *d
 	return mdata;
 }

+/*!
+ * \internal
+ * \brief Callback data for MESSAGE response handling
+ */
+struct message_response_data {
+	char *from;
+	char *to;
+	char *body;
+	char *content_type;
+	struct ast_sip_redirect_state *redirect_state;
+};
+
+static void message_response_data_destroy(void *obj)
+{
+	struct message_response_data *resp_data = obj;
+
+	ast_free(resp_data->from);
+	ast_free(resp_data->to);
+	ast_free(resp_data->body);
+	ast_free(resp_data->content_type);
+
+	if (resp_data->redirect_state) {
+		ast_sip_redirect_state_destroy(resp_data->redirect_state);
+	}
+}
+
+static struct message_response_data *message_response_data_create(
+	struct ast_sip_endpoint *endpoint,
+	const char *from,
+	const char *to,
+	const char *body,
+	const char *content_type,
+	const char *initial_uri)
+{
+	struct message_response_data *resp_data;
+
+	resp_data = ao2_alloc_options(sizeof(*resp_data), message_response_data_destroy, AO2_ALLOC_OPT_LOCK_NOLOCK);
+	if (!resp_data) {
+		return NULL;
+	}
+
+	resp_data->from = ast_strdup(from);
+	resp_data->to = ast_strdup(to);
+	resp_data->body = ast_strdup(body);
+	resp_data->content_type = ast_strdup(content_type);
+
+	if (!resp_data->from || !resp_data->to || !resp_data->body || !resp_data->content_type) {
+		ast_log(LOG_ERROR, "Failed to allocate memory for response data strings on endpoint '%s'.\n",
+			ast_sorcery_object_get_id(endpoint));
+		ao2_ref(resp_data, -1);
+		return NULL;
+	}
+
+	resp_data->redirect_state = ast_sip_redirect_state_create(endpoint, initial_uri);
+	if (!resp_data->redirect_state) {
+		ast_log(LOG_ERROR, "Failed to create redirect state for endpoint '%s'.\n", ast_sorcery_object_get_id(endpoint));
+		ao2_ref(resp_data, -1);
+		return NULL;
+	}
+
+	return resp_data;
+}
+
 static void update_content_type(pjsip_tx_data *tdata, struct ast_msg *msg, struct ast_sip_body *body)
 {
 	static const pj_str_t CONTENT_TYPE = { "Content-Type", sizeof("Content-Type") - 1 };
@@ -609,6 +673,212 @@ static void update_content_type(pjsip_tx_data *tdata, struct ast_msg *msg, struc
 	}
 }

+/* Forward declaration for callback */
+static void msg_response_callback(void *token, pjsip_event *e);
+
+/*!
+ * \internal
+ * \brief Send a MESSAGE to a redirect target
+ *
+ * \param resp_data Response data containing redirect state and message info
+ * \param target_uri The URI to send the message to
+ *
+ * \return 0: success, -1: failure
+ */
+static int send_message_to_uri(struct message_response_data *resp_data, const char *target_uri)
+{
+	pjsip_tx_data *tdata;
+	struct message_response_data *new_resp_data;
+	struct ast_sip_endpoint *endpoint;
+	struct ast_sip_body body = {
+		.type = "text",
+		.subtype = "plain",
+		.body_text = resp_data->body
+	};
+
+	if (!resp_data->redirect_state) {
+		ast_log(LOG_ERROR, "No redirect state available for sending a redirect message.\n");
+		return -1;
+	}
+
+	endpoint = ast_sip_redirect_get_endpoint(resp_data->redirect_state);
+
+	ast_debug(1, "Sending redirected MESSAGE to '%s' (hop %d) on endpoint '%s'\n",
+		target_uri, ast_sip_redirect_get_hop_count(resp_data->redirect_state), ast_sorcery_object_get_id(endpoint));
+
+	if (ast_sip_create_request("MESSAGE", NULL, endpoint, target_uri, NULL, &tdata)) {
+		ast_log(LOG_WARNING, "Could not create redirect request for endpoint '%s'.\n",
+			ast_sorcery_object_get_id(endpoint));
+		return -1;
+	}
+
+	/* Update To header if we have one */
+	if (!ast_strlen_zero(resp_data->to)) {
+		ast_sip_update_to_uri(tdata, resp_data->to);
+	}
+
+	/* Update From header if we have one */
+	if (!ast_strlen_zero(resp_data->from)) {
+		ast_sip_update_from(tdata, resp_data->from);
+	}
+
+	/* Parse and set content type if provided */
+	if (!ast_strlen_zero(resp_data->content_type)) {
+		char *type_copy = ast_strdupa(resp_data->content_type);
+		char *subtype = strchr(type_copy, '/');
+		if (subtype) {
+			*subtype = '\0';
+			subtype++;
+			body.type = type_copy;
+			body.subtype = subtype;
+		}
+	}
+	ast_sip_add_body(tdata, &body);
+	if (!tdata->msg->body) {
+		pjsip_tx_data_dec_ref(tdata);
+		ast_log(LOG_ERROR, "Could not add body to redirect request on endpoint '%s'.\n", ast_sorcery_object_get_id(endpoint));
+		return -1;
+	}
+
+	/* Create new callback data - the redirect state is passed along */
+	new_resp_data = ao2_alloc(sizeof(*new_resp_data), message_response_data_destroy);
+	if (!new_resp_data) {
+		pjsip_tx_data_dec_ref(tdata);
+		ast_log(LOG_ERROR, "Could not allocate redirect callback data for endpoint '%s'.\n", ast_sorcery_object_get_id(endpoint));
+		return -1;
+	}
+
+	/* Copy message-specific data */
+	new_resp_data->from = ast_strdup(resp_data->from);
+	new_resp_data->to = ast_strdup(resp_data->to);
+	new_resp_data->body = ast_strdup(resp_data->body);
+	new_resp_data->content_type = ast_strdup(resp_data->content_type);
+
+	/* Check for allocation failures */
+	if (!new_resp_data->from || !new_resp_data->to || !new_resp_data->body || !new_resp_data->content_type) {
+		pjsip_tx_data_dec_ref(tdata);
+		ao2_ref(new_resp_data, -1);
+		ast_log(LOG_ERROR, "Failed to allocate memory for redirect callback strings for endpoint '%s'.\n",
+			ast_sorcery_object_get_id(endpoint));
+		return -1;
+	}
+
+	/* Transfer the redirect state to the new response data */
+	new_resp_data->redirect_state = resp_data->redirect_state;
+	resp_data->redirect_state = NULL;
+
+	/* Send with callback for potential further redirects */
+	if (ast_sip_send_request(tdata, NULL, endpoint, new_resp_data, msg_response_callback)) {
+		ao2_ref(new_resp_data, -1);
+		ast_log(LOG_ERROR, "Failed to send redirect request to '%s' on endpoint '%s'.\n",
+			new_resp_data->to, ast_sorcery_object_get_id(endpoint));
+		return -1;
+	}
+
+	return 0;
+}
+
+/*!
+ * \internal
+ * \brief Handle a 3xx redirect response to a MESSAGE
+ *
+ * \param resp_data Response callback data
+ * \param rdata The redirect response data
+ */
+static void handle_message_redirect(struct message_response_data *resp_data, pjsip_rx_data *rdata)
+{
+	char *uri = NULL;
+	int hop_count;
+
+	if (!resp_data->redirect_state) {
+		ast_log(LOG_ERROR, "MESSAGE redirect: no redirect state available\n");
+		return;
+	}
+
+	/* Parse the redirect response and extract contacts */
+	if (ast_sip_redirect_parse_3xx(rdata, resp_data->redirect_state)) {
+		ast_debug(1, "MESSAGE redirect on endpoint '%s': not following redirect (parse failed or conditions not met)\n",
+			ast_sorcery_object_get_id(ast_sip_redirect_get_endpoint(resp_data->redirect_state)));
+		return;
+	}
+
+	/* Get the first URI to try */
+	if (ast_sip_redirect_get_next_uri(resp_data->redirect_state, &uri)) {
+		ast_log(LOG_WARNING, "MESSAGE redirect on endpoint '%s': no valid URIs to try\n",
+			ast_sorcery_object_get_id(ast_sip_redirect_get_endpoint(resp_data->redirect_state)));
+		return;
+	}
+
+	hop_count = ast_sip_redirect_get_hop_count(resp_data->redirect_state);
+	ast_log(LOG_NOTICE, "MESSAGE redirect on endpoint '%s': Following redirect to '%s' (hop %d/%d)\n",
+		ast_sorcery_object_get_id(ast_sip_redirect_get_endpoint(resp_data->redirect_state)), uri, hop_count, AST_SIP_MAX_REDIRECT_HOPS);
+
+	/* Try the first contact */
+	send_message_to_uri(resp_data, uri);
+	ast_free(uri);
+}
+
+/*!
+ * \internal
+ * \brief Callback for MESSAGE responses
+ *
+ * \param token Callback data
+ * \param e The pjsip event
+ */
+static void msg_response_callback(void *token, pjsip_event *e)
+{
+	struct message_response_data *resp_data = token;
+	struct ast_sip_redirect_state *state = resp_data->redirect_state;
+	struct ast_sip_endpoint *endpoint = ast_sip_redirect_get_endpoint(state);
+	pjsip_rx_data *rdata;
+	int status_code;
+	char *next_uri = NULL;
+
+	/* Check event type */
+	if (e->body.tsx_state.type == PJSIP_EVENT_TIMER) {
+		ast_debug(1, "MESSAGE request on endpoint '%s' timed out\n", ast_sorcery_object_get_id(endpoint));
+		/* Try next pending contact if available */
+		if (state && !ast_sip_redirect_get_next_uri(state, &next_uri)) {
+			ast_log(LOG_NOTICE, "MESSAGE timed out on endpoint '%s', trying next Contact: '%s'\n",
+				ast_sorcery_object_get_id(endpoint), next_uri);
+			send_message_to_uri(resp_data, next_uri);
+			ast_free(next_uri);
+		}
+		ao2_ref(resp_data, -1);
+		return;
+	}
+
+	if (e->body.tsx_state.type != PJSIP_EVENT_RX_MSG) {
+		ast_debug(3, "MESSAGE response event type %d (not RX_MSG) on endpoint '%s'.\n",
+			e->body.tsx_state.type, ast_sorcery_object_get_id(endpoint));
+		ao2_ref(resp_data, -1);
+		return;
+	}
+
+	rdata = e->body.tsx_state.src.rdata;
+	status_code = e->body.tsx_state.tsx->status_code;
+
+	ast_debug(3, "Received MESSAGE response %d on endpoint '%s'.\n", status_code, ast_sorcery_object_get_id(endpoint));
+
+	/* Handle 3xx redirects */
+	if (PJSIP_IS_STATUS_IN_CLASS(status_code, 300)) {
+		handle_message_redirect(resp_data, rdata);
+	}
+	/* If non-2xx response and we have pending contacts, try the next one */
+	else if (status_code >= 400 && state && !ast_sip_redirect_get_next_uri(state, &next_uri)) {
+		ast_log(LOG_NOTICE, "MESSAGE to redirect target failed (%d) on endpoint '%s', trying next Contact: '%s'\n",
+			status_code, ast_sorcery_object_get_id(endpoint), next_uri);
+		send_message_to_uri(resp_data, next_uri);
+		ast_free(next_uri);
+	}
+	/* Success (2xx) - don't try other contacts */
+	else if (PJSIP_IS_STATUS_IN_CLASS(status_code, 200)) {
+		ast_debug(1, "MESSAGE successfully delivered (%d) for endpoint '%s'.\n", status_code, ast_sorcery_object_get_id(endpoint));
+	}
+
+	ao2_ref(resp_data, -1);
+}
+
 /*!
  * \internal
  * \brief Send a MESSAGE
@@ -631,6 +901,10 @@ static void update_content_type(pjsip_tx_data *tdata, struct ast_msg *msg, struc
 static int msg_send(void *data)
 {
 	struct msg_data *mdata = data; /* The caller holds a reference */
+	/* Callback data for redirect handling */
+	struct message_response_data *resp_data;
+	const char *from_uri;
+	const char *to_uri;

 	struct ast_sip_body body = {
 		.type = "text",
@@ -641,6 +915,7 @@ static int msg_send(void *data)
 	pjsip_tx_data *tdata;
 	RAII_VAR(char *, uri, NULL, ast_free);
 	RAII_VAR(struct ast_sip_endpoint *, endpoint, NULL, ao2_cleanup);
+	RAII_VAR(struct ast_str *, content_type_buf , ast_str_create(128), ast_free);

 	ast_debug(3, "mdata From: %s msg From: %s mdata Destination: %s msg To: %s\n",
 		mdata->from, ast_msg_get_from(mdata->msg), mdata->destination, ast_msg_get_to(mdata->msg));
@@ -736,7 +1011,8 @@ static int msg_send(void *data)

 	update_content_type(tdata, mdata->msg, &body);

-	if (ast_sip_add_body(tdata, &body)) {
+	ast_sip_add_body(tdata, &body);
+	if (!tdata->msg->body) {
 		pjsip_tx_data_dec_ref(tdata);
 		ast_log(LOG_ERROR, "PJSIP MESSAGE - Could not add body to request\n");
 		return -1;
@@ -751,8 +1027,40 @@ static int msg_send(void *data)
 	ast_debug(1, "Sending message to '%s' (via endpoint %s) from '%s'\n",
 		uri, ast_sorcery_object_get_id(endpoint), mdata->from);

-	if (ast_sip_send_request(tdata, NULL, endpoint, NULL, NULL)) {
-		ast_log(LOG_ERROR, "PJSIP MESSAGE - Could not send request\n");
+	/* Determine From URI */
+	if (!ast_strlen_zero(mdata->from)) {
+		from_uri = mdata->from;
+	} else if (!ast_strlen_zero(ast_msg_get_from(mdata->msg))) {
+		from_uri = ast_msg_get_from(mdata->msg);
+	} else {
+		from_uri = "";
+	}
+
+	/* Determine To URI */
+	if (!ast_strlen_zero(ast_msg_get_to(mdata->msg))) {
+		to_uri = ast_msg_get_to(mdata->msg);
+	} else {
+		to_uri = "";
+	}
+
+	/* Build content type string */
+	ast_str_set(&content_type_buf, 0, "%s/%s", body.type, body.subtype);
+
+	/* Create callback data */
+	resp_data = message_response_data_create(endpoint, from_uri, to_uri,
+		body.body_text, ast_str_buffer(content_type_buf), uri);
+
+	if (!resp_data) {
+		pjsip_tx_data_dec_ref(tdata);
+		ast_log(LOG_ERROR, "PJSIP MESSAGE - Could not allocate callback data for endpoint '%s'\n",
+			ast_sorcery_object_get_id(endpoint));
+		return -1;
+	}
+
+	/* Send with callback for redirect handling */
+	if (ast_sip_send_request(tdata, NULL, endpoint, resp_data, msg_response_callback)) {
+		ao2_ref(resp_data, -1);
+		ast_log(LOG_ERROR, "PJSIP MESSAGE - Could not send request on endpoint '%s'\n", ast_sorcery_object_get_id(endpoint));
 		return -1;
 	}