diff --git a/configs/samples/pjsip.conf.sample b/configs/samples/pjsip.conf.sample index 9d73843030..a74b50fac5 100644 --- a/configs/samples/pjsip.conf.sample +++ b/configs/samples/pjsip.conf.sample @@ -955,6 +955,17 @@ ; responses. ; (default: no) +;geoloc_incoming_call_profile = + ; This geolocation profile will be applied to all calls received + ; by the channel driver from the remote endpoint before they're + ; forwarded to the dialplan. +;geoloc_outgoing_call_profile = + ; This geolocation profile will be applied to all calls received + ; by the channel driver from the dialplan before they're forwarded + ; the remote endpoint. +; + + ;==========================AUTH SECTION OPTIONS========================= ;[auth] ; SYNOPSIS: Authentication type diff --git a/contrib/ast-db-manage/config/versions/7197536bb68d_geoloc_endpoint_params.py b/contrib/ast-db-manage/config/versions/7197536bb68d_geoloc_endpoint_params.py new file mode 100644 index 0000000000..5159ff48f1 --- /dev/null +++ b/contrib/ast-db-manage/config/versions/7197536bb68d_geoloc_endpoint_params.py @@ -0,0 +1,22 @@ +"""Geoloc Endpoint Params + +Revision ID: 7197536bb68d +Revises: 58e440314c2a +Create Date: 2022-03-07 05:32:54.909429 + +""" + +# revision identifiers, used by Alembic. +revision = '7197536bb68d' +down_revision = '58e440314c2a' + +from alembic import op +import sqlalchemy as sa + +def upgrade(): + op.add_column('ps_endpoints', sa.Column('geoloc_incoming_call_profile', sa.String(80))) + op.add_column('ps_endpoints', sa.Column('geoloc_outgoing_call_profile', sa.String(80))) + +def downgrade(): + op.drop_column('ps_endpoints', 'geoloc_outgoing_call_profile') + op.drop_column('ps_endpoints', 'geoloc_incoming_call_profile') diff --git a/doc/CHANGES-staging/res_pjsip_geolocation.txt b/doc/CHANGES-staging/res_pjsip_geolocation.txt new file mode 100644 index 0000000000..acc49063e0 --- /dev/null +++ b/doc/CHANGES-staging/res_pjsip_geolocation.txt @@ -0,0 +1,4 @@ +Subject: res_pjsip_geolocation + +Added res_pjsip_geolocation which gives chan_pjsip +the ability to use the core geolocation capabilities. diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h index b5b5a7256d..b3717e722f 100644 --- a/include/asterisk/res_pjsip.h +++ b/include/asterisk/res_pjsip.h @@ -63,6 +63,9 @@ #define PJSIP_EXPIRES_NOT_SPECIFIED ((pj_uint32_t)-1) #endif +#define PJSTR_PRINTF_SPEC "%.*s" +#define PJSTR_PRINTF_VAR(_v) ((int)(_v).slen), ((_v).ptr) + /* Response codes from RFC8224 */ #define AST_STIR_SHAKEN_RESPONSE_CODE_STALE_DATE 403 #define AST_STIR_SHAKEN_RESPONSE_CODE_USE_IDENTITY_HEADER 428 @@ -957,6 +960,10 @@ struct ast_sip_endpoint { unsigned int stir_shaken; /*! Should we authenticate OPTIONS requests per RFC 3261? */ unsigned int allow_unauthenticated_options; + /*! The name of the geoloc profile to apply when Asterisk receives a call from this endpoint */ + AST_STRING_FIELD_EXTENDED(geoloc_incoming_call_profile); + /*! The name of the geoloc profile to apply when Asterisk sends a call to this endpoint */ + AST_STRING_FIELD_EXTENDED(geoloc_outgoing_call_profile); }; /*! URI parameter for symmetric transport */ @@ -2457,6 +2464,17 @@ int ast_sip_set_outbound_proxy(pjsip_tx_data *tdata, const char *proxy); */ int ast_sip_add_header(pjsip_tx_data *tdata, const char *name, const char *value); +/*! + * \brief Add a header to an outbound SIP message, returning a pointer to the header + * + * \param tdata The message to add the header to + * \param name The header name + * \param value The header value + * \return The pjsip_generic_string_hdr * added. + */ +pjsip_generic_string_hdr *ast_sip_add_header2(pjsip_tx_data *tdata, + const char *name, const char *value); + /*! * \brief Add a body to an outbound SIP message * diff --git a/res/res_pjsip.c b/res/res_pjsip.c index 78226450e4..d722bbe0fd 100644 --- a/res/res_pjsip.c +++ b/res/res_pjsip.c @@ -53,6 +53,7 @@ res_sorcery_memory res_sorcery_astdb res_statsd + res_geolocation core ***/ @@ -1870,6 +1871,22 @@ int ast_sip_add_header(pjsip_tx_data *tdata, const char *name, const char *value return 0; } +pjsip_generic_string_hdr *ast_sip_add_header2(pjsip_tx_data *tdata, + const char *name, const char *value) +{ + pj_str_t hdr_name; + pj_str_t hdr_value; + pjsip_generic_string_hdr *hdr; + + pj_cstr(&hdr_name, name); + pj_cstr(&hdr_value, value); + + hdr = pjsip_generic_string_hdr_create(tdata->pool, &hdr_name, &hdr_value); + + pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr *) hdr); + return hdr; +} + static pjsip_msg_body *ast_body_to_pjsip_body(pj_pool_t *pool, const struct ast_sip_body *body) { pj_str_t type; @@ -2810,5 +2827,5 @@ AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ .reload = reload_module, .load_pri = AST_MODPRI_CHANNEL_DEPEND - 5, .requires = "dnsmgr,res_pjproject,res_sorcery_config,res_sorcery_memory,res_sorcery_astdb", - .optional_modules = "res_statsd", + .optional_modules = "res_geolocation,res_statsd", ); diff --git a/res/res_pjsip/pjsip_config.xml b/res/res_pjsip/pjsip_config.xml index e6fca2e86b..63fd1a961f 100644 --- a/res/res_pjsip/pjsip_config.xml +++ b/res/res_pjsip/pjsip_config.xml @@ -1448,6 +1448,24 @@ responses. + + Geolocation profile to apply to incoming calls + + This geolocation profile will be applied to all calls received + by the channel driver from the remote endpoint before they're + forwarded to the dialplan. + + + + + Geolocation profile to apply to outgoing calls + + This geolocation profile will be applied to all calls received + by the channel driver from the dialplan before they're forwarded + the remote endpoint. + + + Authentication type diff --git a/res/res_pjsip/pjsip_configuration.c b/res/res_pjsip/pjsip_configuration.c index ace68d2fdc..5f1f61e397 100644 --- a/res/res_pjsip/pjsip_configuration.c +++ b/res/res_pjsip/pjsip_configuration.c @@ -21,6 +21,7 @@ #include #include +#include "asterisk/res_geolocation.h" #include "asterisk/res_pjsip.h" #include "include/res_pjsip_private.h" #include "asterisk/res_pjsip_cli.h" @@ -1539,6 +1540,36 @@ static int sip_endpoint_apply_handler(const struct ast_sorcery *sorcery, void *o } } + if (!ast_strlen_zero(endpoint->geoloc_incoming_call_profile) || + !ast_strlen_zero(endpoint->geoloc_outgoing_call_profile)) { + + if (!ast_geoloc_is_loaded()) { + ast_log(LOG_ERROR, "A geoloc incoming and/or outgoing call_profile was specified on endpoint '%s'" + " but res_geolocation is not loaded.\n", ast_sorcery_object_get_id(endpoint)); + return -1; + } + + if (!ast_strlen_zero(endpoint->geoloc_incoming_call_profile)) { + struct ast_geoloc_profile *profile = ast_geoloc_get_profile(endpoint->geoloc_incoming_call_profile); + if (!profile) { + ast_log(LOG_ERROR, "geoloc_incoming_call_profile '%s' on endpoint '%s' doesn't exist\n", + endpoint->geoloc_incoming_call_profile, ast_sorcery_object_get_id(endpoint)); + return -1; + } + ao2_cleanup(profile); + } + + if (!ast_strlen_zero(endpoint->geoloc_outgoing_call_profile)) { + struct ast_geoloc_profile *profile = ast_geoloc_get_profile(endpoint->geoloc_outgoing_call_profile); + if (!profile) { + ast_log(LOG_ERROR, "geoloc_outgoing_call_profile '%s' on endpoint '%s' doesn't exist\n", + endpoint->geoloc_outgoing_call_profile, ast_sorcery_object_get_id(endpoint)); + return -1; + } + ao2_cleanup(profile); + } + } + return 0; } @@ -2194,6 +2225,8 @@ int ast_res_pjsip_initialize_configuration(void) ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "stir_shaken", "off", stir_shaken_handler, stir_shaken_to_str, NULL, 0, 0); ast_sorcery_object_field_register(sip_sorcery, "endpoint", "stir_shaken_profile", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, stir_shaken_profile)); ast_sorcery_object_field_register(sip_sorcery, "endpoint", "allow_unauthenticated_options", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, allow_unauthenticated_options)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "geoloc_incoming_call_profile", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, geoloc_incoming_call_profile)); + 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)); if (ast_sip_initialize_sorcery_transport()) { ast_log(LOG_ERROR, "Failed to register SIP transport support with sorcery\n"); @@ -2354,6 +2387,12 @@ void *ast_sip_endpoint_alloc(const char *name) return NULL; } + if (ast_string_field_init_extended(endpoint, geoloc_incoming_call_profile) || + ast_string_field_init_extended(endpoint, geoloc_outgoing_call_profile)) { + ao2_cleanup(endpoint); + return NULL; + } + if (!(endpoint->media.codecs = ast_format_cap_alloc(AST_FORMAT_CAP_FLAG_DEFAULT))) { ao2_cleanup(endpoint); return NULL; diff --git a/res/res_pjsip_geolocation.c b/res/res_pjsip_geolocation.c new file mode 100644 index 0000000000..c4ba0038ef --- /dev/null +++ b/res/res_pjsip_geolocation.c @@ -0,0 +1,713 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2022, Sangoma Technologies Corporation + * + * George Joseph + * + * 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 + res_geolocation + pjproject + res_pjsip + res_pjsip_session + chan_pjsip + libxml2 + core + ***/ + +#include "asterisk.h" +#include "asterisk/module.h" +#include "asterisk/xml.h" +#include "asterisk/res_geolocation.h" + +#include +#include "asterisk/res_pjsip.h" +#include "asterisk/res_pjsip_session.h" + +static pj_str_t GEOLOCATION_HDR; + +static int find_pidf(const char *session_name, struct pjsip_rx_data *rdata, char *geoloc_uri, + char **pidf_body, unsigned int *pidf_len) +{ + char *local_uri = ast_strdupa(geoloc_uri); + char *ra = NULL; + /* + * If the URI is "cid" then we're going to search for a pidf document + * in the body of the message. If there's no body, there's no point. + */ + if (!rdata->msg_info.msg->body) { + ast_log(LOG_WARNING, "%s: There's no message body in which to search for '%s'. Skipping\n", + session_name, geoloc_uri); + return -1; + } + + if (local_uri[0] == '<') { + local_uri++; + } + ra = strchr(local_uri, '>'); + if (ra) { + *ra = '\0'; + } + + /* + * If the message content type is 'application/pidf+xml', then the pidf is + * the only document in the message and we'll just parse the entire body + * as xml. If it's 'multipart/mixed' then we have to find the part that + * has a Content-ID header value matching the URI. + */ + if (ast_sip_are_media_types_equal(&rdata->msg_info.ctype->media, + &pjsip_media_type_application_pidf_xml)) { + *pidf_body = rdata->msg_info.msg->body->data; + *pidf_len = rdata->msg_info.msg->body->len; + } else if (ast_sip_are_media_types_equal(&rdata->msg_info.ctype->media, + &pjsip_media_type_multipart_mixed)) { + pj_str_t cid = pj_str(local_uri); + pjsip_multipart_part *mp = pjsip_multipart_find_part_by_cid_str( + rdata->tp_info.pool, rdata->msg_info.msg->body, &cid); + + if (!mp) { + ast_log(LOG_WARNING, "%s: A Geolocation header was found with URI '%s'" + " but the associated multipart part was not found in the message body. Skipping URI", + session_name, geoloc_uri); + return -1; + } + *pidf_body = mp->body->data; + *pidf_len = mp->body->len; + } else { + ast_log(LOG_WARNING, "%s: A Geolocation header was found with URI '%s'" + " but no pidf document with that content id was found. Skipping URI", + session_name, geoloc_uri); + return -1; + } + + return 0; +} + + +static int handle_incoming_request(struct ast_sip_session *session, struct pjsip_rx_data *rdata) +{ + const char *session_name = (session ? ast_sip_session_get_name(session) : "NULL_SESSION"); + struct ast_sip_endpoint *endpoint = (session ? session->endpoint : NULL); + struct ast_channel *channel = (session ? session->channel : NULL); + RAII_VAR(struct ast_geoloc_profile *, config_profile, NULL, ao2_cleanup); + RAII_VAR(struct ast_geoloc_eprofile *, eprofile, NULL, ao2_cleanup); + RAII_VAR(struct ast_datastore *, ds, NULL, ast_datastore_free); + size_t eprofile_count = 0; + char *geoloc_hdr_value = NULL; + char *geoloc_uri = NULL; + int rc = 0; + RAII_VAR(struct ast_str *, buf, ast_str_create(1024), ast_free); + pjsip_generic_string_hdr *geoloc_hdr = NULL; + SCOPE_ENTER(3, "%s\n", session_name); + + if (!session) { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_WARNING, "%s: session is NULL!!!. Skipping.\n", + session_name); + } + if (!endpoint) { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_WARNING, "%s: Session has no endpoint. Skipping.\n", + session_name); + } + + if (!channel) { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_WARNING, "%s: Session has no channel. Skipping.\n", + session_name); + } + + if (!rdata) { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_WARNING, "%s: Session has no rdata. Skipping.\n", + session_name); + } + + geoloc_hdr = pjsip_msg_find_hdr_by_name(rdata->msg_info.msg, &GEOLOCATION_HDR, NULL); + + if (!geoloc_hdr) { + ast_trace(4, "%s: Message has no Geolocation header\n", session_name); + } else { + ast_trace(4, "%s: Geolocation: " PJSTR_PRINTF_SPEC "\n", session_name, + PJSTR_PRINTF_VAR(geoloc_hdr->hvalue)); + } + + if (ast_strlen_zero(endpoint->geoloc_incoming_call_profile)) { + if (geoloc_hdr) { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_NOTICE, "%s: Message has Geolocation header '" + PJSTR_PRINTF_SPEC "' but endpoint has no geoloc_incoming_call_profile. " + "Geolocation info discarded.\n", session_name, + PJSTR_PRINTF_VAR(geoloc_hdr->hvalue)); + } else { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_NOTICE, "%s: Endpoint has no geoloc_incoming_call_profile. " + "Skipping.\n", session_name); + } + } + + config_profile = ast_geoloc_get_profile(endpoint->geoloc_incoming_call_profile); + if (!config_profile) { + if (geoloc_hdr) { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_NOTICE, "%s: Message has Geolocation header '" + PJSTR_PRINTF_SPEC "' but endpoint's geoloc_incoming_call_profile doesn't exist. " + "Geolocation info discarded.\n", session_name, + PJSTR_PRINTF_VAR(geoloc_hdr->hvalue)); + } else { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_NOTICE, "%s: Message has no Geolocation header and endpoint has " + " an invalid geoloc_incoming_call_profile. Nothing to do..\n", session_name); + } + } + + ds = ast_geoloc_datastore_create(session_name); + if (!ds) { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_WARNING, + "%s: Couldn't allocate a geoloc datastore\n", session_name); + } + + /* + * We want the datastore to pass through the dialplan and the core + * so we need to turn inheritance on. + */ + ast_geoloc_datastore_set_inheritance(ds, 1); + + switch (config_profile->action) { + case AST_GEOLOC_ACT_DISCARD_INCOMING: + if (geoloc_hdr) { + ast_trace(4, "%s: Profile '%s' location_disposition is 'discard_incoming' so " + "discarding Geolocation: " PJSTR_PRINTF_SPEC "\n", session_name, + ast_sorcery_object_get_id(config_profile), + PJSTR_PRINTF_VAR(geoloc_hdr->hvalue)); + } else { + ast_trace(4, "%s: Profile '%s' location_disposition is 'discard_incoming' but there was no Geolocation header" + "so there's nothing to discard\n", + session_name, ast_sorcery_object_get_id(config_profile)); + } + + eprofile = ast_geoloc_eprofile_create_from_profile(config_profile); + if (!eprofile) { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_WARNING, "%s: Unable to create eprofile from " + "profile '%s'\n", session_name, ast_sorcery_object_get_id(config_profile)); + } + + if (!eprofile->effective_location) { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_NOTICE, "%s: Configured profile has no effective location. Skipping." + "profile '%s'\n", session_name, ast_sorcery_object_get_id(eprofile)); + } + + rc = ast_geoloc_datastore_add_eprofile(ds, eprofile); + if (rc <= 0) { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_WARNING, + "%s: Couldn't add eprofile '%s' to datastore\n", session_name, + eprofile->id); + } + + ast_channel_lock(channel); + ast_channel_datastore_add(channel, ds); + ast_channel_unlock(channel); + /* We gave the datastore to the channel so don't let RAII_VAR clean it up. */ + ds = NULL; + + ast_trace(4, "ep: '%s' EffectiveLoc: %s\n", eprofile->id, ast_str_buffer( + ast_variable_list_join(eprofile->effective_location, ",", "=", NULL, &buf))); + ast_str_reset(buf); + + /* We discarded the Geolocation header so there's no need to go on. */ + SCOPE_EXIT_RTN_VALUE(0, "%s: Added geoloc datastore with 1 eprofile from config\n", + session_name); + + case AST_GEOLOC_ACT_DISCARD_CONFIG: + if (geoloc_hdr) { + ast_trace(4, "%s: Profile '%s' location_disposition is 'discard_config' so " + "discarding config profile\n", session_name, ast_sorcery_object_get_id(config_profile)); + /* We process the Geolocation header down below. */ + } else { + /* Discarded the config and there's no Geolocation header so we're done. */ + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_NOTICE, "%s: Profile '%s' location_disposition is 'discard_config' but " + "there was no Geolocation header so there's nothing left to process\n", + session_name, ast_sorcery_object_get_id(config_profile)); + } + break; + + case AST_GEOLOC_ACT_PREFER_CONFIG: + eprofile = ast_geoloc_eprofile_create_from_profile(config_profile); + if (!eprofile) { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_WARNING, "%s: Unable to create eprofile from " + "profile '%s'\n", session_name, ast_sorcery_object_get_id(config_profile)); + } + + if (!eprofile->effective_location) { + if (geoloc_hdr) { + ast_trace(4, "%s: Profile '%s' location_disposition is 'prefer_config' but the configured" + "eprofile has no location information. Falling back to Geolocation: " + PJSTR_PRINTF_SPEC "\n", session_name, ast_sorcery_object_get_id(config_profile), + PJSTR_PRINTF_VAR(geoloc_hdr->hvalue)); + /* We process the Geolocation header down below. */ + } else { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_NOTICE, "%s: Configured profile '%s' has no effective location" + " and there was no Geolocation header. Skipping.\n", + session_name, ast_sorcery_object_get_id(eprofile)); + } + break; + } + + rc = ast_geoloc_datastore_add_eprofile(ds, eprofile); + if (rc <= 0) { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_WARNING, + "%s: Couldn't add eprofile '%s' to datastore\n", session_name, + eprofile->id); + } + + ast_channel_lock(channel); + ast_channel_datastore_add(channel, ds); + ast_channel_unlock(channel); + /* We gave the datastore to the channel so don't let RAII_VAR clean it up. */ + ds = NULL; + + if (geoloc_hdr) { + ast_trace(4, "%s: Profile '%s' location_disposition is 'prefer_config' so " + "discarding Geolocation: " PJSTR_PRINTF_SPEC "\n", + session_name, ast_sorcery_object_get_id(config_profile), PJSTR_PRINTF_VAR(geoloc_hdr->hvalue)); + } + + ast_trace(4, "ep: '%s' EffectiveLoc: %s\n", eprofile->id, ast_str_buffer( + ast_variable_list_join(eprofile->effective_location, ",", "=", NULL, &buf))); + ast_str_reset(buf); + + /* We discarded the Geolocation header so there's no need to go on. */ + SCOPE_EXIT_RTN_VALUE(0, "%s: Added geoloc datastore with 1 eprofile from config\n", + session_name); + + case AST_GEOLOC_ACT_PREFER_INCOMING: + if (geoloc_hdr) { + ast_trace(4, "%s: Profile '%s' location_disposition is 'replace' so " + "we don't need to do anything with the configured profile", session_name, + ast_sorcery_object_get_id(config_profile)); + } else { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_NOTICE, + "%s: Profile '%s' location_disposition is 'replace' but there's " + "no Geolocation header and therefore no location info to replace" + "it with\n", session_name, ast_sorcery_object_get_id(config_profile)); + } + } + + geoloc_hdr_value = ast_alloca(geoloc_hdr->hvalue.slen + 1); + ast_copy_pj_str(geoloc_hdr_value, &geoloc_hdr->hvalue, geoloc_hdr->hvalue.slen + 1); + + /* + * From RFC-6442: + * Geolocation-header = "Geolocation" HCOLON locationValue + * *( COMMA locationValue ) + * locationValue = LAQUOT locationURI RAQUOT + * *(SEMI geoloc-param) + * locationURI = sip-URI / sips-URI / pres-URI + * / http-URI / https-URI + * / cid-url ; (from RFC 2392) + * / absoluteURI ; (from RFC 3261) + */ + while((geoloc_uri = ast_strsep(&geoloc_hdr_value, ',', AST_STRSEP_TRIM))) { + /* geoloc_uri should now be [;loc-src=fqdn] */ + char *pidf_body = NULL; + unsigned int pidf_len = 0; + struct ast_xml_doc *incoming_doc = NULL; + struct ast_geoloc_eprofile *eprofile = NULL; + int rc = 0; + + ast_trace(4, "Processing URI '%s'\n", geoloc_uri); + + if (geoloc_uri[0] != '<' || strchr(geoloc_uri, '>') == NULL) { + ast_log(LOG_WARNING, "%s: Geolocation header has bad URI '%s'. Skipping\n", session_name, + geoloc_uri); + continue; + } + /* + * If the URI isn't "cid" then we're just going to pass it through. + */ + if (!ast_begins_with(geoloc_uri, "action = config_profile->action; + + ast_trace(4, "Processing URI '%s'. Adding to datastore\n", geoloc_uri); + rc = ast_geoloc_datastore_add_eprofile(ds, eprofile); + ao2_ref(eprofile, -1); + if (rc <= 0) { + ast_log(LOG_WARNING, "%s: Unable to add effective profile for URI '%s' to datastore. Skipping\n", + session_name, geoloc_uri); + } + } + + if (config_profile->action == AST_GEOLOC_ACT_PREFER_CONFIG) { + ast_trace(4, "%s: Profile '%s' location_disposition is 'prepend' so " + "adding to datastore first", session_name, ast_sorcery_object_get_id(config_profile)); + + eprofile = ast_geoloc_eprofile_create_from_profile(config_profile); + if (!eprofile) { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_WARNING, "%s: Unable to create eprofile from" + " profile '%s'\n", session_name, ast_sorcery_object_get_id(config_profile)); + } + + rc = ast_geoloc_datastore_add_eprofile(ds, eprofile); + if (rc <= 0) { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_WARNING, + "%s: Couldn't add eprofile '%s' to datastore\n", session_name, + eprofile->id); + } + } + + eprofile_count = ast_geoloc_datastore_size(ds); + if (eprofile_count == 0) { + SCOPE_EXIT_RTN_VALUE(0, + "%s: Unable to add any effective profiles. Not adding datastore to channel.\n", + session_name); + } + + ast_channel_lock(channel); + ast_channel_datastore_add(channel, ds); + ast_channel_unlock(channel); + ds = NULL; + + SCOPE_EXIT_RTN_VALUE(0, "%s: Added geoloc datastore with %" PRIu64 " eprofiles\n", + session_name, eprofile_count); +} + +static int add_pidf_to_tdata(struct ast_datastore *tempds, struct ast_channel *channel, + struct ast_vector_string *uris, int pidf_index, struct pjsip_tx_data *tdata, const char *session_name) +{ + static const pj_str_t from_name = { "From", 4}; + static const pj_str_t cid_name = { "Content-ID", 10 }; + + pjsip_sip_uri *sip_uri; + pjsip_generic_string_hdr *cid; + pj_str_t cid_value; + pjsip_from_hdr *from = pjsip_msg_find_hdr_by_name(tdata->msg, &from_name, NULL); + pjsip_sdp_info *tdata_sdp_info; + pjsip_msg_body *multipart_body = NULL; + pjsip_multipart_part *pidf_part; + pj_str_t pidf_body_text; + char id[6]; + size_t alloc_size; + RAII_VAR(char *, base_cid, NULL, ast_free); + const char *final; + int rc = 0; + RAII_VAR(struct ast_str *, buf, ast_str_create(1024), ast_free); + SCOPE_ENTER(3, "%s\n", session_name); + + /* + * ast_geoloc_eprofiles_to_pidf() takes the datastore with all of the eprofiles + * in it, skips over the ones not needing PIDF processing and combines the + * rest into one document. + */ + final = ast_geoloc_eprofiles_to_pidf(tempds, channel, &buf, session_name); + ast_trace(5, "Final pidf: \n%s\n", final); + + /* + * There _should_ be an SDP already attached to the tdata at this point + * but maybe not. If we can find an existing one, we'll convert the tdata + * body into a multipart body and add the SDP as the first part. Then we'll + * create another part to hold the PIDF. + * + * If we don't find one, we're going to create an empty multipart body + * and add the PIDF part to it. + * + * Technically, if we only have the PIDF, we don't need a multipart + * body to hold it but that means we'd have to add the Content-ID header + * to the main SIP message. Since it's unlikely, it's just better to + * add the multipart body and leave the rest of the processing unchanged. + */ + tdata_sdp_info = pjsip_tdata_get_sdp_info(tdata); + if (tdata_sdp_info->sdp) { + ast_trace(4, "body: %p %u\n", tdata_sdp_info->sdp, (unsigned)tdata_sdp_info->sdp_err); + + rc = pjsip_create_multipart_sdp_body(tdata->pool, tdata_sdp_info->sdp, &multipart_body); + if (rc != PJ_SUCCESS) { + SCOPE_EXIT_LOG_RTN_VALUE(0, LOG_ERROR, "%s: Unable to create sdp multipart body\n", + session_name); + } + } else { + multipart_body = pjsip_multipart_create(tdata->pool, &pjsip_media_type_multipart_mixed, NULL); + } + + pidf_part = pjsip_multipart_create_part(tdata->pool); + pj_cstr(&pidf_body_text, final); + pidf_part->body = pjsip_msg_body_create(tdata->pool, &pjsip_media_type_application_pidf_xml.type, + &pjsip_media_type_application_pidf_xml.subtype, &pidf_body_text); + + pjsip_multipart_add_part(tdata->pool, multipart_body, pidf_part); + + sip_uri = (pjsip_sip_uri *)pjsip_uri_get_uri(from->uri); + alloc_size = sizeof(id) + pj_strlen(&sip_uri->host) + 2; + base_cid = ast_malloc(alloc_size); + sprintf(base_cid, "%s@%.*s", + ast_generate_random_string(id, sizeof(id)), + (int) pj_strlen(&sip_uri->host), pj_strbuf(&sip_uri->host)); + + ast_str_set(&buf, 0, "cid:%s", base_cid); + ast_trace(4, "cid: '%s' uri: '%s' pidf_index: %d\n", base_cid, ast_str_buffer(buf), pidf_index); + + AST_VECTOR_INSERT_AT(uris, pidf_index, ast_strdup(ast_str_buffer(buf))); + + cid_value.ptr = pj_pool_alloc(tdata->pool, alloc_size); + cid_value.slen = sprintf(cid_value.ptr, "<%s>", base_cid); + + cid = pjsip_generic_string_hdr_create(tdata->pool, &cid_name, &cid_value); + + pj_list_insert_after(&pidf_part->hdr, cid); + + tdata->msg->body = multipart_body; + + SCOPE_EXIT_RTN_VALUE(0, "%s: PIDF-LO added with cid '%s'\n", session_name, base_cid); +} + +static void handle_outgoing_request(struct ast_sip_session *session, struct pjsip_tx_data *tdata) +{ + const char *session_name = ast_sip_session_get_name(session); + struct ast_sip_endpoint *endpoint = session->endpoint; + struct ast_channel *channel = session->channel; + RAII_VAR(struct ast_geoloc_profile *, config_profile, NULL, ao2_cleanup); + RAII_VAR(struct ast_geoloc_eprofile *, config_eprofile, NULL, ao2_cleanup); + RAII_VAR(struct ast_str *, buf, ast_str_create(1024), ast_free); + RAII_VAR(struct ast_datastore *, tempds, NULL, ast_datastore_free); + struct ast_datastore *ds = NULL; /* The channel cleans up ds */ + struct ast_vector_string uris; + pjsip_msg_body *orig_body; + pjsip_generic_string_hdr *geoloc_hdr; + int i; + int eprofile_count = 0; + int pidf_index = -1; + int geoloc_routing = 0; + int rc = 0; + const char *final; + SCOPE_ENTER(3, "%s\n", session_name); + + if (!buf) { + SCOPE_EXIT_LOG_RTN(LOG_WARNING, "%s: Unable to allocate buf\n", + session_name); + } + + if (!endpoint) { + SCOPE_EXIT_LOG_RTN(LOG_WARNING, "%s: Session has no endpoint. Skipping.\n", + session_name); + } + + if (!channel) { + SCOPE_EXIT_LOG_RTN(LOG_WARNING, "%s: Session has no channel. Skipping.\n", + session_name); + } + + if (ast_strlen_zero(endpoint->geoloc_outgoing_call_profile)) { + SCOPE_EXIT_LOG_RTN(LOG_NOTICE, "%s: Endpoint has no geoloc_outgoing_call_profile. " + "Skipping.\n", session_name); + } + + config_profile = ast_geoloc_get_profile(endpoint->geoloc_outgoing_call_profile); + if (!config_profile) { + SCOPE_EXIT_LOG_RTN(LOG_ERROR, "%s: Endpoint's geoloc_outgoing_call_profile doesn't exist. " + "Geolocation info discarded.\n", session_name); + } + + config_eprofile = ast_geoloc_eprofile_create_from_profile(config_profile); + if (!config_eprofile) { + SCOPE_EXIT_LOG_RTN(LOG_WARNING, "%s: Unable to create eprofile from " + "profile '%s'\n", session_name, ast_sorcery_object_get_id(config_profile)); + } + + if (config_profile->action != AST_GEOLOC_ACT_PREFER_INCOMING) { + ds = ast_geoloc_datastore_find(channel); + if (!ds) { + ast_trace(4, "%s: There was no geoloc datastore\n", session_name); + } else { + eprofile_count = ast_geoloc_datastore_size(ds); + ast_trace(4, "%s: There are %d geoloc profiles on this channel\n", session_name, + eprofile_count); + } + } + + /* + * We don't want to alter the datastore that may (or may not) be on + * the channel so we're going to create a temporary one to hold the + * config eprofile plus any in the channel datastore. Technically + * we could just use a vector but the datastore already has the logic + * to release all the eprofile references and the datastore itself. + */ + tempds = ast_geoloc_datastore_create("temp"); + if (!ds) { + ast_trace(4, "%s: There are no geoloc profiles on this channel\n", session_name); + ast_geoloc_datastore_add_eprofile(tempds, config_eprofile); + } else { + if (config_profile->action == AST_GEOLOC_ACT_PREFER_CONFIG) { + ast_trace(4, "%s: prepending config_eprofile\n", session_name); + ast_geoloc_datastore_add_eprofile(tempds, config_eprofile); + } + for (i = 0; i < eprofile_count; i++) { + struct ast_geoloc_eprofile *ep = ast_geoloc_datastore_get_eprofile(ds, i); + ast_trace(4, "%s: adding eprofile '%s' from channel\n", session_name, ep->id); + ast_geoloc_datastore_add_eprofile(tempds, ep); + } + if (config_profile->action == AST_GEOLOC_ACT_PREFER_INCOMING) { + ast_trace(4, "%s: appending config_eprofile\n", session_name); + ast_geoloc_datastore_add_eprofile(tempds, config_eprofile); + } + } + + eprofile_count = ast_geoloc_datastore_size(tempds); + if (eprofile_count == 0) { + SCOPE_EXIT_RTN("%s: There are no profiles left to send\n", session_name); + } + ast_trace(4, "%s: There are now %d geoloc profiles to be sent\n", session_name, + eprofile_count); + + /* + * This vector is going to accumulate all of the URIs that + * will need to go on the Geolocation header. + */ + rc = AST_VECTOR_INIT(&uris, 2); + if (rc != 0) { + SCOPE_EXIT_LOG_RTN(LOG_ERROR, "%s: Unable to allocate memory for vector\n", session_name); + } + + /* + * It's possible that we have a list of eprofiles that have both "pass-by-reference (external URI)" + * and "pass by value (to go in PIDF)" eprofiles. The ones that just need a URI added to the + * Geolocation header get added to the "uris" vector in this loop. The ones that result in a + * PIDF though, need to be combined into a single PIDF-LO document so we're just going to + * save the first one's index so we can insert the "cid" header in the right place, then + * we'll send the whole list off to add_pidf_to_tdata() so they can be combined into a + * single document. + */ + + for (i = 0; i < eprofile_count; i++) { + struct ast_geoloc_eprofile *ep = ast_geoloc_datastore_get_eprofile(tempds, i); + ast_geoloc_eprofile_refresh_location(ep); + + ast_trace(4, "ep: '%s' EffectiveLoc: %s\n", ep->id, ast_str_buffer( + ast_variable_list_join(ep->effective_location, ",", "=", NULL, &buf))); + ast_str_reset(buf); + + if (ep->format == AST_GEOLOC_FORMAT_URI) { + final = ast_geoloc_eprofile_to_uri(ep, channel, &buf, session_name); + ast_trace(4, "URI: %s\n", final); + AST_VECTOR_APPEND(&uris, ast_strdup(final)); + ast_str_reset(buf); + } else { + /* + * If there are GML or civicAddress eprofiles, we need to save the position + * of the first one in relation to any URI ones so we can insert the "cid" + * uri for it in the original position. + */ + if (pidf_index < 0) { + pidf_index = i; + } + } + /* The LAST eprofile determines routing */ + geoloc_routing = ep->geolocation_routing; + ao2_ref(ep, -1); + } + + /* + * If we found at least one eprofile needing PIDF processing, we'll + * send the entire list off to add_pidf_to_tdata(). We're going to save + * the pointer to the original tdata body in case we need to revert + * if we can't add the headers. + */ + orig_body = tdata->msg->body; + if (pidf_index >= 0) { + rc = add_pidf_to_tdata(tempds, channel, &uris, pidf_index, tdata, session_name); + } + + /* + * Now that we have all the URIs in the vector, we'll string them together + * to create the data for the Geolocation header. + */ + ast_str_reset(buf); + for (i = 0; i < AST_VECTOR_SIZE(&uris); i++) { + char *uri = AST_VECTOR_GET(&uris, i); + ast_trace(4, "ix: %d of %d LocRef: %s\n", i, (int)AST_VECTOR_SIZE(&uris), uri); + ast_str_append(&buf, 0, "%s<%s>", (i > 0 ? "," : ""), uri); + } + + AST_VECTOR_RESET(&uris, ast_free); + AST_VECTOR_FREE(&uris); + + /* It's almost impossible for add header to fail but you never know */ + geoloc_hdr = ast_sip_add_header2(tdata, "Geolocation", ast_str_buffer(buf)); + if (geoloc_hdr == NULL) { + tdata->msg->body = orig_body; + SCOPE_EXIT_LOG_RTN(LOG_ERROR, "%s: Unable to add Geolocation header\n", session_name); + } + rc = ast_sip_add_header(tdata, "Geolocation-Routing", geoloc_routing ? "yes" : "no"); + if (rc != 0) { + tdata->msg->body = orig_body; + pj_list_erase(geoloc_hdr); + SCOPE_EXIT_LOG_RTN(LOG_ERROR, "%s: Unable to add Geolocation-Routing header\n", session_name); + } + SCOPE_EXIT_RTN("%s: Geolocation: %s\n", session_name, ast_str_buffer(buf)); +} + +static struct ast_sip_session_supplement geolocation_supplement = { + .method = "INVITE", + .priority = AST_SIP_SUPPLEMENT_PRIORITY_CHANNEL + 10, + .incoming_request = handle_incoming_request, + .outgoing_request = handle_outgoing_request, +}; + +static int reload_module(void) +{ + return 0; +} + +static int unload_module(void) +{ + int res = 0; + ast_sip_session_unregister_supplement(&geolocation_supplement); + + return res; +} + +static int load_module(void) +{ + int res = 0; + GEOLOCATION_HDR = pj_str("Geolocation"); + + ast_sip_session_register_supplement(&geolocation_supplement); + + return res; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "res_pjsip_geolocation Module for Asterisk", + .support_level = AST_MODULE_SUPPORT_CORE, + .load = load_module, + .unload = unload_module, + .reload = reload_module, + .load_pri = AST_MODPRI_CHANNEL_DEPEND - 1, + .requires = "res_geolocation,res_pjsip,res_pjsip_session,chan_pjsip", +);