]> git.ipfire.org Git - thirdparty/asterisk.git/commitdiff
Geolocation: chan_pjsip Capability Preview
authorGeorge Joseph <gjoseph@digium.com>
Thu, 7 Jul 2022 15:32:38 +0000 (09:32 -0600)
committerFriendly Automation <jenkins2@gerrit.asterisk.org>
Tue, 12 Jul 2022 18:35:55 +0000 (13:35 -0500)
This commit adds res_pjsip_geolocation which gives chan_pjsip
the ability to use the core geolocation capabilities.

This commit message is intentionally short because this isn't
a simple capability.  See the documentation at
https://wiki.asterisk.org/wiki/display/AST/Geolocation
for more information.

THE CAPABILITIES IMPLEMENTED HERE MAY CHANGE BASED ON
USER FEEDBACK!

ASTERISK-30128

Change-Id: Ie2e2bcd87243c2cfabc43eb823d4427c7086f4d9

configs/samples/pjsip.conf.sample
contrib/ast-db-manage/config/versions/7197536bb68d_geoloc_endpoint_params.py [new file with mode: 0644]
doc/CHANGES-staging/res_pjsip_geolocation.txt [new file with mode: 0644]
include/asterisk/res_pjsip.h
res/res_pjsip.c
res/res_pjsip/pjsip_config.xml
res/res_pjsip/pjsip_configuration.c
res/res_pjsip_geolocation.c [new file with mode: 0644]

index 9d73843030f2b27139d7b878137ecd1aebbc5b3b..a74b50fac59b6fd94d43e1cda6eb9a9ce025f0ee 100644 (file)
                            ; 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 (file)
index 0000000..5159ff4
--- /dev/null
@@ -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 (file)
index 0000000..acc4906
--- /dev/null
@@ -0,0 +1,4 @@
+Subject: res_pjsip_geolocation
+
+Added res_pjsip_geolocation which gives chan_pjsip
+the ability to use the core geolocation capabilities.
index b5b5a7256de229fdfe5371353f50ae8c5196eb19..b3717e722f232aca3647bbf8210721ed20cf02a8 100644 (file)
@@ -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
  *
index 78226450e4ef2bde009ecf324001fe31251aaa45..d722bbe0fd88e09726507aa2b0a6986fc582a4da 100644 (file)
@@ -53,6 +53,7 @@
        <depend>res_sorcery_memory</depend>
        <depend>res_sorcery_astdb</depend>
        <use type="module">res_statsd</use>
+       <use type="module">res_geolocation</use>
        <support_level>core</support_level>
  ***/
 
@@ -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",
 );
index e6fca2e86b0437866451812dfc72f2ebb6803688..63fd1a961fb03d1b1dd8956d773d3855c83fcf01 100644 (file)
                                                responses.</para>
                                        </description>
                                </configOption>
+                               <configOption name="geoloc_incoming_call_profile" default="">
+                                       <synopsis>Geolocation profile to apply to incoming calls</synopsis>
+                                       <description><para>
+                                               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.
+                                               </para>
+                                       </description>
+                               </configOption>
+                               <configOption name="geoloc_outgoing_call_profile" default="">
+                                       <synopsis>Geolocation profile to apply to outgoing calls</synopsis>
+                                       <description><para>
+                                               This geolocation profile will be applied to all calls received
+                                               by the channel driver from the dialplan before they're forwarded
+                                               the remote endpoint.
+                                               </para>
+                                       </description>
+                               </configOption>
                        </configObject>
                        <configObject name="auth">
                                <synopsis>Authentication type</synopsis>
index ace68d2fdc271b95aef7563ffee1f9a1d8f98664..5f1f61e397cd980ac1caa4ec98259c0256e4b189 100644 (file)
@@ -21,6 +21,7 @@
 #include <pjsip.h>
 #include <pjsip_ua.h>
 
+#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 (file)
index 0000000..c4ba003
--- /dev/null
@@ -0,0 +1,713 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2022, Sangoma Technologies Corporation
+ *
+ * George Joseph <gjoseph@sangoma.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*** MODULEINFO
+       <depend>res_geolocation</depend>
+       <depend>pjproject</depend>
+       <depend>res_pjsip</depend>
+       <depend>res_pjsip_session</depend>
+       <depend>chan_pjsip</depend>
+       <depend>libxml2</depend>
+       <support_level>core</support_level>
+ ***/
+
+#include "asterisk.h"
+#include "asterisk/module.h"
+#include "asterisk/xml.h"
+#include "asterisk/res_geolocation.h"
+
+#include <pjsip_ua.h>
+#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 <scheme:location>[;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, "<cid:")) {
+                       ast_trace(4, "Processing URI '%s'\n", geoloc_uri);
+
+                       eprofile = ast_geoloc_eprofile_create_from_uri(geoloc_uri, session_name);
+                       if (!eprofile) {
+                               ast_log(LOG_WARNING, "%s: Unable to create effective profile for URI '%s'.  Skipping\n",
+                                       session_name, geoloc_uri);
+                               continue;
+                       }
+               } else {
+                       ast_trace(4, "Processing PIDF-LO '%s'\n", geoloc_uri);
+
+                       rc = find_pidf(session_name, rdata, geoloc_uri, &pidf_body, &pidf_len);
+                       if (rc != 0 || !pidf_body || pidf_len == 0) {
+                               continue;
+                       }
+                       ast_trace(5, "Processing PIDF-LO "PJSTR_PRINTF_SPEC "\n", (int)pidf_len, pidf_body);
+
+                       incoming_doc = ast_xml_read_memory(pidf_body, pidf_len);
+                       if (!incoming_doc) {
+                               ast_log(LOG_WARNING, "%s: Unable to parse pidf document for URI '%s'\n",
+                                       session_name, geoloc_uri);
+                               continue;
+                       }
+
+                       eprofile = ast_geoloc_eprofile_create_from_pidf(incoming_doc, geoloc_uri, session_name);
+               }
+               eprofile->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",
+);