[Asterisk-code-review] res_pjsip_aoc: New module for sending advice-of-charge with chan_pjsip (asterisk[master])

George Joseph asteriskteam at digium.com
Fri Dec 9 08:26:19 CST 2022


George Joseph has submitted this change. ( https://gerrit.asterisk.org/c/asterisk/+/19460 )

Change subject: res_pjsip_aoc: New module for sending advice-of-charge with chan_pjsip
......................................................................

res_pjsip_aoc: New module for sending advice-of-charge with chan_pjsip

chan_sip supported sending AOC-D and AOC-E information in SIP INFO
messages in an "AOC" header in a format that was originally defined by
Snom. In the meantime, ETSI TS 124 647 introduced an XML-based AOC
format that is supported by devices from multiple vendors, including
Snom phones with firmware >= 8.4.2 (released in 2010).

This commit adds a new res_pjsip_aoc module that inserts AOC information
into outgoing messages or sends SIP INFO messages as described below.
It also fixes a small issue in res_pjsip_session which didn't always
call session supplements on outgoing_response.

* AOC-S in the 180/183/200 responses to an INVITE request
* AOC-S in SIP INFO (if a 200 response has already been sent or if the
  INVITE was sent by Asterisk)
* AOC-D in SIP INFO
* AOC-D in the 200 response to a BYE request (if the client hangs up)
* AOC-D in a BYE request (if Asterisk hangs up)
* AOC-E in the 200 response to a BYE request (if the client hangs up)
* AOC-E in a BYE request (if Asterisk hangs up)

The specification defines one more, AOC-S in an INVITE request, which
is not implemented here because it is not currently possible in
Asterisk to have AOC data ready at this point in call setup. Once
specifying AOC-S via the dialplan or passing it through from another
SIP channel's INVITE is possible, that might be added.

The SIP INFO requests are sent out immediately when the AOC indication
is received. The others are inserted into an appropriate outgoing
message whenever that is ready to be sent. In the latter case, the XML
is stored in a channel variable at the time the AOC indication is
received. Depending on where the AOC indications are coming from (e.g.
PRI or AMI), it may not always be possible to guarantee that the AOC-E
is available in time for the BYE.

Successfully tested AOC-D and both variants of AOC-E with a Snom D735
running firmware 10.1.127.10. It does not appear to properly support
AOC-S however, so that could only be tested by inspecting SIP traces.

ASTERISK-21502 #close
Reported-by: Matt Jordan <mjordan at digium.com>

Change-Id: Iebb7ad0d5f88526bc6629d3a1f9f11665434d333
---
M configs/samples/pjsip.conf.sample
A contrib/ast-db-manage/config/versions/5a2247c957d2_add_aoc_option.py
A doc/CHANGES-staging/res_pjsip_aoc.txt
M include/asterisk/res_pjsip.h
M res/res_pjsip/pjsip_config.xml
M res/res_pjsip/pjsip_configuration.c
A res/res_pjsip_aoc.c
M res/res_pjsip_session.c
8 files changed, 822 insertions(+), 1 deletion(-)

Approvals:
  Joshua Colp: Looks good to me, but someone else must approve
  George Joseph: Looks good to me, approved; Approved for Submit




diff --git a/configs/samples/pjsip.conf.sample b/configs/samples/pjsip.conf.sample
index b6a9cd0..0a93cbe 100644
--- a/configs/samples/pjsip.conf.sample
+++ b/configs/samples/pjsip.conf.sample
@@ -964,6 +964,11 @@
                 ; by the channel driver from the dialplan before they're forwarded
                 ; the remote endpoint.
 ;
+; send_aoc =
+                ; This options turns on and off support for sending AOC to endpoints.
+                ; AOC updates can be sent using the AOCMessage AMI action or come
+                ; from PRI channels.
+                ; (default: no)
 
 
 ;==========================AUTH SECTION OPTIONS=========================
diff --git a/contrib/ast-db-manage/config/versions/5a2247c957d2_add_aoc_option.py b/contrib/ast-db-manage/config/versions/5a2247c957d2_add_aoc_option.py
new file mode 100644
index 0000000..a603e7f
--- /dev/null
+++ b/contrib/ast-db-manage/config/versions/5a2247c957d2_add_aoc_option.py
@@ -0,0 +1,38 @@
+"""add aoc option
+
+Revision ID: 5a2247c957d2
+Revises: ccf795ee535f
+Create Date: 2022-10-30 12:47:56.173511
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '5a2247c957d2'
+down_revision = 'ccf795ee535f'
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects.postgresql import ENUM
+
+AST_BOOL_NAME = 'ast_bool_values'
+# We'll just ignore the n/y and f/t abbreviations as Asterisk does not write
+# those aliases.
+AST_BOOL_VALUES = [ '0', '1',
+                    'off', 'on',
+                    'false', 'true',
+                    'no', 'yes' ]
+
+def upgrade():
+    ############################# Enums ##############################
+
+    # ast_bool_values has already been created, so use postgres enum object
+    # type to get around "already created" issue - works okay with mysql
+    ast_bool_values = ENUM(*AST_BOOL_VALUES, name=AST_BOOL_NAME, create_type=False)
+
+    op.add_column('ps_endpoints', sa.Column('send_aoc', ast_bool_values))
+
+def downgrade():
+    if op.get_context().bind.dialect.name == 'mssql':
+        op.drop_constraint('ck_ps_endpoints_send_aoc_ast_bool_values', 'ps_endpoints')
+    op.drop_column('ps_endpoints', 'send_aoc')
+    pass
diff --git a/doc/CHANGES-staging/res_pjsip_aoc.txt b/doc/CHANGES-staging/res_pjsip_aoc.txt
new file mode 100644
index 0000000..496bd0b
--- /dev/null
+++ b/doc/CHANGES-staging/res_pjsip_aoc.txt
@@ -0,0 +1,4 @@
+Subject: res_pjsip_aoc
+
+Added res_pjsip_aoc which gives chan_pjsip the ability to send Advice-of-Charge messages.
+A new endpoint option, send_aoc, controls this.
diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h
index f31cd10..35c7339 100644
--- a/include/asterisk/res_pjsip.h
+++ b/include/asterisk/res_pjsip.h
@@ -1067,6 +1067,8 @@
 	AST_STRING_FIELD_EXTENDED(geoloc_outgoing_call_profile);
 	/*! 100rel mode to use with this endpoint */
 	enum ast_sip_100rel_mode rel100;
+	/*! Send Advice-of-Charge messages */
+	unsigned int send_aoc;
 };
 
 /*! URI parameter for symmetric transport */
diff --git a/res/res_pjsip/pjsip_config.xml b/res/res_pjsip/pjsip_config.xml
index ba8ba91..99ca645 100644
--- a/res/res_pjsip/pjsip_config.xml
+++ b/res/res_pjsip/pjsip_config.xml
@@ -1506,6 +1506,9 @@
 						</para>
 					</description>
 				</configOption>
+				<configOption name="send_aoc" default="no">
+					<synopsis>Send Advice-of-Charge messages</synopsis>
+				</configOption>
 			</configObject>
 			<configObject name="auth">
 				<synopsis>Authentication type</synopsis>
diff --git a/res/res_pjsip/pjsip_configuration.c b/res/res_pjsip/pjsip_configuration.c
index e63018e..bb015d0 100644
--- a/res/res_pjsip/pjsip_configuration.c
+++ b/res/res_pjsip/pjsip_configuration.c
@@ -2263,6 +2263,7 @@
 	ast_sorcery_object_field_register(sip_sorcery, "endpoint", "geoloc_outgoing_call_profile", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, geoloc_outgoing_call_profile));
 	ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "security_mechanisms", "", security_mechanism_handler, NULL, NULL, 0, 0);
 	ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "security_negotiation", "no", security_negotiation_handler, NULL, NULL, 0, 0);
+	ast_sorcery_object_field_register(sip_sorcery, "endpoint", "send_aoc", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, send_aoc));
 
 	if (ast_sip_initialize_sorcery_transport()) {
 		ast_log(LOG_ERROR, "Failed to register SIP transport support with sorcery\n");
diff --git a/res/res_pjsip_aoc.c b/res/res_pjsip_aoc.c
new file mode 100644
index 0000000..dba4e26
--- /dev/null
+++ b/res/res_pjsip_aoc.c
@@ -0,0 +1,698 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2022, Michael Kuron
+ *
+ * Michael Kuron <m.kuron at gmx.de>
+ *
+ * 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.
+ */
+
+/*** MODULEINFO
+	<depend>pjproject</depend>
+	<depend>res_pjsip</depend>
+	<support_level>extended</support_level>
+ ***/
+
+#include "asterisk.h"
+
+#include <pjsip.h>
+#include <pjlib.h>
+
+#include "asterisk/aoc.h"
+#include "asterisk/module.h"
+#include "asterisk/pbx.h"
+#include "asterisk/res_pjsip.h"
+#include "asterisk/res_pjsip_session.h"
+
+static pj_xml_attr *aoc_xml_create_attr(pj_pool_t *pool, pj_xml_node *node,
+	const char *name, const char *value)
+{
+	pj_xml_attr *attr;
+
+	attr = PJ_POOL_ALLOC_T(pool, pj_xml_attr);
+
+	pj_strdup2(pool, &attr->name, name);
+	pj_strdup2(pool, &attr->value, value);
+
+	pj_xml_add_attr(node, attr);
+	return attr;
+}
+
+static pj_xml_node *aoc_xml_create_node(pj_pool_t *pool, pj_xml_node *parent,
+	const char *name)
+{
+	pj_xml_node *node;
+
+	node = PJ_POOL_ZALLOC_T(pool, pj_xml_node);
+
+	pj_list_init(&node->attr_head);
+	pj_list_init(&node->node_head);
+
+	pj_strdup2(pool, &node->name, name);
+
+	if (parent) {
+		pj_xml_add_node(parent, node);
+	}
+
+	return node;
+}
+
+static void aoc_xml_set_node_content(pj_pool_t *pool, pj_xml_node *node,
+	const char *content)
+{
+	pj_strdup2(pool, &node->content, content);
+}
+
+static char * aoc_format_amount(pj_pool_t *pool, unsigned int amount,
+		enum ast_aoc_currency_multiplier multiplier)
+{
+	const size_t amount_max_size = 16;
+	char *amount_str;
+
+	amount_str = pj_pool_alloc(pool, amount_max_size);
+
+	switch (multiplier) {
+	case AST_AOC_MULT_ONETHOUSANDTH:
+		pj_ansi_snprintf(amount_str, amount_max_size, "%.3f", amount*0.001f);
+		break;
+	case AST_AOC_MULT_ONEHUNDREDTH:
+		pj_ansi_snprintf(amount_str, amount_max_size, "%.2f", amount*0.01f);
+		break;
+	case AST_AOC_MULT_ONETENTH:
+		pj_ansi_snprintf(amount_str, amount_max_size, "%.1f", amount*0.1f);
+		break;
+	case AST_AOC_MULT_ONE:
+		pj_ansi_snprintf(amount_str, amount_max_size, "%d", amount);
+		break;
+	case AST_AOC_MULT_TEN:
+		pj_ansi_snprintf(amount_str, amount_max_size, "%d", amount*10);
+		break;
+	case AST_AOC_MULT_HUNDRED:
+		pj_ansi_snprintf(amount_str, amount_max_size, "%d", amount*100);
+		break;
+	case AST_AOC_MULT_THOUSAND:
+		pj_ansi_snprintf(amount_str, amount_max_size, "%d", amount*1000);
+		break;
+	default:
+		pj_ansi_snprintf(amount_str, amount_max_size, "%d", amount);
+	}
+
+	return amount_str;
+}
+
+static const char *aoc_time_scale_str(enum ast_aoc_time_scale value)
+{
+	const char *str;
+
+	switch (value) {
+	default:
+	case AST_AOC_TIME_SCALE_HUNDREDTH_SECOND:
+		str = "one-hundredth-second";
+		break;
+	case AST_AOC_TIME_SCALE_TENTH_SECOND:
+		str = "one-tenth-second";
+		break;
+	case AST_AOC_TIME_SCALE_SECOND:
+		str = "one-second";
+		break;
+	case AST_AOC_TIME_SCALE_TEN_SECOND:
+		str = "ten-seconds";
+		break;
+	case AST_AOC_TIME_SCALE_MINUTE:
+		str = "one-minute";
+		break;
+	case AST_AOC_TIME_SCALE_HOUR:
+		str = "one-hour";
+		break;
+	case AST_AOC_TIME_SCALE_DAY:
+		str = "twenty-four-hours";
+		break;
+	}
+	return str;
+}
+
+static void aoc_datastore_destroy(void *obj)
+{
+	char *xml = obj;
+	ast_free(xml);
+}
+
+static const struct ast_datastore_info aoc_s_datastore = {
+	.type = "AOC-S",
+	.destroy = aoc_datastore_destroy,
+};
+
+static const struct ast_datastore_info aoc_d_datastore = {
+	.type = "AOC-D",
+	.destroy = aoc_datastore_destroy,
+};
+
+static const struct ast_datastore_info aoc_e_datastore = {
+	.type = "AOC-E",
+	.destroy = aoc_datastore_destroy,
+};
+
+struct aoc_data {
+	struct ast_sip_session *session;
+	struct ast_aoc_decoded *decoded;
+	enum ast_channel_state channel_state;
+};
+
+static void aoc_release_pool(void * data)
+{
+	pj_pool_t *pool = data;
+	pjsip_endpt_release_pool(ast_sip_get_pjsip_endpoint(), pool);
+}
+
+static int aoc_send_as_xml(void * data)
+{
+	RAII_VAR(struct aoc_data *, adata, data, ao2_cleanup);
+	RAII_VAR(pj_pool_t *, pool, NULL, aoc_release_pool);
+
+	pool = pjsip_endpt_create_pool(ast_sip_get_pjsip_endpoint(), "AOC", 2048, 512);
+
+	if (!pool) {
+		ast_log(LOG_ERROR, "Could not create a memory pool for AOC XML\n");
+		return 1;
+	}
+
+	if (ast_aoc_get_msg_type(adata->decoded) == AST_AOC_D ||
+			ast_aoc_get_msg_type(adata->decoded) == AST_AOC_E) {
+		pj_xml_node *aoc;
+		pj_xml_node *aoc_type;
+		pj_xml_node *charging_info = NULL;
+		pj_xml_node *charges;
+		pj_xml_node *charge;
+		char *xml;
+		size_t size;
+		const size_t xml_max_size = 512;
+
+		aoc = aoc_xml_create_node(pool, NULL, "aoc");
+		aoc_xml_create_attr(pool, aoc, "xmlns",
+				"http://uri.etsi.org/ngn/params/xml/simservs/aoc");
+		aoc_type = aoc_xml_create_node(pool, aoc,
+				ast_aoc_get_msg_type(adata->decoded) == AST_AOC_D ? "aoc-d" : "aoc-e");
+		if (ast_aoc_get_msg_type(adata->decoded) == AST_AOC_D) {
+			charging_info = aoc_xml_create_node(pool, aoc_type, "charging-info");
+			aoc_xml_set_node_content(pool, charging_info,
+					ast_aoc_get_total_type(adata->decoded) == AST_AOC_SUBTOTAL ? "subtotal" : "total");
+		}
+		charges = aoc_xml_create_node(pool, aoc_type, "recorded-charges");
+
+		if (ast_aoc_get_charge_type(adata->decoded) == AST_AOC_CHARGE_FREE) {
+			charge = aoc_xml_create_node(pool, charges, "free-charge");
+		} else if (ast_aoc_get_charge_type(adata->decoded) == AST_AOC_CHARGE_CURRENCY ||
+				ast_aoc_get_charge_type(adata->decoded) == AST_AOC_CHARGE_UNIT) {
+			charge = aoc_xml_create_node(pool, charges, "recorded-currency-units");
+		} else {
+			charge = aoc_xml_create_node(pool, charges, "not-available");
+		}
+
+		if (ast_aoc_get_charge_type(adata->decoded) == AST_AOC_CHARGE_CURRENCY) {
+			const char *currency;
+			pj_xml_node *amount;
+			char *amount_str;
+
+			currency = ast_aoc_get_currency_name(adata->decoded);
+			if (!ast_strlen_zero(currency)) {
+				pj_xml_node *currency_id;
+
+				currency_id = aoc_xml_create_node(pool, charge, "currency-id");
+				aoc_xml_set_node_content(pool, currency_id, currency);
+			}
+
+			amount = aoc_xml_create_node(pool, charge, "currency-amount");
+			amount_str = aoc_format_amount(pool, ast_aoc_get_currency_amount(adata->decoded),
+					ast_aoc_get_currency_multiplier(adata->decoded));
+			aoc_xml_set_node_content(pool, amount, amount_str);
+		} else if (ast_aoc_get_charge_type(adata->decoded) == AST_AOC_CHARGE_UNIT) {
+			pj_xml_node *currency_id;
+			const struct ast_aoc_unit_entry *unit_entry;
+
+			currency_id = aoc_xml_create_node(pool, charge, "currency-id");
+			aoc_xml_set_node_content(pool, currency_id, "UNIT");
+
+			unit_entry = ast_aoc_get_unit_info(adata->decoded, 0);
+			if (unit_entry) {
+				pj_xml_node *amount;
+				char *amount_str;
+
+				amount = aoc_xml_create_node(pool, charge, "currency-amount");
+				amount_str = aoc_format_amount(pool, unit_entry->amount,
+						AST_AOC_MULT_ONE);
+				aoc_xml_set_node_content(pool, amount, amount_str);
+			}
+		}
+
+		xml = pj_pool_alloc(pool, xml_max_size);
+		size = pj_xml_print(aoc, xml, xml_max_size - 1, PJ_TRUE);
+		if (size >= xml_max_size) {
+			ast_log(LOG_ERROR, "aoc+xml body text too large\n");
+			return 1;
+		}
+		xml[size] = 0;
+
+		if (ast_aoc_get_msg_type(adata->decoded) == AST_AOC_D) {
+			RAII_VAR(struct ast_datastore *, datastore,
+					ast_sip_session_get_datastore(adata->session, aoc_d_datastore.type),
+					ao2_cleanup);
+			struct pjsip_tx_data *tdata;
+			struct ast_sip_body body = {
+				.type = "application",
+				.subtype = "vnd.etsi.aoc+xml",
+				.body_text = xml
+			};
+
+			if (ast_sip_create_request("INFO", adata->session->inv_session->dlg,
+					adata->session->endpoint, NULL, NULL, &tdata)) {
+				ast_log(LOG_ERROR, "Could not create AOC INFO request\n");
+				return 1;
+			}
+			if (ast_sip_add_body(tdata, &body)) {
+				ast_log(LOG_ERROR, "Could not add body to AOC INFO request\n");
+				pjsip_tx_data_dec_ref(tdata);
+				return 1;
+			}
+			ast_sip_session_send_request(adata->session, tdata);
+
+			if (!datastore) {
+				datastore = ast_sip_session_alloc_datastore(&aoc_d_datastore, aoc_d_datastore.type);
+				if (!datastore) {
+					ast_log(LOG_ERROR, "Unable to create datastore for AOC-D.\n");
+					return 1;
+				}
+				datastore->data = NULL;
+				if (ast_sip_session_add_datastore(adata->session, datastore)) {
+					ast_log(LOG_ERROR, "Unable to create datastore for AOC-D.\n");
+					return 1;
+				}
+			} else {
+				ast_free(datastore->data);
+			}
+
+			aoc_xml_set_node_content(pool, charging_info, "total");
+			size = pj_xml_print(aoc, xml, xml_max_size - 1, PJ_TRUE);
+			xml[size] = 0;
+			datastore->data = ast_strdup(xml);
+		} else if (ast_aoc_get_msg_type(adata->decoded) == AST_AOC_E) {
+			RAII_VAR(struct ast_datastore *, datastore,
+					ast_sip_session_get_datastore(adata->session, aoc_e_datastore.type),
+					ao2_cleanup);
+			if (!datastore) {
+				datastore = ast_sip_session_alloc_datastore(&aoc_e_datastore, aoc_e_datastore.type);
+				if (!datastore) {
+					ast_log(LOG_ERROR, "Unable to create datastore for AOC-E.\n");
+					return 1;
+				}
+				datastore->data = NULL;
+				if (ast_sip_session_add_datastore(adata->session, datastore)) {
+					ast_log(LOG_ERROR, "Unable to create datastore for AOC-E.\n");
+					return 1;
+				}
+			} else {
+				ast_free(datastore->data);
+			}
+			datastore->data = ast_strdup(xml);
+		}
+	} else if (ast_aoc_get_msg_type(adata->decoded) == AST_AOC_S) {
+		pj_xml_node *aoc;
+		pj_xml_node *aoc_type;
+		pj_xml_node *charged_items;
+		const struct ast_aoc_s_entry *entry;
+		int idx;
+		char *xml;
+		size_t size;
+		const size_t xml_max_size = 1024;
+
+		aoc = aoc_xml_create_node(pool, NULL, "aoc");
+		aoc_xml_create_attr(pool, aoc, "xmlns",
+				"http://uri.etsi.org/ngn/params/xml/simservs/aoc");
+		aoc_type = aoc_xml_create_node(pool, aoc, "aoc-s");
+		charged_items = aoc_xml_create_node(pool, aoc_type, "charged-items");
+
+		for (idx = 0; idx < ast_aoc_s_get_count(adata->decoded); idx++) {
+			pj_xml_node *charged_item;
+			pj_xml_node *charge;
+
+			if (!(entry = ast_aoc_s_get_rate_info(adata->decoded, idx))) {
+				break;
+			}
+
+			if (entry->charged_item == AST_AOC_CHARGED_ITEM_BASIC_COMMUNICATION) {
+				charged_item = aoc_xml_create_node(pool, charged_items, "basic");
+			} else if (entry->charged_item == AST_AOC_CHARGED_ITEM_CALL_ATTEMPT) {
+				charged_item = aoc_xml_create_node(pool, charged_items,
+						"communication-attempt");
+			} else if (entry->charged_item == AST_AOC_CHARGED_ITEM_CALL_SETUP) {
+				charged_item = aoc_xml_create_node(pool, charged_items,
+						"communication-setup");
+			} else {
+				continue;
+			}
+
+			if (entry->rate_type == AST_AOC_RATE_TYPE_FREE) {
+				charge = aoc_xml_create_node(pool, charged_item, "free-charge");
+			} else if (entry->rate_type == AST_AOC_RATE_TYPE_FLAT) {
+				charge = aoc_xml_create_node(pool, charged_item, "flat-rate");
+			} else if (entry->rate_type == AST_AOC_RATE_TYPE_DURATION &&
+					entry->charged_item == AST_AOC_CHARGED_ITEM_BASIC_COMMUNICATION) {
+				charge = aoc_xml_create_node(pool, charged_item, "price-time");
+			} else {
+				continue;
+			}
+
+			if (entry->rate_type == AST_AOC_RATE_TYPE_DURATION ||
+					entry->rate_type == AST_AOC_RATE_TYPE_FLAT) {
+				const char *currency;
+				pj_xml_node *amount;
+				uint32_t amount_val;
+				enum ast_aoc_currency_multiplier multiplier_val;
+				char *amount_str;
+
+				currency = (entry->rate_type == AST_AOC_RATE_TYPE_DURATION ?
+						entry->rate.duration.currency_name :
+						entry->rate.flat.currency_name);
+				if (!ast_strlen_zero(currency)) {
+					pj_xml_node *currency_id;
+
+					currency_id = aoc_xml_create_node(pool, charge, "currency-id");
+					aoc_xml_set_node_content(pool, currency_id, currency);
+				}
+
+				amount = aoc_xml_create_node(pool, charge, "currency-amount");
+				amount_val = (entry->rate_type == AST_AOC_RATE_TYPE_DURATION ?
+						entry->rate.duration.amount : entry->rate.flat.amount);
+				multiplier_val = (entry->rate_type == AST_AOC_RATE_TYPE_DURATION ?
+						entry->rate.duration.multiplier : entry->rate.flat.multiplier);
+				amount_str = aoc_format_amount(pool, amount_val, multiplier_val);
+				aoc_xml_set_node_content(pool, amount, amount_str);
+			}
+
+			if (entry->rate_type == AST_AOC_RATE_TYPE_DURATION) {
+				pj_xml_node *length_time_unit;
+				pj_xml_node *time_unit;
+				char *time_str;
+				pj_xml_node *scale;
+				pj_xml_node *charging_type;
+
+				length_time_unit = aoc_xml_create_node(pool, charge, "length-time-unit");
+				time_unit = aoc_xml_create_node(pool, length_time_unit, "time-unit");
+				time_str = aoc_format_amount(pool, entry->rate.duration.time,
+						AST_AOC_MULT_ONE);
+				aoc_xml_set_node_content(pool, time_unit, time_str);
+				scale = aoc_xml_create_node(pool, length_time_unit, "scale");
+				aoc_xml_set_node_content(pool, scale,
+						aoc_time_scale_str(entry->rate.duration.time_scale));
+				charging_type = aoc_xml_create_node(pool, charge, "charging-type");
+				aoc_xml_set_node_content(pool, charging_type,
+						entry->rate.duration.charging_type ? "step-function" :
+						"continuous");
+			}
+		}
+
+		xml = pj_pool_alloc(pool, xml_max_size);
+		size = pj_xml_print(aoc, xml, xml_max_size - 1, PJ_TRUE);
+		if (size >= xml_max_size) {
+			ast_log(LOG_ERROR, "aoc+xml body text too large\n");
+			return 1;
+		}
+		xml[size] = 0;
+
+		if (adata->channel_state == AST_STATE_UP ||
+				adata->session->call_direction == AST_SIP_SESSION_OUTGOING_CALL) {
+			struct pjsip_tx_data *tdata;
+			struct ast_sip_body body = {
+				.type = "application",
+				.subtype = "vnd.etsi.aoc+xml",
+				.body_text = xml
+			};
+
+			if (ast_sip_create_request("INFO", adata->session->inv_session->dlg,
+					adata->session->endpoint, NULL, NULL, &tdata)) {
+				ast_log(LOG_ERROR, "Could not create AOC INFO request\n");
+				return 1;
+			}
+			if (ast_sip_add_body(tdata, &body)) {
+				ast_log(LOG_ERROR, "Could not add body to AOC INFO request\n");
+				pjsip_tx_data_dec_ref(tdata);
+				return 1;
+			}
+			ast_sip_session_send_request(adata->session, tdata);
+		} else {
+			RAII_VAR(struct ast_datastore *, datastore,
+					ast_sip_session_get_datastore(adata->session, aoc_s_datastore.type),
+					ao2_cleanup);
+			if (!datastore) {
+				datastore = ast_sip_session_alloc_datastore(&aoc_s_datastore, aoc_s_datastore.type);
+				if (!datastore) {
+					ast_log(LOG_ERROR, "Unable to create datastore for AOC-S.\n");
+					return 1;
+				}
+				if (ast_sip_session_add_datastore(adata->session, datastore)) {
+					ast_log(LOG_ERROR, "Unable to create datastore for AOC-S.\n");
+					return 1;
+				}
+			} else {
+				ast_free(datastore->data);
+			}
+			datastore->data = ast_strdup(xml);
+		}
+	}
+
+	return 0;
+}
+
+static void aoc_data_destroy(void * data)
+{
+	struct aoc_data *adata = data;
+
+	ast_aoc_destroy_decoded(adata->decoded);
+	ao2_cleanup(adata->session);
+}
+
+static struct ast_frame *aoc_framehook(struct ast_channel *ast, struct ast_frame *f,
+		enum ast_framehook_event event, void *data)
+{
+	struct ast_sip_channel_pvt *channel;
+	struct aoc_data *adata;
+
+	if (!f || f->frametype != AST_FRAME_CONTROL || event != AST_FRAMEHOOK_EVENT_WRITE ||
+			f->subclass.integer != AST_CONTROL_AOC) {
+		return f;
+	}
+
+	adata = ao2_alloc(sizeof(struct aoc_data), aoc_data_destroy);
+	if (!adata) {
+		ast_log(LOG_ERROR, "Failed to allocate AOC data\n");
+		return f;
+	}
+
+	adata->decoded = ast_aoc_decode((struct ast_aoc_encoded *) f->data.ptr, f->datalen, ast);
+	if (!adata->decoded) {
+		ast_log(LOG_ERROR, "Error decoding indicated AOC data\n");
+		ao2_ref(adata, -1);
+		return f;
+	}
+
+	channel = ast_channel_tech_pvt(ast);
+	adata->session = ao2_bump(channel->session);
+	adata->channel_state = ast_channel_state(ast);
+
+	if (ast_sip_push_task(adata->session->serializer, aoc_send_as_xml, adata)) {
+		ast_log(LOG_ERROR, "Unable to send AOC XML for channel %s\n", ast_channel_name(ast));
+		ao2_ref(adata, -1);
+	}
+	return &ast_null_frame;
+}
+
+static int aoc_consume(void *data, enum ast_frame_type type)
+{
+	return (type == AST_FRAME_CONTROL) ? 1 : 0;
+}
+
+static void aoc_attach_framehook(struct ast_sip_session *session)
+{
+	int framehook_id;
+	static struct ast_framehook_interface hook = {
+		.version = AST_FRAMEHOOK_INTERFACE_VERSION,
+		.event_cb = aoc_framehook,
+		.consume_cb = aoc_consume,
+	};
+
+	if (!session->channel || !session->endpoint->send_aoc) {
+		return;
+	}
+
+	ast_channel_lock(session->channel);
+
+	framehook_id = ast_framehook_attach(session->channel, &hook);
+	if (framehook_id < 0) {
+		ast_log(LOG_WARNING, "Could not attach AOC Frame hook, AOC will be unavailable on '%s'\n",
+			ast_channel_name(session->channel));
+	}
+
+	ast_channel_unlock(session->channel);
+}
+
+static int aoc_incoming_invite_request(struct ast_sip_session *session,
+		struct pjsip_rx_data *rdata)
+{
+	aoc_attach_framehook(session);
+	return 0;
+}
+
+static void aoc_outgoing_invite_request(struct ast_sip_session *session,
+		struct pjsip_tx_data *tdata)
+{
+	aoc_attach_framehook(session);
+}
+
+static void aoc_bye_outgoing_response(struct ast_sip_session *session,
+		struct pjsip_tx_data *tdata)
+{
+	struct ast_sip_body body = {
+		.type = "application",
+		.subtype = "vnd.etsi.aoc+xml",
+	};
+	RAII_VAR(struct ast_datastore *, datastore_d, ast_sip_session_get_datastore(session,
+			aoc_d_datastore.type), ao2_cleanup);
+	RAII_VAR(struct ast_datastore *, datastore_e, ast_sip_session_get_datastore(session,
+			aoc_e_datastore.type), ao2_cleanup);
+
+	if (datastore_e) {
+		body.body_text = datastore_e->data;
+	} else if (datastore_d) {
+		body.body_text = datastore_d->data;
+	}
+	else {
+		return;
+	}
+
+	if (ast_sip_add_body(tdata, &body)) {
+		ast_log(LOG_ERROR, "Could not add body to AOC INFO request\n");
+	}
+}
+
+static void aoc_bye_outgoing_request(struct ast_sip_session *session,
+		struct pjsip_tx_data *tdata)
+{
+	struct ast_sip_body body = {
+		.type = "application",
+		.subtype = "vnd.etsi.aoc+xml",
+	};
+	RAII_VAR(struct ast_datastore *, datastore_d, ast_sip_session_get_datastore(session,
+			aoc_d_datastore.type), ao2_cleanup);
+	RAII_VAR(struct ast_datastore *, datastore_e, ast_sip_session_get_datastore(session,
+			aoc_e_datastore.type), ao2_cleanup);
+
+	if (datastore_e) {
+		body.body_text = datastore_e->data;
+	} else if (datastore_d) {
+		body.body_text = datastore_d->data;
+	}
+	else {
+		return;
+	}
+
+	if (ast_sip_add_body(tdata, &body)) {
+		ast_log(LOG_ERROR, "Could not add body to AOC INFO request\n");
+	}
+}
+
+static void aoc_invite_outgoing_response(struct ast_sip_session *session,
+		struct pjsip_tx_data *tdata)
+{
+	pjsip_msg_body *multipart_body;
+	pjsip_multipart_part *part;
+	pj_str_t body_text;
+	pj_str_t type;
+	pj_str_t subtype;
+	RAII_VAR(struct ast_datastore *, datastore, ast_sip_session_get_datastore(session,
+			aoc_s_datastore.type), ao2_cleanup);
+
+	if (tdata->msg->line.status.code != 180 && tdata->msg->line.status.code != 183 &&
+			tdata->msg->line.status.code != 200) {
+		return;
+	}
+
+	if (!datastore) {
+		return;
+	}
+
+	if (pjsip_media_type_cmp(&tdata->msg->body->content_type,
+			&pjsip_media_type_multipart_mixed, 0) == 0) {
+		multipart_body = tdata->msg->body;
+	} else {
+		pjsip_sdp_info *tdata_sdp_info;
+
+		tdata_sdp_info = pjsip_tdata_get_sdp_info(tdata);
+		if (tdata_sdp_info->sdp) {
+			pj_status_t rc;
+
+			rc = pjsip_create_multipart_sdp_body(tdata->pool, tdata_sdp_info->sdp,
+					&multipart_body);
+			if (rc != PJ_SUCCESS) {
+				ast_log(LOG_ERROR, "Unable to create sdp multipart body\n");
+				return;
+			}
+		} else {
+			multipart_body = pjsip_multipart_create(tdata->pool,
+					&pjsip_media_type_multipart_mixed, NULL);
+		}
+	}
+
+	part = pjsip_multipart_create_part(tdata->pool);
+	pj_strdup2(tdata->pool, &body_text, datastore->data);
+	pj_cstr(&type, "application");
+	pj_cstr(&subtype, "vnd.etsi.aoc+xml");
+	part->body = pjsip_msg_body_create(tdata->pool, &type, &subtype, &body_text);
+	pjsip_multipart_add_part(tdata->pool, multipart_body, part);
+
+	tdata->msg->body = multipart_body;
+}
+
+static struct ast_sip_session_supplement aoc_bye_supplement = {
+	.method = "BYE",
+	.priority = AST_SIP_SUPPLEMENT_PRIORITY_LAST,
+	.outgoing_request = aoc_bye_outgoing_request,
+	.outgoing_response = aoc_bye_outgoing_response,
+};
+
+static struct ast_sip_session_supplement aoc_invite_supplement = {
+	.method = "INVITE",
+	.priority = AST_SIP_SUPPLEMENT_PRIORITY_LAST,
+	.incoming_request = aoc_incoming_invite_request,
+	.outgoing_request = aoc_outgoing_invite_request,
+	.outgoing_response = aoc_invite_outgoing_response,
+};
+
+static int load_module(void)
+{
+	ast_sip_session_register_supplement(&aoc_bye_supplement);
+	ast_sip_session_register_supplement(&aoc_invite_supplement);
+	return AST_MODULE_LOAD_SUCCESS;
+}
+
+static int unload_module(void)
+{
+	ast_sip_session_unregister_supplement(&aoc_bye_supplement);
+	ast_sip_session_unregister_supplement(&aoc_invite_supplement);
+	return 0;
+}
+
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "PJSIP AOC Support",
+	.support_level = AST_MODULE_SUPPORT_EXTENDED,
+	.load = load_module,
+	.unload = unload_module,
+	.load_pri = AST_MODPRI_CHANNEL_DEPEND,
+	.requires = "res_pjsip",
+);
diff --git a/res/res_pjsip_session.c b/res/res_pjsip_session.c
index 67dcdf5..652b735 100644
--- a/res/res_pjsip_session.c
+++ b/res/res_pjsip_session.c
@@ -2568,13 +2568,20 @@
 
 void ast_sip_session_send_response(struct ast_sip_session *session, pjsip_tx_data *tdata)
 {
-	handle_outgoing_response(session, tdata);
+	pjsip_dialog *dlg = pjsip_tdata_get_dlg(tdata);
+	RAII_VAR(struct ast_sip_session *, dlg_session, dlg ? ast_sip_dialog_get_session(dlg) : NULL, ao2_cleanup);
+	if (!dlg_session) {
+		/* If the dialog has a session, handle_outgoing_response will be called
+		   from session_on_tx_response. If it does not, call it from here. */
+		handle_outgoing_response(session, tdata);
+	}
 	pjsip_inv_send_msg(session->inv_session, tdata);
 	return;
 }
 
 static pj_bool_t session_on_rx_request(pjsip_rx_data *rdata);
 static pj_bool_t session_on_rx_response(pjsip_rx_data *rdata);
+static pj_status_t session_on_tx_response(pjsip_tx_data *tdata);
 static void session_on_tsx_state(pjsip_transaction *tsx, pjsip_event *e);
 
 static pjsip_module session_module = {
@@ -2583,6 +2590,7 @@
 	.on_rx_request = session_on_rx_request,
 	.on_rx_response = session_on_rx_response,
 	.on_tsx_state = session_on_tsx_state,
+	.on_tx_response = session_on_tx_response,
 };
 
 /*! \brief Determine whether the SDP provided requires deferral of negotiating or not
@@ -4266,6 +4274,18 @@
 		handled == PJ_TRUE ? "yes" : "no");
 }
 
+
+static pj_bool_t session_on_tx_response(pjsip_tx_data *tdata)
+{
+	pjsip_dialog *dlg = pjsip_tdata_get_dlg(tdata);
+	RAII_VAR(struct ast_sip_session *, session, dlg ? ast_sip_dialog_get_session(dlg) : NULL, ao2_cleanup);
+	if (session) {
+		handle_outgoing_response(session, tdata);
+	}
+
+	return PJ_SUCCESS;
+}
+
 static void resend_reinvite(pj_timer_heap_t *timer, pj_timer_entry *entry)
 {
 	struct ast_sip_session *session = entry->user_data;

-- 
To view, visit https://gerrit.asterisk.org/c/asterisk/+/19460
To unsubscribe, or for help writing mail filters, visit https://gerrit.asterisk.org/settings

Gerrit-Project: asterisk
Gerrit-Branch: master
Gerrit-Change-Id: Iebb7ad0d5f88526bc6629d3a1f9f11665434d333
Gerrit-Change-Number: 19460
Gerrit-PatchSet: 16
Gerrit-Owner: Michael Kuron <m.kuron at gmx.de>
Gerrit-Reviewer: Friendly Automation
Gerrit-Reviewer: George Joseph <gjoseph at digium.com>
Gerrit-Reviewer: Joshua Colp <jcolp at sangoma.com>
Gerrit-Reviewer: N A <asterisk at phreaknet.org>
Gerrit-MessageType: merged
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.digium.com/pipermail/asterisk-code-review/attachments/20221209/1d035db8/attachment-0001.html>


More information about the asterisk-code-review mailing list