diff --git a/Makefile.rules b/Makefile.rules index 934e44a92d41e28b9ecd89b160427683dc069b67..e6b6589cc7c51e34e8e514bc2ae368ed3fe208a7 100644 --- a/Makefile.rules +++ b/Makefile.rules @@ -204,4 +204,19 @@ endif $(ECHO_PREFIX) echo " [LD] $^ -> $@" $(CMD_PREFIX) $(CXX) -o $@ $(PTHREAD_CFLAGS) $(_ASTLDFLAGS) $^ $(CXX_LIBS) $(ASTLDFLAGS) +# These CC commands just create an object file with the input file embedded in it. +# It can be access from code as follows: +# If your input file is named abc_def.xml... +# +# extern const uint8_t _binary_abc_def_xml_start[]; +# extern const uint8_t _binary_abc_def_xml_end[]; +# extern const size_t _binary_abc_def_xml_size; +%.o: %.xml + $(ECHO_PREFIX) echo " [LD] $^ -> $@" + $(CMD_PREFIX) $(CC) -g -nostartfiles -nodefaultlibs -nostdlib -r -Wl,-b,binary -o $@ $^ + +%.o: %.xslt + $(ECHO_PREFIX) echo " [LD] $^ -> $@" + $(CMD_PREFIX) $(CC) -g -nostartfiles -nodefaultlibs -nostdlib -r -Wl,-b,binary -o $@ $^ + dist-clean:: clean diff --git a/configs/samples/geolocation.conf.sample b/configs/samples/geolocation.conf.sample new file mode 100644 index 0000000000000000000000000000000000000000..0566cbd9837b30f3d88d844f05d4f856482c9f1f --- /dev/null +++ b/configs/samples/geolocation.conf.sample @@ -0,0 +1,264 @@ +;-- + Geolocation Profile Sample Configuration + +--; + +;-- +======================================================================= + Overview +======================================================================= + +Geolocation information is actually comprised of two objects, a +Location object, and a Profile object. + +Location objects must contain one of the following: + + - Location information specified in Geographic Markup Language + (GML) or civicAddress formats. + + - A URI that points to externally hosted location information. + +Profile objects contain instructions for the disposition of location +information, an optional reference to a Location object, and updates or +overrides to that Location object if specified. + +Channel drivers and the dialplan functions are responsible for +associating Profiles to endpoints/devices and calls. Normally, two +profiles would be assigned to an endpoint to control behavior in each +direction and to optionally specify location information. One for +incoming calls (Asterisk is the UAS) and and one for outgoing calls +(Asterisk is the UAC). + +NOTE: + +See https://wiki.asterisk.org/wiki/display/AST/Geolocation for the most +complete and up-to-date information on valid values for the object +parameters and a full list of references. + +GENERAL CAUTION: You must coordinate with your partners with regards +to what location information is expected by each party and how it should +be formatted. An outgoing configuration mismatch for instance, could +result in misinformation or no information being sent to an emergency +response center or even call failure for which you are solely responsible. +--; + + +;-- +======================================================================= + Location Object Description +======================================================================= +[<location_id>] + +-- type (required) ---------------------------------------------------- +Defines the object type. +type = location + +Must be "location" to identify this configuration section as a +Geolocation Location object. + +-- format (required) -------------------------------------------------- +Sets the format used to express the location. +format = < civicAddress | GML | URI > + +Values: +civicAddress: [RFC4119] [RFC5139] [RFC5491] + The location information will be placed in an XML document + conforming to the PIDF-LO standard. + For chan_pjsip, this will be placed in the body of + outgoing INVITE messages in addition to any SDP. + +GML: [RFC4119] [RFC5491] [GeoShape] + The location information will be placed in an XML document + conforming to the PIDF-LO standard. + For chan_pjsip, this will be placed in the body of + outgoing INVITE messages in addition to any SDP. + +URI: [RFC6442] + The external URI at which the the location information + can be found. For chan_pjsip, this URI will be placed + in a "Geolocation" header in outgoing INVITE messages. + +There is no default. + +Example: +format = civicAddress + +-- location_info (required) ------------------------------------------- +The location-format-specific information describing the location. +location_info = <location_format_specific_description> + +For readability, multiple "location" parameters can be specified and +they will be concatenated into one specification. The description may +contain replacement variables which may be the names of common channel +variables like ${EXTEN}, channel variables you may have added in the +dialplan, or variables you may have specified in the profile that +references this location object. + +NOTE: See https://wiki.asterisk.org/wiki/display/AST/Geolocation for the +most complete and up-to-date information on valid values for the object +parameters and a full list of references. + +WARNING: Asterisk can only validate that a particular sub-parameter +name is valid for a particular format. It can't validate the actual +value of the sub-parameter. + +Example for civicAddress: + +location_info = country=US +location_info = A1="New York", A3="New York", A4=Manhattan, +location_info = HNO=1633, PRD=W, RD=46th, STS=Street +location_info = PC=10222 + +Example for GML with replacement variables: + +location_info = type=Point, crs=2d, pos="${mylat} ${mylon}" + +Example for URI with replacement variables: +location_info = URI=https://some.company.com?number=${phone_number} + +-- method (optional) -------------------------------------------------- +The method used to determine the location_info +method = <"GPS" | "A-GPS" | "Manual" | "DHCP" + | "Triangulation" | "Cell" | "802.11"> + +Example: +method = Manual + +-- location_source (optional) ----------------------------------------- +Original source of the location-info. +location_source = < FQDN > + +The value MUST be a FQDN. IP addresses are specifically not +allowed. See RFC8787. + +Example: +location_source = sip1.myserver.net + +-- Location Example --------------------------------------------------- + +[mylocation] +type = location +format = civicAddress +location_info = country=US +location_info = A1="New York", A3="New York", A4=Manhattan +location_info = HNO=1633, PRD=W, RD=46th, STS=Street +location_info = PC=10222 +method = Manual +location_source = sip1.myserver.net + +======================================================================= +--; + + +;-- +======================================================================= + Profile Object Descriptions +======================================================================= +[<profile_id>] + +-- type (required) ---------------------------------------------------- +Defines the object type. +type = profile + +-- profile_action (optional) ------------------------------------------ +Sets how to reconcile incoming and configured profiles. +profile_action = < prefer_incoming | prefer_config | discard_incoming + | discard_config > + +On an incoming call leg, "incoming" is the location description +received in the SIP INVITE (if any) and "config" is this profile. + +On an outgoing call leg, "incoming" is the location description +passed through the dialplan to this channel (if any) and "config" +is this profile. + +Values: + +prefer_incoming: If there's an incoming location description, use it + even if there's also a configured one. +prefer_config: If there's a configured location description, use it + even if there's also an incoming one. +discard_incoming: Discard any incoming location description. If there's + a configured one, use it. If not, no location + information is propagated. +discard_config: Discard any configured location description. If + there's an incoming one, use it. If not, no location + information is propagated. + +discard_incoming is the default. + +Example: +profile_action = prefer_config + +-- pidf_element (optional) -------------------------------------------- +PIDF-LO element in which to place the location description. +pidf_element = < tuple | device | person > + +If the format is civicAddress or GML, this sets the PIDF element into +which the location information will be placed. + +Values: +tuple: Places the information in a "tuple" element. +device: Places the information in a "device" element. +person: Places the information in a "person" element. + +Per [RFC5491], "device" is preferred and therefore the default. + +Example: +pidf_element = tuple + +-- geolocation_routing (optional) ------------------------------------- +Sets whether the "Geolocation-Routing" header is added to outgoing +requests. +geolocation_routing = < yes | no > + +Set to "yes" to indicate that servers later in the path +can use the location information for routing purposes. Set to "no" +if they should not. If this value isn't specified, no +"Geolocation-Routing" header will be added. + +Example: +geolocation_routing = yes + +-- location_reference (optional) -------------------------------------- +The name of an existing Location object. +location_reference = <location_id> + +The location_info_refinement and location_variables parameters below can +be used to refine the location object for this specific profile. + +Example: +location_reference = "my_building" + +-- location_info_refinement (optional) -------------------------------- +Location info to add to that already retrieved from the location object. + +location_info_refinement = <location_format_specific_description> + +The information in the referenced Location object can be refined on a +per-profile basis. For example, if the referenced Location object has a +civicAddress for a building, you could set location_refinement to add a +floor and room just for this profile + +Example: +location_info_refinement = floor=20, room=20a2 + +-- location_variables ------------------------------------------------- + +If the referenced Location object uses any replacement variables, they +can be assigned here. There is no need to define variables that come +from the channel using this profile. They get assigned automatically. + +location_variables = myfloor=20, myroom=222 + +-- Profile Example ---------------------------------------------------- + +[myprofile] +type = profile +location_reference = mylocation +location_info_refinement = floor=20, room=20a2 +pidf_element = tuple +profile_action = discard_incoming + +======================================================================= +--; diff --git a/doc/CHANGES-staging/res_geolocation.txt b/doc/CHANGES-staging/res_geolocation.txt new file mode 100644 index 0000000000000000000000000000000000000000..5fe7316333d9b392823260d27a85cb59dabfd416 --- /dev/null +++ b/doc/CHANGES-staging/res_geolocation.txt @@ -0,0 +1,4 @@ +Subject: res_geolocation + +Added res_geolocation which creates the core capabilities +to manipulate Geolocation information on SIP INVITEs. diff --git a/include/asterisk/res_geolocation.h b/include/asterisk/res_geolocation.h new file mode 100644 index 0000000000000000000000000000000000000000..403e6c869555f6f5e579d7db1226eecd1b0cd810 --- /dev/null +++ b/include/asterisk/res_geolocation.h @@ -0,0 +1,353 @@ + /* + * 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. + */ + +#ifndef INCLUDE_ASTERISK_RES_GEOLOCATION_H_ +#define INCLUDE_ASTERISK_RES_GEOLOCATION_H_ + +#include "asterisk/channel.h" +#include "asterisk/config.h" +#include "asterisk/sorcery.h" +#include "asterisk/xml.h" +#include "asterisk/optional_api.h" + +#define AST_GEOLOC_INVALID_VALUE -1 + +enum ast_geoloc_pidf_element { + AST_PIDF_ELEMENT_NONE = 0, + AST_PIDF_ELEMENT_TUPLE, + AST_PIDF_ELEMENT_DEVICE, + AST_PIDF_ELEMENT_PERSON, + AST_PIDF_ELEMENT_LAST, +}; + +enum ast_geoloc_format { + AST_GEOLOC_FORMAT_NONE = 0, + AST_GEOLOC_FORMAT_CIVIC_ADDRESS, + AST_GEOLOC_FORMAT_GML, + AST_GEOLOC_FORMAT_URI, + AST_GEOLOC_FORMAT_LAST, +}; + +enum ast_geoloc_action { + AST_GEOLOC_ACT_PREFER_INCOMING = 0, + AST_GEOLOC_ACT_PREFER_CONFIG, + AST_GEOLOC_ACT_DISCARD_INCOMING, + AST_GEOLOC_ACT_DISCARD_CONFIG, +}; + +struct ast_geoloc_location { + SORCERY_OBJECT(details); + AST_DECLARE_STRING_FIELDS( + AST_STRING_FIELD(method); + AST_STRING_FIELD(location_source); + ); + enum ast_geoloc_format format; + struct ast_variable *location_info; +}; + +struct ast_geoloc_profile { + SORCERY_OBJECT(details); + AST_DECLARE_STRING_FIELDS( + AST_STRING_FIELD(location_reference); + AST_STRING_FIELD(notes); + ); + enum ast_geoloc_pidf_element pidf_element; + enum ast_geoloc_action action; + int geolocation_routing; + struct ast_variable *location_refinement; + struct ast_variable *location_variables; + struct ast_variable *usage_rules; +}; + +struct ast_geoloc_eprofile { + AST_DECLARE_STRING_FIELDS( + AST_STRING_FIELD(id); + AST_STRING_FIELD(location_reference); + AST_STRING_FIELD(location_source); + AST_STRING_FIELD(method); + AST_STRING_FIELD(notes); + ); + enum ast_geoloc_pidf_element pidf_element; + enum ast_geoloc_action action; + int geolocation_routing; + enum ast_geoloc_format format; + struct ast_variable *location_info; + struct ast_variable *location_refinement; + struct ast_variable *location_variables; + struct ast_variable *effective_location; + struct ast_variable *usage_rules; +}; + +/*! + * \brief Check if res_geolocation is available + * + * \return 1 if available, 0 otherwise. + */ +AST_OPTIONAL_API(int, ast_geoloc_is_loaded, (void), { return 0; }); + +/*! + * \brief Retrieve a geolocation location object by id. + * + * \param id Location object id. + * + * \return Location object or NULL if not found. + */ +AST_OPTIONAL_API(struct ast_geoloc_location *, ast_geoloc_get_location, + (const char *id), + { return NULL; }); + +/*! + * \brief Retrieve a geolocation profile by id. + * + * \param id profile id. + * + * \return Profile or NULL if not found. + */ +AST_OPTIONAL_API(struct ast_geoloc_profile *, ast_geoloc_get_profile, + (const char *id), + { return NULL; }); + +/*! + * \brief Given a civicAddress code, check whether it's valid. + * + * \param code Pointer to the code to check + * + * \return 1 if valid, 0 otherwise. + */ +int ast_geoloc_civicaddr_is_code_valid(const char *code); + +enum ast_geoloc_validate_result { + AST_GEOLOC_VALIDATE_INVALID_VALUE = -1, + AST_GEOLOC_VALIDATE_SUCCESS = 0, + AST_GEOLOC_VALIDATE_MISSING_SHAPE, + AST_GEOLOC_VALIDATE_INVALID_SHAPE, + AST_GEOLOC_VALIDATE_INVALID_VARNAME, + AST_GEOLOC_VALIDATE_NOT_ENOUGH_VARNAMES, + AST_GEOLOC_VALIDATE_TOO_MANY_VARNAMES, +}; + +const char *ast_geoloc_validate_result_to_str(enum ast_geoloc_validate_result result); + +/*! + * \brief Validate that the names of the variables in the list are valid codes or synonyms + * + * \param varlist Variable list to check. + * \param result[OUT] Pointer to char * to receive failing item. + * + * \return result code. + */ +enum ast_geoloc_validate_result ast_geoloc_civicaddr_validate_varlist( + const struct ast_variable *varlist, const char **result); + +/*! + * \brief Validate that the variables in the list represent a valid GML shape + * + * \param varlist Variable list to check. + * \param result[OUT] Pointer to char * to receive failing item. + * + * \return result code. + */ +enum ast_geoloc_validate_result ast_geoloc_gml_validate_varlist(const struct ast_variable *varlist, + const char **result); + + +/*! + * \brief Geolocation datastore Functions + * @{ + */ + +/*! + * \brief Create a geoloc datastore from a profile name + * + * \param profile_name The name of the profile to use. + * + * \return The datastore. + */ +struct ast_datastore *ast_geoloc_datastore_create_from_profile_name(const char *profile_name); + +/*! + * \brief Create a geoloc datastore from an effective profile. + * + * \param eprofile The effective profile to use. + * + * \return The datastore. + */ +struct ast_datastore *ast_geoloc_datastore_create_from_eprofile( + struct ast_geoloc_eprofile *eprofile); + +/*! + * \brief Create an empty geoloc datastore. + * + * \param id An id to use for the datastore. + * + * \return The datastore. + */ +struct ast_datastore *ast_geoloc_datastore_create(const char *id); + +/*! + * \brief Retrieve a geoloc datastore's id. + * + * \param ds The datastore + * + * \return The datastore's id. + */ +const char *ast_geoloc_datastore_get_id(struct ast_datastore *ds); + +/*! + * \brief Add an eprofile to a datastore + * + * \param ds The datastore + * \param eprofile The eprofile to add. + * + * \return The new number of eprofiles or -1 to indicate a failure. + */ +int ast_geoloc_datastore_add_eprofile(struct ast_datastore *ds, + struct ast_geoloc_eprofile *eprofile); + +/*! + * \brief Insert an eprofile to a datastore at the specified position + * + * \param ds The datastore + * \param eprofile The eprofile to add. + * \param index The position to insert at. Existing eprofiles will + * be moved up to make room. + * + * \return The new number of eprofiles or -1 to indicate a failure. + */ +int ast_geoloc_datastore_insert_eprofile(struct ast_datastore *ds, + struct ast_geoloc_eprofile *eprofile, int index); + +/*! + * \brief Retrieves the number of eprofiles in the datastore + * + * \param ds The datastore + * + * \return The number of eprofiles. + */ +int ast_geoloc_datastore_size(struct ast_datastore *ds); + +/*! + * \brief Sets the inheritance flag on the datastore + * + * \param ds The datastore + * \param inherit 1 to allow the datastore to be inherited by other channels + * 0 to prevent the datastore to be inherited by other channels + * + * \return 0 if successful, -1 otherwise. + */ +int ast_geoloc_datastore_set_inheritance(struct ast_datastore *ds, int inherit); + +/*! + * \brief Retrieve a specific eprofile from a datastore by index + * + * \param ds The datastore + * \param ix The index + * + * \return The effective profile ao2 object with its reference count bumped. + */ +struct ast_geoloc_eprofile *ast_geoloc_datastore_get_eprofile(struct ast_datastore *ds, int ix); + +/*! + * \brief Delete a specific eprofile from a datastore by index + * + * \param ds The datastore + * \param ix The index + * + * \return 0 if succesful, -1 otherwise. + */ +int ast_geoloc_datastore_delete_eprofile(struct ast_datastore *ds, int ix); + +/*! + * \brief Retrieves the geoloc datastore from a channel, if any + * + * \param chan Channel + * + * \return datastore if found, NULL otherwise. + */ +struct ast_datastore *ast_geoloc_datastore_find(struct ast_channel *chan); + +/*! + * @} + */ + +/*! + * \brief Geolocation Effective Profile Functions + * @{ + */ + +/*! + * \brief Allocate a new, empty effective profile. + * + * \param name The profile's name + * + * \return The effective profile ao2 object. + */ +struct ast_geoloc_eprofile *ast_geoloc_eprofile_alloc(const char *name); + +/*! + * \brief Allocate a new effective profile from an existing profile. + * + * \param profile The profile to use. + * + * \return The effective profile ao2 object. + */ +struct ast_geoloc_eprofile *ast_geoloc_eprofile_create_from_profile(struct ast_geoloc_profile *profile); + +/*! + * \brief Allocate a new effective profile from an XML PIDF-LO document + * + * \param pidf_xmldoc The ast_xml_doc to use. + * \param geoloc_uri The URI that referenced this document. + * \param reference_string An identifying string to use in error messages. + * + * \return The effective profile ao2 object. + */ +struct ast_geoloc_eprofile *ast_geoloc_eprofile_create_from_pidf( + struct ast_xml_doc *pidf_xmldoc, const char *geoloc_uri, const char *reference_string); + +/*! + * \brief Allocate a new effective profile from a URI. + * + * \param uri The URI to use. + * \param reference_string An identifying string to use in error messages. + * + * \return The effective profile ao2 object. + */ +struct ast_geoloc_eprofile *ast_geoloc_eprofile_create_from_uri(const char *uri, + const char *reference_string); + +const char *ast_geoloc_eprofile_to_uri(struct ast_geoloc_eprofile *eprofile, + struct ast_channel *chan, struct ast_str **buf, const char *ref_string); + +const char *ast_geoloc_eprofiles_to_pidf(struct ast_datastore *ds, + struct ast_channel *chan, struct ast_str **buf, const char * ref_string); + +/*! + * \brief Refresh the effective profile with any changed info. + * + * \param eprofile The eprofile to refresh. + * + * \return 0 on success, any other value on error. + */ +int ast_geoloc_eprofile_refresh_location(struct ast_geoloc_eprofile *eprofile); + +/*! + * @} + */ + +#endif /* INCLUDE_ASTERISK_RES_GEOLOCATION_H_ */ diff --git a/res/Makefile b/res/Makefile index d54130011faf702a422dc6b893b83b161ab87be6..0987f43a437191aa755a5e50235ef922312484cc 100644 --- a/res/Makefile +++ b/res/Makefile @@ -70,6 +70,10 @@ $(call MOD_ADD_C,res_ari_model,ari/ari_model_validators.c) $(call MOD_ADD_C,res_stasis_recording,stasis_recording/stored.c) $(call MOD_ADD_C,res_stir_shaken,$(wildcard res_stir_shaken/*.c)) $(call MOD_ADD_C,res_aeap,$(wildcard res_aeap/*.c)) +$(call MOD_ADD_C,res_geolocation,$(wildcard res_geolocation/*.c)) + +# These are the xml and xslt files to be embedded +res_geolocation.so: res_geolocation/pidf_lo_test.o res_geolocation/pidf_to_eprofile.o res_geolocation/eprofile_to_pidf.o res_parking.o: _ASTCFLAGS+=$(AST_NO_FORMAT_TRUNCATION) snmp/agent.o: _ASTCFLAGS+=-fPIC @@ -77,3 +81,4 @@ res_snmp.o: _ASTCFLAGS+=-fPIC # Dependencies for res_ari_*.so are generated, so they're in this file include ari.make + diff --git a/res/res_geolocation.c b/res/res_geolocation.c new file mode 100644 index 0000000000000000000000000000000000000000..19dd84b0d5f18d633a056d8eeea47486d2f4d0dd --- /dev/null +++ b/res/res_geolocation.c @@ -0,0 +1,125 @@ +/* + * 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>libxml2</depend> + <depend>libxslt</depend> + <support_level>core</support_level> + ***/ + + +#include "asterisk.h" +#define AST_API_MODULE +#include "asterisk/res_geolocation.h" +#include "res_geolocation/geoloc_private.h" + +static int reload_module(void) +{ + int res = 0; + + res = geoloc_civicaddr_reload(); + if (res) { + return AST_MODULE_LOAD_DECLINE; + } + res = geoloc_gml_reload(); + if (res) { + return AST_MODULE_LOAD_DECLINE; + } + res = geoloc_config_reload(); + if (res) { + return AST_MODULE_LOAD_DECLINE; + } + res = geoloc_eprofile_reload(); + if (res) { + return AST_MODULE_LOAD_DECLINE; + } + res = geoloc_dialplan_reload(); + if (res) { + return AST_MODULE_LOAD_DECLINE; + } + res = geoloc_channel_reload(); + if (res) { + return AST_MODULE_LOAD_DECLINE; + } + + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + int res = 0; + + res += geoloc_channel_unload(); + res += geoloc_dialplan_unload(); + res += geoloc_eprofile_unload(); + res += geoloc_config_unload(); + res += geoloc_gml_unload(); + res += geoloc_civicaddr_unload(); + + return (res != 0); +} + +static int load_module(void) +{ + int res = 0; + + res = geoloc_civicaddr_load(); + if (res) { + unload_module(); + return AST_MODULE_LOAD_DECLINE; + } + + res = geoloc_gml_load(); + if (res) { + return AST_MODULE_LOAD_DECLINE; + } + + res = geoloc_config_load(); + if (res) { + unload_module(); + return AST_MODULE_LOAD_DECLINE; + } + + res = geoloc_eprofile_load(); + if (res) { + unload_module(); + return AST_MODULE_LOAD_DECLINE; + } + + res = geoloc_dialplan_load(); + if (res) { + unload_module(); + return AST_MODULE_LOAD_DECLINE; + } + + res = geoloc_channel_load(); + if (res) { + unload_module(); + return AST_MODULE_LOAD_DECLINE; + } + + return AST_MODULE_LOAD_SUCCESS; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "res_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 - 10, +); diff --git a/res/res_geolocation.exports.in b/res/res_geolocation.exports.in new file mode 100644 index 0000000000000000000000000000000000000000..da0a9810aaa81f34cbc2d1766d982f87415c04bb --- /dev/null +++ b/res/res_geolocation.exports.in @@ -0,0 +1,6 @@ +{ + global: + LINKER_SYMBOL_PREFIXast_geo*; + local: + *; +}; diff --git a/res/res_geolocation/eprofile_to_pidf.xslt b/res/res_geolocation/eprofile_to_pidf.xslt new file mode 100644 index 0000000000000000000000000000000000000000..dbfe17b021e18e2ad22d1c809ccc9023055f9a97 --- /dev/null +++ b/res/res_geolocation/eprofile_to_pidf.xslt @@ -0,0 +1,237 @@ +<?xml version="1.0"?> +<xsl:stylesheet version="1.1" + xmlns:ca="urn:ietf:params:xml:ns:pidf:geopriv10:civicAddr" + xmlns:dm="urn:ietf:params:xml:ns:pidf:data-model" + xmlns:fn="http://www.w3.org/2005/xpath-functions" + xmlns:gbp="urn:ietf:params:xml:ns:pidf:geopriv10:basicPolicy" + xmlns:gml="http://www.opengis.net/gml" + xmlns:gp="urn:ietf:params:xml:ns:pidf:geopriv10" + xmlns:gs="http://www.opengis.net/pidflo/1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:date="http://exslt.org/dates-and-times"> + + <xsl:output method="xml" indent="yes"/> + <xsl:strip-space elements="*"/> + + <!-- REMINDER: The "match" and "select" xpaths refer to the input document, + not the output document --> + + <xsl:template match="presence"> + <!-- xslt will take care of adding all of the namespace declarations + from the list above --> + <presence xmlns="urn:ietf:params:xml:ns:pidf" entity="{@entity}"> + <xsl:apply-templates select="./device|tuple|person"/> + </presence> + </xsl:template> + + <xsl:template match="device"> + <dm:device> + <gp:geopriv> + <xsl:apply-templates select="./location-info"/> + <xsl:apply-templates select="./usage-rules"/> + <xsl:apply-templates select="./method"/> + <xsl:apply-templates select="./note-well"/> + </gp:geopriv> + <xsl:if test="./timestamp"> + <dm:timestamp> + <xsl:value-of select="./timestamp"/> + </dm:timestamp> + </xsl:if> + <xsl:if test="./deviceID"> + <dm:deviceID> + <xsl:value-of select="./deviceID"/> + </dm:deviceID> + </xsl:if> + </dm:device> + </xsl:template> + + <xsl:template match="tuple"> + <xsl:element name="tuple" namespace="urn:ietf:params:xml:ns:pidf"> + <xsl:element name="status" namespace="urn:ietf:params:xml:ns:pidf"> + <gp:geopriv> + <xsl:apply-templates select="./location-info"/> + <xsl:apply-templates select="./usage-rules"/> + <xsl:apply-templates select="./method"/> + <xsl:apply-templates select="./note-well"/> + </gp:geopriv> + </xsl:element> + <xsl:if test="./timestamp"> + <xsl:element name="timestamp" namespace="urn:ietf:params:xml:ns:pidf"> + <xsl:value-of select="./timestamp"/> + </xsl:element> + </xsl:if> + </xsl:element> + </xsl:template> + + <xsl:template match="person"> + <dm:person> + <gp:geopriv> + <xsl:apply-templates select="./location-info"/> + <xsl:apply-templates select="./usage-rules"/> + <xsl:apply-templates select="./method"/> + <xsl:apply-templates select="./note-well"/> + </gp:geopriv> + <xsl:if test="./timestamp"> + <dm:timestamp> + <xsl:value-of select="./timestamp"/> + </dm:timestamp> + </xsl:if> + </dm:person> + </xsl:template> + + <xsl:template match="location-info"> + <gp:location-info> + <xsl:apply-templates/> + </gp:location-info> + </xsl:template> + + <!-- When we're using the civicAddress format, the translation is simple. + We add gp:location-info and ca:civicAddress, then we just copy in + each element, adding the "ca" namespace --> + + <xsl:template match="civicAddress/*"> + <xsl:element name="ca:{name()}"> + <xsl:value-of select="."/> + </xsl:element> + </xsl:template> + + <xsl:template match="location-info/civicAddress"> + <ca:civicAddress xml:lang="{@lang}"> + <xsl:apply-templates/> + </ca:civicAddress> + </xsl:template> + + <!-- All GML shapes share common processing for the "srsName" attribute --> + <xsl:template name="shape"> + <xsl:choose> + <xsl:when test="@crs = '3d'"> + <xsl:attribute name="srsName">urn:ogc:def:crs:EPSG::4979</xsl:attribute> + </xsl:when> + <xsl:otherwise> + <xsl:attribute name="srsName">urn:ogc:def:crs:EPSG::4326</xsl:attribute> + </xsl:otherwise> + </xsl:choose> + </xsl:template> + + <!-- The GML shapes themselves. They don't all have the same namespace unfortunately... --> + + <xsl:template match="Point|Circle|Ellipse|ArcBand|Sphere|Ellipsoid"> + <xsl:variable name="namespace"> + <xsl:choose> + <xsl:when test="name() = 'Point'"> + <xsl:value-of select="'gml'"/> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="'gs'"/> + </xsl:otherwise> + </xsl:choose> + </xsl:variable> + + <xsl:element name="{$namespace}:{name()}"> + <xsl:call-template name="shape"/> + <xsl:apply-templates select="./*"/> + </xsl:element> + </xsl:template> + + <!-- ... and some are more complex than others. --> + + <xsl:template match="Polygon"> + <gml:Polygon> + <xsl:call-template name="shape"/> + <gml:exterior> + <gml:LinearRing> + <xsl:apply-templates select="./pos|posList"/> + </gml:LinearRing> + </gml:exterior> + </gml:Polygon> + </xsl:template> + + <!-- Prism with a Polygon and height --> + <xsl:template match="Prism"> + <gs:Prism> + <xsl:call-template name="shape"/> + <gs:base> + <gml:Polygon> + <gml:exterior> + <gml:LinearRing> + <xsl:apply-templates select="./pos|posList"/> + </gml:LinearRing> + </gml:exterior> + </gml:Polygon> + </gs:base> + <xsl:apply-templates select="./height"/> + </gs:Prism> + </xsl:template> + + <!-- method has no children so we add the "gp" namespace and copy in the value --> + <xsl:template match="method"> + <gp:method> + <xsl:value-of select="."/> + </gp:method> + </xsl:template> + + <!-- note-well has no children so we add the "gp" namespace and copy in the value --> + <xsl:template match="note-well"> + <gp:note-well> + <xsl:value-of select="."/> + </gp:note-well> + </xsl:template> + + <!-- usage-rules does have children so we add the "gp" namespace and copy in + the children, also adding the "gp" namespace --> + <xsl:template match="usage-rules"> + <gp:usage-rules> + <xsl:for-each select="*"> + <xsl:element name="gp:{local-name()}"> + <xsl:value-of select="."/> + </xsl:element> + </xsl:for-each> + </gp:usage-rules> + </xsl:template> + + <!-- These are the GML format primitives --> + + <xsl:template name="name-value"> + <xsl:element name="gml:{name()}"> + <xsl:value-of select="."/> + </xsl:element> + </xsl:template> + + <xsl:template name="length"> + <xsl:element name="gs:{name()}"> + <xsl:attribute name="uom">urn:ogc:def:uom:EPSG::9001</xsl:attribute> + <xsl:value-of select="."/> + </xsl:element> + </xsl:template> + + <xsl:template name="angle"> + <xsl:element name="gs:{name()}"> + <xsl:choose> + <xsl:when test="@uom = 'radians'"> + <xsl:attribute name="uom">urn:ogc:def:uom:EPSG::9102</xsl:attribute> + </xsl:when> + <xsl:otherwise> + <xsl:attribute name="uom">urn:ogc:def:uom:EPSG::9101</xsl:attribute> + </xsl:otherwise> + </xsl:choose> + <xsl:value-of select="."/> + </xsl:element> + </xsl:template> + + <!-- These are the GML shape parameters --> + + <xsl:template match="orientation"><xsl:call-template name="angle" /></xsl:template> + <xsl:template match="radius"><xsl:call-template name="length" /></xsl:template> + <xsl:template match="height"><xsl:call-template name="length" /></xsl:template> + <xsl:template match="semiMajorAxis"><xsl:call-template name="length" /></xsl:template> + <xsl:template match="semiMinorAxis"><xsl:call-template name="length" /></xsl:template> + <xsl:template match="verticalAxis"><xsl:call-template name="length" /></xsl:template> + <xsl:template match="innerRadius"><xsl:call-template name="length" /></xsl:template> + <xsl:template match="outerRadius"><xsl:call-template name="length" /></xsl:template> + <xsl:template match="startAngle"><xsl:call-template name="angle" /></xsl:template> + <xsl:template match="openingAngle"><xsl:call-template name="angle" /></xsl:template> + <xsl:template match="pos"><xsl:call-template name="name-value" /></xsl:template> + <xsl:template match="posList"><xsl:call-template name="name-value" /></xsl:template> + + +</xsl:stylesheet> diff --git a/res/res_geolocation/geoloc_civicaddr.c b/res/res_geolocation/geoloc_civicaddr.c new file mode 100644 index 0000000000000000000000000000000000000000..f5a7c22c5d7dcaf5e1246ae2301134faa33d01ed --- /dev/null +++ b/res/res_geolocation/geoloc_civicaddr.c @@ -0,0 +1,151 @@ +/* + * 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. + */ + +#include "asterisk.h" +#include "asterisk/config.h" +#include "asterisk/cli.h" +#include "asterisk/res_geolocation.h" +#include "asterisk/xml.h" +#include "geoloc_private.h" + +static const char *addr_code_name_entries[] = { + "country", + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "ADDCODE", + "BLD", + "FLR", + "HNO", + "HNS", + "LMK", + "LOC", + "NAM", + "PC", + "PCN", + "PLC", + "POBOX", + "POD", + "POM", + "PRD", + "PRM", + "RD", + "RD", + "RDBR", + "RDSEC", + "RDSUBBR", + "ROOM", + "SEAT", + "STS", + "UNIT", +}; + +static int compare_civicaddr_codes(const void *_a, const void *_b) +{ + /* See the man page for qsort(3) for an explanation of the casts */ + int rc = strcmp(*(const char **)_a, *(const char **)_b); + return rc; +} + +int ast_geoloc_civicaddr_is_code_valid(const char *code) +{ + const char **entry = bsearch(&code, addr_code_name_entries, ARRAY_LEN(addr_code_name_entries), + sizeof(const char *), compare_civicaddr_codes); + return (entry != NULL); +} + +enum ast_geoloc_validate_result ast_geoloc_civicaddr_validate_varlist( + const struct ast_variable *varlist, const char **result) +{ + const struct ast_variable *var = varlist; + for (; var; var = var->next) { + int valid = ast_geoloc_civicaddr_is_code_valid(var->name); + if (!valid) { + *result = var->name; + return AST_GEOLOC_VALIDATE_INVALID_VARNAME; + } + } + return AST_GEOLOC_VALIDATE_SUCCESS; +} + +struct ast_xml_node *geoloc_civicaddr_list_to_xml(const struct ast_variable *resolved_location, + const char *ref_string) +{ + char *lang = NULL; + char *s = NULL; + struct ast_variable *var; + struct ast_xml_node *ca_node; + struct ast_xml_node *child_node; + int rc = 0; + SCOPE_ENTER(3, "%s", ref_string); + + lang = (char *)ast_variable_find_in_list(resolved_location, "lang"); + if (ast_strlen_zero(lang)) { + lang = ast_strdupa(ast_defaultlanguage); + for (s = lang; *s; s++) { + if (*s == '_') { + *s = '-'; + } + } + } + + ca_node = ast_xml_new_node("civicAddress"); + if (!ca_node) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'civicAddress' XML node\n", ref_string); + } + rc = ast_xml_set_attribute(ca_node, "lang", lang); + if (rc != 0) { + ast_xml_free_node(ca_node); + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'lang' XML attribute\n", ref_string); + } + + for (var = (struct ast_variable *)resolved_location; var; var = var->next) { + if (ast_strings_equal(var->name, "lang")) { + continue; + } + child_node = ast_xml_new_child(ca_node, var->name); + if (!child_node) { + ast_xml_free_node(ca_node); + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create '%s' XML node\n", var->name, ref_string); + } + ast_xml_set_text(child_node, var->value); + } + + SCOPE_EXIT_RTN_VALUE(ca_node, "%s: Done\n", ref_string); +} + +int geoloc_civicaddr_unload(void) +{ + return AST_MODULE_LOAD_SUCCESS; +} + +int geoloc_civicaddr_load(void) +{ + qsort(addr_code_name_entries, ARRAY_LEN(addr_code_name_entries), sizeof(const char *), + compare_civicaddr_codes); + + return AST_MODULE_LOAD_SUCCESS; +} + +int geoloc_civicaddr_reload(void) +{ + return AST_MODULE_LOAD_SUCCESS; +} diff --git a/res/res_geolocation/geoloc_common.c b/res/res_geolocation/geoloc_common.c new file mode 100644 index 0000000000000000000000000000000000000000..bb24a3100ad27b87e0e8c4b0449eea8fe9b9caed --- /dev/null +++ b/res/res_geolocation/geoloc_common.c @@ -0,0 +1,36 @@ +/* + * 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. + */ + +#include "asterisk.h" +#include "geoloc_private.h" + +static const char *result_names[] = { + "Success", + "Missing type", + "Invalid shape type", + "Invalid variable name", + "Not enough variables", + "Too many variables", + "Invalid variable value" +}; + +const char *ast_geoloc_validate_result_to_str(enum ast_geoloc_validate_result result) +{ + return result_names[result]; +} + diff --git a/res/res_geolocation/geoloc_config.c b/res/res_geolocation/geoloc_config.c new file mode 100644 index 0000000000000000000000000000000000000000..33cd33363c80fdfe78b3557cb6949a1ec827ed41 --- /dev/null +++ b/res/res_geolocation/geoloc_config.c @@ -0,0 +1,641 @@ +/* + * 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. + */ + +#include "asterisk.h" +#include "asterisk/module.h" +#include "asterisk/cli.h" +#define AST_API_MODULE +#include "geoloc_private.h" + +static struct ast_sorcery *geoloc_sorcery; + +static const char *pidf_element_names[] = { + "<none>", + "tuple", + "device", + "person" +}; + +static const char *format_names[] = { + "<none>", + "civicAddress", + "GML", + "URI", +}; + +static const char * action_names[] = { + "prefer_incoming", + "prefer_config", + "discard_incoming", + "discard_config", +}; + +CONFIG_ENUM(location, format) +CONFIG_VAR_LIST(location, location_info) + +static void geoloc_location_destructor(void *obj) { + struct ast_geoloc_location *location = obj; + + ast_string_field_free_memory(location); + ast_variables_destroy(location->location_info); +} + +static void *geoloc_location_alloc(const char *name) +{ + struct ast_geoloc_location *location = ast_sorcery_generic_alloc(sizeof(struct ast_geoloc_location), geoloc_location_destructor); + if (location) { + ast_string_field_init(location, 128); + } + + return location; +} + + +CONFIG_ENUM(profile, pidf_element) +CONFIG_ENUM(profile, action) +CONFIG_VAR_LIST(profile, location_refinement) +CONFIG_VAR_LIST(profile, location_variables) +CONFIG_VAR_LIST(profile, usage_rules) + +static void geoloc_profile_destructor(void *obj) { + struct ast_geoloc_profile *profile = obj; + + ast_string_field_free_memory(profile); + ast_variables_destroy(profile->location_refinement); + ast_variables_destroy(profile->location_variables); + ast_variables_destroy(profile->usage_rules); +} + +static void *geoloc_profile_alloc(const char *name) +{ + struct ast_geoloc_profile *profile = ast_sorcery_generic_alloc(sizeof(*profile), geoloc_profile_destructor); + if (profile) { + ast_string_field_init(profile, 128); + } + + return profile; +} + +static int geoloc_location_apply_handler(const struct ast_sorcery *sorcery, void *obj) +{ + struct ast_geoloc_location *location = obj; + const char *location_id = ast_sorcery_object_get_id(location); + const char *failed; + const char *uri; + enum ast_geoloc_validate_result result; + + switch (location->format) { + case AST_GEOLOC_FORMAT_NONE: + case AST_GEOLOC_FORMAT_LAST: + ast_log(LOG_ERROR, "Location '%s' must have a format\n", location_id); + return -1; + case AST_GEOLOC_FORMAT_CIVIC_ADDRESS: + result = ast_geoloc_civicaddr_validate_varlist(location->location_info, &failed); + if (result != AST_GEOLOC_VALIDATE_SUCCESS) { + ast_log(LOG_ERROR, "Location '%s' has invalid item '%s' in the location\n", + location_id, failed); + return -1; + } + break; + case AST_GEOLOC_FORMAT_GML: + result = ast_geoloc_gml_validate_varlist(location->location_info, &failed); + if (result != AST_GEOLOC_VALIDATE_SUCCESS) { + ast_log(LOG_ERROR, "%s for item '%s' in location '%s'\n", + ast_geoloc_validate_result_to_str(result), failed, location_id); + return -1; + } + + break; + case AST_GEOLOC_FORMAT_URI: + uri = ast_variable_find_in_list(location->location_info, "URI"); + if (!uri) { + struct ast_str *str = ast_variable_list_join(location->location_info, ",", "=", "\"", NULL); + + ast_log(LOG_ERROR, "Geolocation location '%s' format is set to '%s' but no 'URI' was found in location parameter '%s'\n", + location_id, format_names[AST_GEOLOC_FORMAT_URI], ast_str_buffer(str)); + ast_free(str); + return -1; + } + break; + } + + if (!ast_strlen_zero(location->location_source)) { + struct ast_sockaddr loc_source_addr; + int rc = ast_sockaddr_parse(&loc_source_addr, location->location_source, PARSE_PORT_FORBID); + if (rc == 1) { + ast_log(LOG_ERROR, "Geolocation location '%s' location_source '%s' must be a FQDN." + " RFC8787 expressly forbids IP addresses.\n", + location_id, location->location_source); + return -1; + } + } + + + return 0; +} + +static int geoloc_profile_apply_handler(const struct ast_sorcery *sorcery, void *obj) +{ + struct ast_geoloc_profile *profile = obj; + struct ast_geoloc_location *location; + const char *profile_id = ast_sorcery_object_get_id(profile); + const char *failed; + enum ast_geoloc_validate_result result; + + if (ast_strlen_zero(profile->location_reference)) { + if (profile->location_refinement || + profile->location_variables) { + ast_log(LOG_ERROR, "Profile '%s' can't have location_refinement or location_variables without a location_reference", + profile_id); + return -1; + } + return 0; + } + + location = ast_sorcery_retrieve_by_id(geoloc_sorcery, "location", profile->location_reference); + if (!location) { + ast_log(LOG_ERROR, "Profile '%s' has a location_reference '%s' that doesn't exist", + profile_id, profile->location_reference); + return -1; + } + + if (profile->location_refinement) { + switch (location->format) { + case AST_GEOLOC_FORMAT_NONE: + case AST_GEOLOC_FORMAT_LAST: + break; + case AST_GEOLOC_FORMAT_CIVIC_ADDRESS: + result = ast_geoloc_civicaddr_validate_varlist(profile->location_refinement, &failed); + if (result != AST_GEOLOC_VALIDATE_SUCCESS) { + ast_log(LOG_ERROR, "Profile '%s' error: %s: for item '%s' in the location_refinement\n", + profile_id, ast_geoloc_validate_result_to_str(result), failed); + ao2_ref(location, -1); + return -1; + } + break; + case AST_GEOLOC_FORMAT_GML: + break; + case AST_GEOLOC_FORMAT_URI: + break; + } + } + ao2_ref(location, -1); + + return 0; +} + +struct ast_sorcery *geoloc_get_sorcery(void) +{ + ast_sorcery_ref(geoloc_sorcery); + return geoloc_sorcery; +} + +static char *geoloc_config_list_locations(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + struct ao2_iterator iter; + struct ao2_container *sorted_container; + struct ao2_container *unsorted_container; + struct ast_geoloc_location *loc; + int using_regex = 0; + char *result = CLI_SUCCESS; + int ret = 0; + char *format_name; + int count = 0; + + switch (cmd) { + case CLI_INIT: + e->command = "geoloc list locations"; + e->usage = "Usage: geoloc list locations [ like <pattern> ]\n" + " List Geolocation Location Objects\n"; + return NULL; + case CLI_GENERATE: + return NULL; + } + + if (a->argc != 3 && a->argc != 5) { + return CLI_SHOWUSAGE; + } + + if (a->argc == 5) { + if (strcasecmp(a->argv[3], "like")) { + return CLI_SHOWUSAGE; + } + using_regex = 1; + } + + sorted_container = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK, 0, + ast_sorcery_object_id_sort, NULL); + if (!sorted_container) { + ast_cli(a->fd, "Geolocation Location Objects: Unable to allocate temporary container\n"); + return CLI_FAILURE; + } + + /* Get a sorted snapshot of the scheduled tasks */ + if (using_regex) { + unsorted_container = ast_sorcery_retrieve_by_regex(geoloc_sorcery, "location", a->argv[4]); + } else { + unsorted_container = ast_sorcery_retrieve_by_fields(geoloc_sorcery, "location", + AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL); + } + + ret = ao2_container_dup(sorted_container, unsorted_container, 0); + ao2_ref(unsorted_container, -1); + if (ret != 0) { + ao2_ref(sorted_container, -1); + ast_cli(a->fd, "Geolocation Location Objects: Unable to sort temporary container\n"); + return CLI_FAILURE; + } + + ast_cli(a->fd, "Geolocation Location Objects:\n\n"); + + ast_cli(a->fd, + "<Object ID...................................> <Format.....> <Details.............>\n" + "===================================================================================\n"); + + iter = ao2_iterator_init(sorted_container, AO2_ITERATOR_UNLINK); + for (; (loc = ao2_iterator_next(&iter)); ao2_ref(loc, -1)) { + struct ast_str *str; + + ao2_lock(loc); + str = ast_variable_list_join(loc->location_info, ",", "=", "\"", NULL); + if (!str) { + ao2_unlock(loc); + ao2_ref(loc, -1); + ast_cli(a->fd, "Geolocation Location Objects: Unable to allocate temp string for '%s'\n", + ast_sorcery_object_get_id(loc)); + result = CLI_FAILURE; + break; + } + + format_to_str(loc, NULL, &format_name); + ast_cli(a->fd, "%-46.46s %-13s %-s\n", + ast_sorcery_object_get_id(loc), + format_name, + ast_str_buffer(str)); + ao2_unlock(loc); + ast_free(str); + ast_free(format_name); + count++; + } + ao2_iterator_destroy(&iter); + ao2_ref(sorted_container, -1); + ast_cli(a->fd, "\nTotal Location Objects: %d\n\n", count); + + return result; +} + +static char *geoloc_config_list_profiles(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + struct ao2_iterator iter; + struct ao2_container *sorted_container; + struct ao2_container *unsorted_container; + struct ast_geoloc_profile *profile; + int using_regex = 0; + char *result = CLI_SUCCESS; + int ret = 0; + char *action; + int count = 0; + + switch (cmd) { + case CLI_INIT: + e->command = "geoloc list profiles"; + e->usage = "Usage: geoloc list profiles [ like <pattern> ]\n" + " List Geolocation Profile Objects\n"; + return NULL; + case CLI_GENERATE: + return NULL; + } + + if (a->argc != 3 && a->argc != 5) { + return CLI_SHOWUSAGE; + } + + if (a->argc == 5) { + if (strcasecmp(a->argv[3], "like")) { + return CLI_SHOWUSAGE; + } + using_regex = 1; + } + + sorted_container = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK, 0, + ast_sorcery_object_id_sort, NULL); + if (!sorted_container) { + ast_cli(a->fd, "Geolocation Profile Objects: Unable to allocate temporary container\n"); + return CLI_FAILURE; + } + + /* Get a sorted snapshot of the scheduled tasks */ + if (using_regex) { + unsorted_container = ast_sorcery_retrieve_by_regex(geoloc_sorcery, "profile", a->argv[4]); + } else { + unsorted_container = ast_sorcery_retrieve_by_fields(geoloc_sorcery, "profile", + AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL); + } + + ret = ao2_container_dup(sorted_container, unsorted_container, 0); + ao2_ref(unsorted_container, -1); + if (ret != 0) { + ao2_ref(sorted_container, -1); + ast_cli(a->fd, "Geolocation Profile Objects: Unable to sort temporary container\n"); + return CLI_FAILURE; + } + + ast_cli(a->fd, "Geolocation Profile Objects:\n\n"); + + ast_cli(a->fd, + "<Object ID...................................> <Profile Action> <Location Reference> \n" + "=====================================================================================\n"); + + iter = ao2_iterator_init(sorted_container, AO2_ITERATOR_UNLINK); + for (; (profile = ao2_iterator_next(&iter)); ao2_ref(profile, -1)) { + ao2_lock(profile); + + action_to_str(profile, NULL, &action); + ast_cli(a->fd, "%-46.46s %-16s %-s\n", + ast_sorcery_object_get_id(profile), + action, + profile->location_reference); + ao2_unlock(profile); + ast_free(action); + count++; + } + ao2_iterator_destroy(&iter); + ao2_ref(sorted_container, -1); + ast_cli(a->fd, "\nTotal Profile Objects: %d\n\n", count); + + return result; +} + +static char *geoloc_config_show_profiles(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + struct ao2_iterator iter; + struct ao2_container *sorted_container; + struct ao2_container *unsorted_container; + struct ast_geoloc_profile *profile; + int using_regex = 0; + char *result = CLI_SUCCESS; + int ret = 0; + int count = 0; + + switch (cmd) { + case CLI_INIT: + e->command = "geoloc show profiles"; + e->usage = "Usage: geoloc show profiles [ like <pattern> ]\n" + " List Geolocation Profile Objects\n"; + return NULL; + case CLI_GENERATE: + return NULL; + } + + if (a->argc != 3 && a->argc != 5) { + return CLI_SHOWUSAGE; + } + + if (a->argc == 5) { + if (strcasecmp(a->argv[3], "like")) { + return CLI_SHOWUSAGE; + } + using_regex = 1; + } + + /* Create an empty rb-tree container which always sorts its contents. */ + sorted_container = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK, 0, + ast_sorcery_object_id_sort, NULL); + if (!sorted_container) { + ast_cli(a->fd, "Geolocation Profile Objects: Unable to allocate temporary container\n"); + return CLI_FAILURE; + } + + /* Get an unsorted list of profile parameters */ + if (using_regex) { + unsorted_container = ast_sorcery_retrieve_by_regex(geoloc_sorcery, "profile", a->argv[4]); + } else { + unsorted_container = ast_sorcery_retrieve_by_fields(geoloc_sorcery, "profile", + AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL); + } + + /* Copy the unsorted parameters into the rb-tree container which will sort them automatically. */ + ret = ao2_container_dup(sorted_container, unsorted_container, 0); + ao2_ref(unsorted_container, -1); + if (ret != 0) { + ao2_ref(sorted_container, -1); + ast_cli(a->fd, "Geolocation Profile Objects: Unable to sort temporary container\n"); + return CLI_FAILURE; + } + + ast_cli(a->fd, "Geolocation Profile Objects:\n\n"); + + iter = ao2_iterator_init(sorted_container, AO2_ITERATOR_UNLINK); + for (; (profile = ao2_iterator_next(&iter)); ) { + char *action = NULL; + struct ast_str *loc_str = NULL; + struct ast_str *refinement_str = NULL; + struct ast_str *variables_str = NULL; + struct ast_str *resolved_str = NULL; + struct ast_str *usage_rules_str = NULL; + struct ast_geoloc_eprofile *eprofile = ast_geoloc_eprofile_create_from_profile(profile); + ao2_ref(profile, -1); + + if (!ast_strlen_zero(eprofile->location_reference)) { + loc_str = ast_variable_list_join(eprofile->location_info, ",", "=", "\"", NULL); + resolved_str = ast_variable_list_join(eprofile->effective_location, ",", "=", "\"", NULL); + } + + refinement_str = ast_variable_list_join(eprofile->location_refinement, ",", "=", "\"", NULL); + variables_str = ast_variable_list_join(eprofile->location_variables, ",", "=", "\"", NULL); + usage_rules_str = ast_variable_list_join(eprofile->usage_rules, ",", "=", "\"", NULL); + + action_to_str(eprofile, NULL, &action); + + ast_cli(a->fd, + "id: %-s\n" + "profile_action: %-s\n" + "pidf_element: %-s\n" + "location_reference: %-s\n" + "Location_format: %-s\n" + "location_details: %-s\n" + "location_method: %-s\n" + "location_refinement: %-s\n" + "location_variables: %-s\n" + "effective_location: %-s\n" + "usage_rules: %-s\n" + "notes: %-s\n", + eprofile->id, + action, + pidf_element_names[eprofile->pidf_element], + S_OR(eprofile->location_reference, "<none>"), + format_names[eprofile->format], + S_COR(loc_str, ast_str_buffer(loc_str), "<none>"), + S_OR(eprofile->method, "<none>"), + S_COR(refinement_str, ast_str_buffer(refinement_str), "<none>"), + S_COR(variables_str, ast_str_buffer(variables_str), "<none>"), + S_COR(resolved_str, ast_str_buffer(resolved_str), "<none>"), + S_COR(usage_rules_str, ast_str_buffer(usage_rules_str), "<none>"), + S_OR(eprofile->notes, "<none>") + ); + ao2_ref(eprofile, -1); + + ast_free(action); + ast_free(loc_str); + ast_free(refinement_str); + ast_free(variables_str); + ast_free(resolved_str); + ast_free(usage_rules_str); + count++; + } + ao2_iterator_destroy(&iter); + ao2_ref(sorted_container, -1); + ast_cli(a->fd, "\nTotal Profile Objects: %d\n\n", count); + + return result; +} + +static char *geoloc_config_cli_reload(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + char *result = CLI_SUCCESS; + + switch (cmd) { + case CLI_INIT: + e->command = "geoloc reload"; + e->usage = "Usage: geoloc reload\n" + " Reload Geolocation Configuration\n"; + return NULL; + case CLI_GENERATE: + return NULL; + } + + if (a->argc != 2) { + return CLI_SHOWUSAGE; + } + + geoloc_config_reload(); + ast_cli(a->fd, "Geolocation Configuration reloaded.\n"); + + return result; +} + +static struct ast_cli_entry geoloc_location_cli_commands[] = { + AST_CLI_DEFINE(geoloc_config_list_locations, "List Geolocation Location Objects"), + AST_CLI_DEFINE(geoloc_config_list_profiles, "List Geolocation Profile Objects"), + AST_CLI_DEFINE(geoloc_config_show_profiles, "Show Geolocation Profile Objects"), + AST_CLI_DEFINE(geoloc_config_cli_reload, "Reload Geolocation Configuration"), +}; + +struct ast_geoloc_location * AST_OPTIONAL_API_NAME(ast_geoloc_get_location)(const char *id) +{ + if (ast_strlen_zero(id)) { + return NULL; + } + + return ast_sorcery_retrieve_by_id(geoloc_sorcery, "location", id); +} + +struct ast_geoloc_profile * AST_OPTIONAL_API_NAME(ast_geoloc_get_profile)(const char *id) +{ + if (ast_strlen_zero(id)) { + return NULL; + } + + return ast_sorcery_retrieve_by_id(geoloc_sorcery, "profile", id); +} + +int geoloc_config_reload(void) +{ + if (geoloc_sorcery) { + ast_sorcery_reload(geoloc_sorcery); + } + return AST_MODULE_LOAD_SUCCESS; +} + +int geoloc_config_unload(void) +{ + ast_cli_unregister_multiple(geoloc_location_cli_commands, ARRAY_LEN(geoloc_location_cli_commands)); + + ast_sorcery_object_unregister(geoloc_sorcery, "profile"); + ast_sorcery_object_unregister(geoloc_sorcery, "location"); + + if (geoloc_sorcery) { + ast_sorcery_unref(geoloc_sorcery); + } + geoloc_sorcery = NULL; + + return 0; +} + +int geoloc_config_load(void) +{ + if (!(geoloc_sorcery = ast_sorcery_open())) { + ast_log(LOG_ERROR, "Failed to open geolocation sorcery\n"); + return AST_MODULE_LOAD_DECLINE; + } + + ast_sorcery_apply_default(geoloc_sorcery, "location", "config", "geolocation.conf,criteria=type=location"); + if (ast_sorcery_object_register(geoloc_sorcery, "location", geoloc_location_alloc, NULL, geoloc_location_apply_handler)) { + ast_log(LOG_ERROR, "Failed to register geoloc location object with sorcery\n"); + ast_sorcery_unref(geoloc_sorcery); + geoloc_sorcery = NULL; + return AST_MODULE_LOAD_DECLINE; + } + + ast_sorcery_object_field_register(geoloc_sorcery, "location", "type", "", OPT_NOOP_T, 0, 0); + ast_sorcery_object_field_register_custom(geoloc_sorcery, "location", "format", AST_GEOLOC_FORMAT_NONE, + format_handler, format_to_str, NULL, 0, 0); + ast_sorcery_object_field_register_custom(geoloc_sorcery, "location", "location_info", NULL, + location_info_handler, location_info_to_str, location_info_dup, 0, 0); + ast_sorcery_object_field_register(geoloc_sorcery, "location", "location_source", "", OPT_STRINGFIELD_T, + 0, STRFLDSET(struct ast_geoloc_location, location_source)); + ast_sorcery_object_field_register(geoloc_sorcery, "location", "method", "", OPT_STRINGFIELD_T, + 0, STRFLDSET(struct ast_geoloc_location, method)); + + + ast_sorcery_apply_default(geoloc_sorcery, "profile", "config", "geolocation.conf,criteria=type=profile"); + if (ast_sorcery_object_register(geoloc_sorcery, "profile", geoloc_profile_alloc, NULL, geoloc_profile_apply_handler)) { + ast_log(LOG_ERROR, "Failed to register geoloc profile object with sorcery\n"); + ast_sorcery_unref(geoloc_sorcery); + geoloc_sorcery = NULL; + return AST_MODULE_LOAD_DECLINE; + } + + ast_sorcery_object_field_register(geoloc_sorcery, "profile", "type", "", OPT_NOOP_T, 0, 0); + ast_sorcery_object_field_register_custom(geoloc_sorcery, "profile", "pidf_element", + pidf_element_names[AST_PIDF_ELEMENT_DEVICE], pidf_element_handler, pidf_element_to_str, NULL, 0, 0); + ast_sorcery_object_field_register(geoloc_sorcery, "profile", "location_reference", "", OPT_STRINGFIELD_T, + 0, STRFLDSET(struct ast_geoloc_profile, location_reference)); + ast_sorcery_object_field_register_custom(geoloc_sorcery, "profile", "profile_action", "discard_incoming", + action_handler, action_to_str, NULL, 0, 0); + ast_sorcery_object_field_register_custom(geoloc_sorcery, "profile", "usage_rules", NULL, + usage_rules_handler, usage_rules_to_str, usage_rules_dup, 0, 0); + ast_sorcery_object_field_register_custom(geoloc_sorcery, "profile", "location_info_refinement", NULL, + location_refinement_handler, location_refinement_to_str, location_refinement_dup, 0, 0); + ast_sorcery_object_field_register_custom(geoloc_sorcery, "profile", "location_variables", NULL, + location_variables_handler, location_variables_to_str, location_variables_dup, 0, 0); + ast_sorcery_object_field_register(geoloc_sorcery, "profile", "notes", "", OPT_STRINGFIELD_T, + 0, STRFLDSET(struct ast_geoloc_profile, notes)); + + ast_sorcery_load(geoloc_sorcery); + + ast_cli_register_multiple(geoloc_location_cli_commands, ARRAY_LEN(geoloc_location_cli_commands)); + + return AST_MODULE_LOAD_SUCCESS; +} + +int AST_OPTIONAL_API_NAME(ast_geoloc_is_loaded)(void) +{ + return 1; +} + diff --git a/res/res_geolocation/geoloc_datastore.c b/res/res_geolocation/geoloc_datastore.c new file mode 100644 index 0000000000000000000000000000000000000000..040a9bdcdb3f4950fae2694224c1a94985711bd4 --- /dev/null +++ b/res/res_geolocation/geoloc_datastore.c @@ -0,0 +1,325 @@ +/* + * 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. + */ + +#include "asterisk.h" +#include "asterisk/astobj2.h" +#include "asterisk/datastore.h" +#include "asterisk/channel.h" +#include "asterisk/res_geolocation.h" +#include "asterisk/vector.h" +#include "geoloc_private.h" + +#define GEOLOC_DS_TYPE "geoloc_eprofiles" + +struct ast_sorcery *geoloc_sorcery; + +struct eprofiles_datastore { + const char *id; + AST_VECTOR(geoloc_eprofiles, struct ast_geoloc_eprofile *) eprofiles; +}; + +static void geoloc_datastore_free(void *obj) +{ + struct eprofiles_datastore *eds = obj; + + AST_VECTOR_RESET(&eds->eprofiles, ao2_cleanup); + AST_VECTOR_FREE(&eds->eprofiles); + ast_free(eds); +} + +static void *geoloc_datastore_duplicate(void *obj) +{ + struct eprofiles_datastore *in_eds = obj; + struct eprofiles_datastore *out_eds; + int rc = 0; + int i = 0; + int eprofile_count = 0; + + out_eds = ast_calloc(1, sizeof(*out_eds)); + if (!out_eds) { + return NULL; + } + + rc = AST_VECTOR_INIT(&out_eds->eprofiles, 2); + if (rc != 0) { + ast_free(out_eds); + return NULL; + } + + eprofile_count = AST_VECTOR_SIZE(&in_eds->eprofiles); + for (i = 0; i < eprofile_count; i++) { + struct ast_geoloc_eprofile *ep = AST_VECTOR_GET(&in_eds->eprofiles, i); + rc = AST_VECTOR_APPEND(&out_eds->eprofiles, ao2_bump(ep)); + if (rc != 0) { + /* This will clean up the bumped reference to the eprofile */ + geoloc_datastore_free(out_eds); + return NULL; + } + } + + return out_eds; +} + +static const struct ast_datastore_info geoloc_datastore_info = { + .type = GEOLOC_DS_TYPE, + .destroy = geoloc_datastore_free, + .duplicate = geoloc_datastore_duplicate, +}; + +#define IS_GEOLOC_DS(_ds) (_ds && _ds->data && ast_strings_equal(_ds->info->type, GEOLOC_DS_TYPE)) + +const char *ast_geoloc_datastore_get_id(struct ast_datastore *ds) +{ + struct eprofiles_datastore *eds = NULL; + + if (!IS_GEOLOC_DS(ds)) { + return NULL; + } + + eds = (struct eprofiles_datastore *)ds->data; + + return eds->id; +} + +struct ast_datastore *ast_geoloc_datastore_create(const char *id) +{ + struct ast_datastore *ds = NULL; + struct eprofiles_datastore *eds = NULL; + int rc = 0; + + if (ast_strlen_zero(id)) { + ast_log(LOG_ERROR, "A geoloc datastore can't be allocated with a NULL or empty id\n"); + return NULL; + } + + ds = ast_datastore_alloc(&geoloc_datastore_info, NULL); + if (!ds) { + ast_log(LOG_ERROR, "Geoloc datastore '%s' couldn't be allocated\n", id); + return NULL; + } + + eds = ast_calloc(1, sizeof(*eds)); + if (!eds) { + ast_datastore_free(ds); + ast_log(LOG_ERROR, "Private structure for geoloc datastore '%s' couldn't be allocated\n", id); + return NULL; + } + ds->data = eds; + + + rc = AST_VECTOR_INIT(&eds->eprofiles, 2); + if (rc != 0) { + ast_datastore_free(ds); + ast_log(LOG_ERROR, "Vector for geoloc datastore '%s' couldn't be initialized\n", id); + return NULL; + } + + return ds; +} + +int ast_geoloc_datastore_add_eprofile(struct ast_datastore *ds, + struct ast_geoloc_eprofile *eprofile) +{ + struct eprofiles_datastore *eds = NULL; + int rc = 0; + + if (!IS_GEOLOC_DS(ds) || !eprofile) { + return -1; + } + + eds = ds->data; + rc = AST_VECTOR_APPEND(&eds->eprofiles, ao2_bump(eprofile)); + if (rc != 0) { + ao2_ref(eprofile, -1); + ast_log(LOG_ERROR, "Couldn't add eprofile '%s' to geoloc datastore '%s'\n", eprofile->id, eds->id); + return -1; + } + + return AST_VECTOR_SIZE(&eds->eprofiles); +} + +int ast_geoloc_datastore_insert_eprofile(struct ast_datastore *ds, + struct ast_geoloc_eprofile *eprofile, int index) +{ + struct eprofiles_datastore *eds = NULL; + int rc = 0; + + if (!IS_GEOLOC_DS(ds) || !eprofile) { + return -1; + } + + eds = ds->data; + rc = AST_VECTOR_INSERT_AT(&eds->eprofiles, index, ao2_bump(eprofile)); + if (rc != 0) { + ao2_ref(eprofile, -1); + ast_log(LOG_ERROR, "Couldn't add eprofile '%s' to geoloc datastore '%s' in position '%d'\n", + eprofile->id, eds->id, index); + return -1; + } + + return AST_VECTOR_SIZE(&eds->eprofiles); +} + +int ast_geoloc_datastore_size(struct ast_datastore *ds) +{ + struct eprofiles_datastore *eds = NULL; + + if (!IS_GEOLOC_DS(ds)) { + return -1; + } + + eds = ds->data; + + return AST_VECTOR_SIZE(&eds->eprofiles); +} + +int ast_geoloc_datastore_set_inheritance(struct ast_datastore *ds, int inherit) +{ + if (!IS_GEOLOC_DS(ds)) { + return -1; + } + ds->inheritance = inherit ? DATASTORE_INHERIT_FOREVER : 0; + return 0; +} + +struct ast_geoloc_eprofile *ast_geoloc_datastore_get_eprofile(struct ast_datastore *ds, int ix) +{ + struct eprofiles_datastore *eds = NULL; + struct ast_geoloc_eprofile *eprofile; + + if (!IS_GEOLOC_DS(ds)) { + return NULL; + } + + eds = ds->data; + + if (ix >= AST_VECTOR_SIZE(&eds->eprofiles)) { + return NULL; + } + + eprofile = AST_VECTOR_GET(&eds->eprofiles, ix); + return ao2_bump(eprofile); +} + +struct ast_datastore *ast_geoloc_datastore_find(struct ast_channel *chan) +{ + return ast_channel_datastore_find(chan, &geoloc_datastore_info, NULL); +} + +int ast_geoloc_datastore_delete_eprofile(struct ast_datastore *ds, int ix) +{ + struct eprofiles_datastore *eds = NULL; + + if (!IS_GEOLOC_DS(ds)) { + return -1; + } + + eds = ds->data; + + if (ix >= AST_VECTOR_SIZE(&eds->eprofiles)) { + return -1; + } + + ao2_ref(AST_VECTOR_REMOVE(&eds->eprofiles, ix, 1), -1); + return 0; +} + +struct ast_datastore *ast_geoloc_datastore_create_from_eprofile( + struct ast_geoloc_eprofile *eprofile) +{ + struct ast_datastore *ds; + int rc = 0; + + if (!eprofile) { + return NULL; + } + + ds = ast_geoloc_datastore_create(eprofile->id); + if (!ds) { + return NULL; + } + + rc = ast_geoloc_datastore_add_eprofile(ds, eprofile); + if (rc != 0) { + ast_datastore_free(ds); + ds = NULL; + } + + return ds; +} + +struct ast_datastore *ast_geoloc_datastore_create_from_profile_name(const char *profile_name) +{ + struct ast_datastore *ds = NULL; + struct ast_geoloc_eprofile *eprofile = NULL; + struct ast_geoloc_profile *profile = NULL; + int rc = 0; + + if (ast_strlen_zero(profile_name)) { + return NULL; + } + + profile = ast_sorcery_retrieve_by_id(geoloc_sorcery, "profile", profile_name); + if (!profile) { + ast_log(LOG_ERROR, "A profile with the name '%s' was not found\n", profile_name); + return NULL; + } + + ds = ast_geoloc_datastore_create(profile_name); + if (!ds) { + ast_log(LOG_ERROR, "A datastore couldn't be allocated for profile '%s'\n", profile_name); + ao2_ref(profile, -1); + return NULL; + } + + eprofile = ast_geoloc_eprofile_create_from_profile(profile); + ao2_ref(profile, -1); + if (!eprofile) { + ast_datastore_free(ds); + ast_log(LOG_ERROR, "An effective profile with the name '%s' couldn't be allocated\n", profile_name); + return NULL; + } + + rc = ast_geoloc_datastore_add_eprofile(ds, eprofile); + ao2_ref(eprofile, -1); + if (rc != 0) { + ast_datastore_free(ds); + ds = NULL; + } + + return ds; +} + +int geoloc_channel_unload(void) +{ + if (geoloc_sorcery) { + ast_sorcery_unref(geoloc_sorcery); + } + return AST_MODULE_LOAD_SUCCESS; +} + +int geoloc_channel_load(void) +{ + geoloc_sorcery = geoloc_get_sorcery(); + return AST_MODULE_LOAD_SUCCESS; +} + +int geoloc_channel_reload(void) +{ + return AST_MODULE_LOAD_SUCCESS; +} diff --git a/res/res_geolocation/geoloc_dialplan.c b/res/res_geolocation/geoloc_dialplan.c new file mode 100644 index 0000000000000000000000000000000000000000..efa234a0f090dfede783a5b730d5692a5a5eaa9c --- /dev/null +++ b/res/res_geolocation/geoloc_dialplan.c @@ -0,0 +1,457 @@ +/* + * 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. + */ + +#include "asterisk.h" +#include "asterisk/config.h" +#include "asterisk/cli.h" +#include "asterisk/module.h" +#include "asterisk/channel.h" +#include "asterisk/pbx.h" +#include "asterisk/strings.h" +#include "asterisk/utils.h" +#include "asterisk/app.h" +#include "asterisk/res_geolocation.h" +#include "geoloc_private.h" + +static void varlist_to_str(struct ast_variable *list, struct ast_str** buf, size_t len) +{ + struct ast_variable *var = list; + + for (; var; var = var->next) { + ast_str_append(buf, len, "%s=\"%s\"%s", var->name, var->value, var->next ? "," : ""); + } +} + +static int geoloc_profile_read(struct ast_channel *chan, + const char *cmd, char *data, struct ast_str **buf, ssize_t len) +{ + char *parsed_data = ast_strdupa(data); + int index = -1; + struct ast_datastore *ds; + struct ast_geoloc_eprofile *eprofile = NULL; + int profile_count = 0; + + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(field); + AST_APP_ARG(index); + ); + + /* Check for zero arguments */ + if (ast_strlen_zero(parsed_data)) { + ast_log(LOG_ERROR, "%s: Cannot call without arguments\n", cmd); + return -1; + } + + AST_STANDARD_APP_ARGS(args, parsed_data); + + if (ast_strlen_zero(args.field)) { + ast_log(LOG_ERROR, "%s: Cannot call without a field to query\n", cmd); + return -1; + } + + if (!ast_strlen_zero(args.index)) { + if (sscanf(args.index, "%30d", &index) != 1) { + ast_log(LOG_ERROR, "%s: profile_index '%s' is invalid\n", cmd, args.index); + return -1; + } + } + + ds = ast_geoloc_datastore_find(chan); + if (!ds) { + ast_log(LOG_NOTICE, "%s: There are no geoloc profiles on this channel\n", cmd); + return -1; + } + + profile_count = ast_geoloc_datastore_size(ds); + + if (index < 0) { + if (ast_strings_equal(args.field, "count")) { + ast_str_append(buf, len, "%d", profile_count); + } else if (ast_strings_equal(args.field, "inheritable")) { + ast_str_append(buf, len, "%d", ds->inheritance ? 1 : 0); + } else { + ast_log(LOG_ERROR, "%s: Field '%s' is not valid\n", cmd, args.field); + return -1; + } + + return 0; + } + + if (index >= profile_count) { + ast_log(LOG_ERROR, "%s: index %d is out of range 0 -> %d\n", cmd, index, profile_count); + return -1; + } + + eprofile = ast_geoloc_datastore_get_eprofile(ds, index); + if (!eprofile) { + ast_log(LOG_ERROR, "%s: Internal Error. Profile at index %d couldn't be retrieved.\n", cmd, index); + return -1; + } + + if (ast_strings_equal(args.field, "id")) { + ast_str_append(buf, len, "%s", eprofile->id); + } else if (ast_strings_equal(args.field, "location_reference")) { + ast_str_append(buf, len, "%s", eprofile->location_reference); + } else if (ast_strings_equal(args.field, "method")) { + ast_str_append(buf, len, "%s", eprofile->method); + } else if (ast_strings_equal(args.field, "geolocation_routing")) { + ast_str_append(buf, len, "%s", eprofile->geolocation_routing ? "yes" : "no"); + } else if (ast_strings_equal(args.field, "profile_action")) { + ast_str_append(buf, len, "%s", geoloc_action_to_name(eprofile->action)); + } else if (ast_strings_equal(args.field, "format")) { + ast_str_append(buf, len, "%s", geoloc_format_to_name(eprofile->format)); + } else if (ast_strings_equal(args.field, "pidf_element")) { + ast_str_append(buf, len, "%s", geoloc_pidf_element_to_name(eprofile->pidf_element)); + } else if (ast_strings_equal(args.field, "location_source")) { + ast_str_append(buf, len, "%s", eprofile->location_source); + } else if (ast_strings_equal(args.field, "location_info")) { + varlist_to_str(eprofile->location_info, buf, len); + } else if (ast_strings_equal(args.field, "location_info_refinement")) { + varlist_to_str(eprofile->location_refinement, buf, len); + } else if (ast_strings_equal(args.field, "location_variables")) { + varlist_to_str(eprofile->location_variables, buf, len); + } else if (ast_strings_equal(args.field, "effective_location")) { + varlist_to_str(eprofile->effective_location, buf, len); + } else if (ast_strings_equal(args.field, "usage_rules")) { + varlist_to_str(eprofile->usage_rules, buf, len); + } else { + ast_log(LOG_ERROR, "%s: Field '%s' is not valid\n", cmd, args.field); + return -1; + } + + ao2_ref(eprofile, -1); + return 0; +} + +#define TEST_ENUM_VALUE(_cmd, _ep, _field, _value) \ +({ \ + enum ast_geoloc_ ## _field v; \ + if (!_ep) { \ + ast_log(LOG_ERROR, "%s: Field %s requires a valid index\n", _cmd, #_field); \ + return -1; \ + } \ + v = geoloc_ ## _field ## _str_to_enum(_value); \ + if (v == AST_GEOLOC_INVALID_VALUE) { \ + ast_log(LOG_ERROR, "%s: %s '%s' is invalid\n", _cmd, #_field, value); \ + return -1; \ + } \ + _ep->_field = v; \ +}) + +#define TEST_VARLIST(_cmd, _ep, _field, _value) \ +({ \ + struct ast_variable *_list; \ + if (!_ep) { \ + ast_log(LOG_ERROR, "%s: Field %s requires a valid index\n", _cmd, #_field); \ + return -1; \ + } \ + _list = ast_variable_list_from_quoted_string(_value, ",", "=", "\"" ); \ + if (!_list) { \ + ast_log(LOG_ERROR, "%s: %s '%s' is malformed or contains invalid values", _cmd, #_field, _value); \ + return -1; \ + } \ + ast_variables_destroy(_ep->_field); \ + _ep->_field = _list; \ +}) + +static int geoloc_profile_write(struct ast_channel *chan, const char *cmd, char *data, + const char *value) +{ + char *parsed_data = ast_strdupa(data); + struct ast_datastore *ds; + RAII_VAR(struct ast_geoloc_eprofile *, eprofile, NULL, ao2_cleanup); + int profile_count = 0; + int index = -1; + + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(field); + AST_APP_ARG(index); + ); + + /* Check for zero arguments */ + if (ast_strlen_zero(parsed_data)) { + ast_log(LOG_ERROR, "%s: Cannot call without arguments\n", cmd); + return -1; + } + + AST_STANDARD_APP_ARGS(args, parsed_data); + + if (ast_strlen_zero(args.field)) { + ast_log(LOG_ERROR, "%s: Cannot call without a field to set\n", cmd); + return -1; + } + + if (!ast_strlen_zero(args.index)) { + if (sscanf(args.index, "%30d", &index) != 1) { + ast_log(LOG_ERROR, "%s: profile_index '%s' is invalid\n", cmd, args.index); + return -1; + } + } + + ds = ast_geoloc_datastore_find(chan); + if (!ds) { + ast_log(LOG_WARNING, "%s: There are no geoloc profiles on this channel\n", cmd); + return -1; + } + + profile_count = ast_geoloc_datastore_size(ds); + + if (index >= 0 && index < profile_count) { + eprofile = ast_geoloc_datastore_get_eprofile(ds, index); + if (!eprofile) { + ast_log(LOG_ERROR, "%s: Internal Error. Profile at index %d couldn't be retrieved.\n", cmd, index); + return -1; + } + } else if (index >= profile_count) { + ast_log(LOG_ERROR, "%s: index %d is out of range 0 -> %d\n", cmd, index, profile_count); + return -1; + } else { + if (ast_strings_equal(args.field, "inheritable")) { + ast_geoloc_datastore_set_inheritance(ds, ast_true(value)); + } else { + ast_log(LOG_ERROR, "%s: Field '%s' is not valid or requires a profile index\n", cmd, args.field); + return -1; + } + + return 0; + } + + if (ast_strings_equal(args.field, "location_reference")) { + struct ast_geoloc_location *loc = ast_geoloc_get_location(value); + ao2_cleanup(loc); + if (!loc) { + ast_log(LOG_ERROR, "%s: Location reference '%s' doesn't exist\n", cmd, value); + return -1; + } + ast_string_field_set(eprofile, location_reference, value); + } else if (ast_strings_equal(args.field, "method")) { + ast_string_field_set(eprofile, method, value); + + } else if (ast_strings_equal(args.field, "geolocation_routing")) { + eprofile->geolocation_routing = ast_true(value); + + } else if (ast_strings_equal(args.field, "profile_action")) { + TEST_ENUM_VALUE(cmd, eprofile, action, value); + + } else if (ast_strings_equal(args.field, "format")) { + TEST_ENUM_VALUE(cmd, eprofile, format, value); + + } else if (ast_strings_equal(args.field, "pidf_element")) { + TEST_ENUM_VALUE(cmd, eprofile, pidf_element, value); + + } else if (ast_strings_equal(args.field, "location_info")) { + TEST_VARLIST(cmd, eprofile, location_info, value); + } else if (ast_strings_equal(args.field, "location_source")) { + ast_string_field_set(eprofile, location_source, value); + } else if (ast_strings_equal(args.field, "location_info_refinement")) { + TEST_VARLIST(cmd, eprofile, location_refinement, value); + } else if (ast_strings_equal(args.field, "location_variables")) { + TEST_VARLIST(cmd, eprofile, location_variables, value); + } else if (ast_strings_equal(args.field, "effective_location")) { + TEST_VARLIST(cmd, eprofile, effective_location, value); + } else if (ast_strings_equal(args.field, "usage_rules")) { + TEST_VARLIST(cmd, eprofile, usage_rules, value); + } else { + ast_log(LOG_ERROR, "%s: Field '%s' is not valid\n", cmd, args.field); + return -1; + } + + ast_geoloc_eprofile_refresh_location(eprofile); + return 0; +} + +static struct ast_custom_function geoloc_function = { + .name = "GEOLOC_PROFILE", + .read2 = geoloc_profile_read, + .write = geoloc_profile_write, +}; + +#define profile_create "GeolocProfileCreate" + +static int geoloc_eprofile_create(struct ast_channel *chan, const char *data) +{ + char *parsed_data = ast_strdupa(data); + struct ast_datastore *ds; + struct ast_geoloc_eprofile * eprofile; + int profile_count = 0; + int index = -1; + int rc = 0; + struct ast_str *new_size; + + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(id); + AST_APP_ARG(index); + ); + + /* Check for zero arguments */ + if (ast_strlen_zero(parsed_data)) { + ast_log(LOG_ERROR, "%s: Cannot call without arguments\n", profile_create); + return -1; + } + + AST_STANDARD_APP_ARGS(args, parsed_data); + + if (ast_strlen_zero(args.id)) { + ast_log(LOG_ERROR, "%s: Cannot call without an id field\n", profile_create); + return -1; + } + + if (!ast_strlen_zero(args.index)) { + if (sscanf(args.index, "%30d", &index) != 1) { + ast_log(LOG_ERROR, "%s: profile_index '%s' is invalid\n", profile_create, args.index); + return -1; + } + } else { + index = -1; + } + + ds = ast_geoloc_datastore_find(chan); + if (!ds) { + ast_log(LOG_WARNING, "%s: There are no geoloc profiles on this channel\n", profile_create); + return -1; + } + + profile_count = ast_geoloc_datastore_size(ds); + if (index < -1 || index >= profile_count) { + ast_log(LOG_ERROR, "%s: Invalid insert_before index '%d'. It must be 0 to insert at the beginning of the list or -1 to append to the end of the list\n", profile_create, index); + return -1; + } + + eprofile = ast_geoloc_eprofile_alloc(args.id); + if (!eprofile) { + ast_log(LOG_ERROR, "%s: Could not allocate eprofile '%s'\n", profile_create, args.id); + return -1; + } + + ds = ast_geoloc_datastore_find(chan); + if (!ds) { + ds = ast_geoloc_datastore_create_from_eprofile(eprofile); + if (!ds) { + ao2_ref(eprofile, -1); + ast_log(LOG_ERROR, "%s: Could not create datastore for eprofile '%s'\n", profile_create, args.id); + return -1; + } + rc = 1; + ast_channel_datastore_add(chan, ds); + } else if (index < 0) { + rc = ast_geoloc_datastore_add_eprofile(ds, eprofile); + if (rc <= 0) { + ao2_ref(eprofile, -1); + ast_log(LOG_ERROR, "%s: Could not add eprofile '%s' to datastore\n", profile_create, args.id); + return -1; + } + } else { + rc = ast_geoloc_datastore_insert_eprofile(ds, eprofile, index); + if (rc <= 0) { + ao2_ref(eprofile, -1); + ast_log(LOG_ERROR, "%s: Could not insert eprofile '%s' to datastore\n", profile_create, args.id); + return -1; + } + } + + new_size = ast_str_alloca(16); + ast_str_append(&new_size, 0, "%d", rc); + pbx_builtin_setvar_helper(chan, "GEOLOC_PROFILE_COUNT", ast_str_buffer(new_size)); + + return 0; +} + +#define profile_delete "GeolocProfileDelete" + +static int geoloc_eprofile_delete(struct ast_channel *chan, const char *data) +{ + char *parsed_data = ast_strdupa(data); + struct ast_datastore *ds; + int profile_count = 0; + int index = -1; + struct ast_str *new_size; + + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(index); + ); + + /* Check for zero arguments */ + if (ast_strlen_zero(parsed_data)) { + ast_log(LOG_ERROR, "%s: Cannot call without arguments\n", profile_delete); + return -1; + } + + AST_STANDARD_APP_ARGS(args, parsed_data); + + if (!ast_strlen_zero(args.index)) { + if (sscanf(args.index, "%30d", &index) != 1) { + ast_log(LOG_ERROR, "%s: profile_index '%s' is invalid\n", profile_delete, args.index); + return -1; + } + } else { + ast_log(LOG_ERROR, "%s: A profile_index is required\n", profile_delete); + return -1; + } + + ds = ast_geoloc_datastore_find(chan); + if (!ds) { + ast_log(LOG_WARNING, "%s: There are no geoloc profiles on this channel\n", profile_delete); + return -1; + } + + profile_count = ast_geoloc_datastore_size(ds); + if (index < -1 || index >= profile_count) { + ast_log(LOG_ERROR, "%s: Invalid profile_index '%d'. It must be between 0 and %d\n", + profile_create, index, profile_count - 1); + return -1; + } + + ast_geoloc_datastore_delete_eprofile(ds, index); + profile_count = ast_geoloc_datastore_size(ds); + + new_size = ast_str_alloca(16); + ast_str_append(&new_size, 0, "%d", profile_count); + pbx_builtin_setvar_helper(chan, "GEOLOC_PROFILE_COUNT", ast_str_buffer(new_size)); + + return 0; +} + +int geoloc_dialplan_unload(void) +{ + ast_unregister_application(profile_delete); + ast_unregister_application(profile_create); + ast_custom_function_unregister(&geoloc_function); + + return AST_MODULE_LOAD_SUCCESS; +} + +int geoloc_dialplan_load(void) +{ + int res = 0; + + res = ast_custom_function_register(&geoloc_function); + if (res == 0) { + res = ast_register_application_xml(profile_create, geoloc_eprofile_create); + } + if (res == 0) { + res = ast_register_application_xml(profile_delete, geoloc_eprofile_delete); + } + + return res == 0 ? AST_MODULE_LOAD_SUCCESS : AST_MODULE_LOAD_DECLINE; +} + +int geoloc_dialplan_reload(void) +{ + return AST_MODULE_LOAD_SUCCESS; +} + diff --git a/res/res_geolocation/geoloc_doc.xml b/res/res_geolocation/geoloc_doc.xml new file mode 100644 index 0000000000000000000000000000000000000000..5c8bf793312c13181550525b53fe1afd481bc2bd --- /dev/null +++ b/res/res_geolocation/geoloc_doc.xml @@ -0,0 +1,235 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE docs SYSTEM "appdocsxml.dtd"> +<docs> + <configInfo name="res_geolocation" language="en_US"> + <synopsis>Core Geolocation Support</synopsis> + <configFile name="geolocation.conf"> + <configObject name="location"> + <synopsis>Location</synopsis> + <description> + <para>cffdffff</para> + </description> + <configOption name="type"> + <synopsis>Must be of type 'location'.</synopsis> + </configOption> + <configOption name="format" default=""> + <synopsis>Location specification type</synopsis> + <description> + <enumlist> + <enum name="civicAddress"> + <para> + The <literal>location_info</literal> + parameter must contain a comma separated list of IANA codes + or synonyms describing the civicAddress of this location. + The IANA codes and synonyms can be obtained by executing + the CLI command <literal>geoloc show civicAddr_mapping</literal>. + </para> + </enum> + <enum name="GML"> + <para> + The + <literal>location_info</literal> parameter must contain a comma + separated list valid GML elements describing this location. + </para> + </enum> + <enum name="URI"> + <para> + The + <literal>location_info</literal> parameter must contain a single + URI parameter which contains an external URI describing this location. + </para> + </enum> + </enumlist> + </description> + </configOption> + <configOption name="location_info" default=""> + <synopsis>Location information</synopsis> + <description> + <para>The contents of this parameter are specific to the + location <literal>format</literal>.</para> + <enumlist> + <enum name="civicAddress"> + <para> + location_info = country=US,A1="New York",city_district=Manhattan, + A3="New York", house_number=1633, street=46th, street_suffix = Street, + postal_code=10222,floor=20,room=20A2 + </para> + </enum> + <enum name="GML"> + <para> + location_info = Shape=Sphere, pos3d="39.12345 -105.98766 1920", radius=200 + </para> + </enum> + <enum name="URI"> + <para> + location_info = URI=https:/something.com?exten=${EXTEN} + </para> + </enum> + </enumlist> + </description> + </configOption> + <configOption name="location_source" default=""> + <synopsis>Fully qualified host name</synopsis> + <description> + <para>This parameter isn't required but if provided, RFC8787 says it MUST be a fully + qualified host name. IP addresses are specifically NOT allowed. The value will be placed + in a <literal>loc-src</literal> parameter appended to the URI in the <literal> + Geolocation</literal> header.</para> + </description> + </configOption> + <configOption name="method" default=""> + <synopsis>Location determination method</synopsis> + <description> + <para>This is a rarely used field in the specification that would + indicate the method used to determine the location. Its usage and values should be + pre-negotiated with any recipients.</para> + <enumlist> + <enum name="GPS"/> + <enum name="A-GPS"/> + <enum name="Manual"/> + <enum name="DHCP"/> + <enum name="Triangulation"/> + <enum name="Cell"/> + <enum name="802.11"/> + </enumlist> + </description> + </configOption> + </configObject> + <configObject name="profile"> + <synopsis>Profile</synopsis> + <description> + <para>cffdffff</para> + </description> + <configOption name="type"> + <synopsis>Must be of type 'profile'.</synopsis> + </configOption> + <configOption name="pidf_element" default="device"> + <synopsis>PIDF-LO element to place this profile in</synopsis> + <description> + <enumlist> + <enum name="tuple" /> + <enum name="device" /> + <enum name="person" /> + </enumlist> + <para> + Based on RFC5491 (see below) the recommended and default element + is <literal>device</literal>. + </para> + </description> + <see-also> + <ref type="link">https://www.rfc-editor.org/rfc/rfc5491.html#section-3.4</ref> + </see-also> + </configOption> + <configOption name="location_reference" default=""> + <synopsis>Reference to a location object</synopsis> + </configOption> + <configOption name="location_info_refinement" default=""> + <synopsis>Reference to a location object</synopsis> + </configOption> + <configOption name="location_variables" default=""> + <synopsis>Reference to a location object</synopsis> + </configOption> + <configOption name="usage_rules" default="yes"> + <synopsis>location specification type</synopsis> + <description> + <para>xxxx</para> + </description> + </configOption> + <configOption name="notes" default=""> + <synopsis>Notes to be added to the outgoing PIDF-LO document</synopsis> + <description> + <para>The specification of this parameter will cause a + <literal><note-well></literal> element to be added to the + outgoing PIDF-LO document. Its usage should be pre-negotiated with + any recipients.</para> + </description> + </configOption> + <configOption name="profile_action" default="discard_incoming"> + <synopsis>Determine which profile on a channel should be used</synopsis> + <description> + <enumlist> + <enum name="prefer_incoming"> + <para>Use the incoming profile if it exists and has location information, otherwise use the + configured profile if it exists and has location information. If neither profile has location + information, nothing is sent. + </para></enum> + <enum name="prefer_config"> + <para>Use the configured profile if it exists and has location information, otherwise use the + incoming profile if it exists and has location information. If neither profile has location + information, nothing is sent. + </para></enum> + <enum name="discard_incoming" + ><para>Discard any incoming profile and use the configured profile if it exists and + it has location information. If the configured profile doesn't exist or has no + location information, nothing is sent. + </para></enum> + <enum name="discard_config"> + <para>Discard any configured profile and use the incoming profile if it exists and + it has location information. If the incoming profile doesn't exist or has no + location information, nothing is sent. + </para></enum> + </enumlist> + </description> + </configOption> + </configObject> + </configFile> + </configInfo> + <function name="GEOLOC_PROFILE" language="en_US"> + <synopsis> + Get or Set a field in a geolocation profile + </synopsis> + <syntax> + <parameter name="field" required="true"> + <para>The profile field to operate on.</para> + </parameter> + <parameter name="profile_index" required="false"> + <para>The index of the profile to operate on. Not required for the special fields.</para> + </parameter> + </syntax> + </function> + <application name="GeolocProfileCreate" language="en_US"> + <synopsis> + Create a new, empty Geolocation Profile on a channel + </synopsis> + <syntax> + <parameter name="id" required="true"><para> + The id of the new profile. + </para></parameter> + <parameter name="profile_index" required="false"><para> + The position at which to insert the new eprofile. + Existing profiles will be moved forward to make room. + Leave empty to append to the end of the list. + </para></parameter> + </syntax> + <description> + <para>This application adds a new, empty Geolocation Profile to a channel.</para> + <para>The following variable is set:</para> + <variablelist> + <variable name="GEOLOC_PROFILE_COUNT"> + <para>The number of profiles on the channel after the new one is created</para> + </variable> + </variablelist> + </description> + </application> + <application name="GeolocProfileDelete" language="en_US"> + <synopsis> + Delete a Geolocation Profile from a channel + </synopsis> + <syntax> + <parameter name="profile_index" required="true"><para> + The position of the profile to be deleted + Existing profiles will be moved back. + </para></parameter> + </syntax> + <description> + <para>This application deletes a Geolocation Profile from a channel.</para> + <para>The following variable is set:</para> + <variablelist> + <variable name="GEOLOC_PROFILE_COUNT"> + <para>The number of profiles left on the channel after the delete.</para> + </variable> + </variablelist> + </description> + </application> +</docs> + diff --git a/res/res_geolocation/geoloc_eprofile.c b/res/res_geolocation/geoloc_eprofile.c new file mode 100644 index 0000000000000000000000000000000000000000..64244f44245108f1166751d512d306f5571cb774 --- /dev/null +++ b/res/res_geolocation/geoloc_eprofile.c @@ -0,0 +1,1002 @@ +/* + * 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. + */ + +#include "asterisk.h" +#include "asterisk/pbx.h" +#include "asterisk/strings.h" +#include "asterisk/xml.h" +#include "geoloc_private.h" + +extern const uint8_t _binary_res_geolocation_pidf_to_eprofile_xslt_start[]; +extern const uint8_t _binary_res_geolocation_pidf_to_eprofile_xslt_end[]; +static size_t pidf_to_eprofile_xslt_size; + +extern const uint8_t _binary_res_geolocation_pidf_lo_test_xml_start[]; +extern const uint8_t _binary_res_geolocation_pidf_lo_test_xml_end[]; +static size_t pidf_lo_test_xml_size; + +extern const uint8_t _binary_res_geolocation_eprofile_to_pidf_xslt_start[]; +extern const uint8_t _binary_res_geolocation_eprofile_to_pidf_xslt_end[]; +static size_t eprofile_to_pidf_xslt_size; + +static struct ast_xslt_doc *eprofile_to_pidf_xslt; +static struct ast_xslt_doc *pidf_to_eprofile_xslt; + +static struct ast_sorcery *geoloc_sorcery; + +#define DUP_VARS(_dest, _source) \ +({ \ + int _rc = 0; \ + if (_source) { \ + struct ast_variable *_vars = ast_variables_dup(_source); \ + if (!_vars) { \ + _rc = -1; \ + } else { \ + _dest = _vars; \ + } \ + } \ + (_rc); \ +}) + +static void geoloc_eprofile_destructor(void *obj) +{ + struct ast_geoloc_eprofile *eprofile = obj; + + ast_string_field_free_memory(eprofile); + ast_variables_destroy(eprofile->location_info); + ast_variables_destroy(eprofile->location_refinement); + ast_variables_destroy(eprofile->location_variables); + ast_variables_destroy(eprofile->effective_location); + ast_variables_destroy(eprofile->usage_rules); +} + +struct ast_geoloc_eprofile *ast_geoloc_eprofile_alloc(const char *name) +{ + struct ast_geoloc_eprofile *eprofile = ao2_alloc_options(sizeof(*eprofile), + geoloc_eprofile_destructor, AO2_ALLOC_OPT_LOCK_NOLOCK); + + ast_string_field_init(eprofile, 256); + ast_string_field_set(eprofile, id, name); /* SAFE string fields handle NULL */ + + return eprofile; +} + +int ast_geoloc_eprofile_refresh_location(struct ast_geoloc_eprofile *eprofile) +{ + struct ast_geoloc_location *loc = NULL; + struct ast_variable *temp_locinfo = NULL; + struct ast_variable *temp_effloc = NULL; + struct ast_variable *var; + int rc = 0; + + if (!eprofile) { + return -1; + } + + if (!ast_strlen_zero(eprofile->location_reference)) { + loc = ast_sorcery_retrieve_by_id(geoloc_sorcery, "location", eprofile->location_reference); + if (!loc) { + ast_log(LOG_ERROR, "Profile '%s' referenced location '%s' does not exist!", eprofile->id, + eprofile->location_reference); + return -1; + } + + eprofile->format = loc->format; + rc = DUP_VARS(temp_locinfo, loc->location_info); + ast_string_field_set(eprofile, method, loc->method); + ao2_ref(loc, -1); + if (rc != 0) { + return -1; + } + } else { + temp_locinfo = eprofile->location_info; + } + + rc = DUP_VARS(temp_effloc, temp_locinfo); + if (rc != 0) { + ast_variables_destroy(temp_locinfo); + return -1; + } + + if (eprofile->location_refinement) { + for (var = eprofile->location_refinement; var; var = var->next) { + struct ast_variable *newvar = ast_variable_new(var->name, var->value, ""); + if (!newvar) { + ast_variables_destroy(temp_locinfo); + ast_variables_destroy(temp_effloc); + return -1; + } + if (ast_variable_list_replace(&temp_effloc, newvar)) { + ast_variable_list_append(&temp_effloc, newvar); + } + } + } + + ast_variables_destroy(eprofile->location_info); + eprofile->location_info = temp_locinfo; + ast_variables_destroy(eprofile->effective_location); + eprofile->effective_location = temp_effloc; + + return 0; +} + +struct ast_geoloc_eprofile *ast_geoloc_eprofile_create_from_profile(struct ast_geoloc_profile *profile) +{ + struct ast_geoloc_eprofile *eprofile; + const char *profile_id; + int rc = 0; + + if (!profile) { + return NULL; + } + + profile_id = ast_sorcery_object_get_id(profile); + + eprofile = ast_geoloc_eprofile_alloc(profile_id); + if (!eprofile) { + return NULL; + } + + ao2_lock(profile); + eprofile->geolocation_routing = profile->geolocation_routing; + eprofile->pidf_element = profile->pidf_element; + + rc = ast_string_field_set(eprofile, location_reference, profile->location_reference); + if (rc == 0) { + ast_string_field_set(eprofile, notes, profile->notes); + } + if (rc == 0) { + rc = DUP_VARS(eprofile->location_refinement, profile->location_refinement); + } + if (rc == 0) { + rc = DUP_VARS(eprofile->location_variables, profile->location_variables); + } + if (rc == 0) { + rc = DUP_VARS(eprofile->usage_rules, profile->usage_rules); + } + if (rc != 0) { + ao2_unlock(profile); + ao2_ref(eprofile, -1); + return NULL; + } + + eprofile->action = profile->action; + ao2_unlock(profile); + + if (ast_geoloc_eprofile_refresh_location(eprofile) != 0) { + ao2_ref(eprofile, -1); + return NULL; + } + + return eprofile; +} + +static int set_loc_src(struct ast_geoloc_eprofile *eprofile, const char *uri, const char *ref_str) +{ + char *local_uri = ast_strdupa(uri); + char *loc_src = NULL; + + loc_src = strchr(local_uri, ';'); + if (loc_src) { + loc_src = '\0'; + loc_src++; + } + + if (!ast_strlen_zero(loc_src)) { + if (ast_begins_with(loc_src, "loc-src=")) { + struct ast_sockaddr loc_source_addr; + int rc = 0; + loc_src += 8; + rc = ast_sockaddr_parse(&loc_source_addr, loc_src, PARSE_PORT_FORBID); + if (rc == 1) { + ast_log(LOG_WARNING, "%s: URI '%s' has an invalid 'loc-src' parameter." + " RFC8787 states that IP addresses MUST be dropped.\n", + ref_str, uri); + return -1; + } else { + ast_string_field_set(eprofile, location_source, loc_src); + } + } + } + return 0; +} + +struct ast_geoloc_eprofile *ast_geoloc_eprofile_create_from_uri(const char *uri, + const char *ref_str) +{ + struct ast_geoloc_eprofile *eprofile = NULL; + char *ra = NULL; + char *local_uri; + + if (ast_strlen_zero(uri)) { + return NULL; + } + local_uri = ast_strdupa(uri); + + if (local_uri[0] == '<') { + local_uri++; + } + ra = strchr(local_uri, '>'); + if (ra) { + *ra = '\0'; + } + + ast_strip(local_uri); + + eprofile = ast_geoloc_eprofile_alloc(local_uri); + if (!eprofile) { + return NULL; + } + + set_loc_src(eprofile, uri, ref_str); + + eprofile->format = AST_GEOLOC_FORMAT_URI; + eprofile->location_info = ast_variable_new("URI", local_uri, ""); + + return eprofile; +} + +static struct ast_variable *geoloc_eprofile_resolve_varlist(struct ast_variable *source, + struct ast_variable *variables, struct ast_channel *chan) +{ + struct ast_variable *dest = NULL; + struct ast_variable *var = NULL; + struct varshead *vh = NULL; + struct ast_str *buf = ast_str_alloca(256); + + if (!source || !chan) { + return NULL; + } + + /* + * ast_str_substitute_variables does only minimal recursive resolution so we need to + * pre-resolve each variable in the "variables" list, then use that result to + * do the final pass on the "source" variable list. + */ + if (variables) { + var = variables; + vh = ast_var_list_create(); + if (!vh) { + return NULL; + } + for ( ; var; var = var->next) { + ast_str_substitute_variables_full2(&buf, 0, chan, vh, var->value, NULL, 1); + AST_VAR_LIST_INSERT_TAIL(vh, ast_var_assign(var->name, ast_str_buffer(buf))); + ast_str_reset(buf); + } + } + + var = source; + for ( ; var; var = var->next) { + struct ast_variable *newvar = NULL; + ast_str_substitute_variables_full2(&buf, 0, chan, vh, var->value, NULL, 1); + newvar = ast_variable_new(var->name, ast_str_buffer(buf), ""); + if (!newvar) { + ast_variables_destroy(dest); + ast_var_list_destroy(vh); + return NULL; + } + ast_variable_list_append(&dest, newvar); + ast_str_reset(buf); + } + ast_var_list_destroy(vh); + + return dest; +} + + +const char *ast_geoloc_eprofile_to_uri(struct ast_geoloc_eprofile *eprofile, + struct ast_channel *chan, struct ast_str **buf, const char *ref_string) +{ + const char *uri = NULL; + struct ast_variable *resolved = NULL; + char *result; + int we_created_buf = 0; + + if (!eprofile || !buf) { + return NULL; + } + + if (eprofile->format != AST_GEOLOC_FORMAT_URI) { + ast_log(LOG_ERROR, "%s: '%s' is not a URI profile. It's '%s'\n", + ref_string, eprofile->id, geoloc_format_to_name(eprofile->format)); + return NULL; + } + + resolved = geoloc_eprofile_resolve_varlist(eprofile->effective_location, + eprofile->location_variables, chan); + if (!resolved) { + return NULL; + } + + uri = ast_variable_find_in_list(resolved, "URI"); + result = uri ? ast_strdupa(uri) : NULL; + ast_variables_destroy(resolved); + + if (ast_strlen_zero(result)) { + ast_log(LOG_ERROR, "%s: '%s' is a URI profile but had no, or an empty, 'URI' entry in location_info\n", + ref_string, eprofile->id); + return NULL; + } + + if (!*buf) { + *buf = ast_str_create(256); + if (!*buf) { + return NULL; + } + we_created_buf = 1; + } + + if (ast_str_append(buf, 0, "%s", result) <= 0) { + if (we_created_buf) { + ast_free(*buf); + *buf = NULL; + return NULL; + } + } + + return ast_str_buffer(*buf); +} + +static struct ast_variable *var_list_from_node(struct ast_xml_node *node, const char *reference_string) +{ + struct ast_variable *list = NULL; + struct ast_xml_node *container; + struct ast_xml_node *child; + struct ast_variable *var; + RAII_VAR(struct ast_str *, buf, NULL, ast_free); + SCOPE_ENTER(3, "%s\n", reference_string); + + container = ast_xml_node_get_children(node); + for (child = container; child; child = ast_xml_node_get_next(child)) { + const char *name = ast_xml_node_get_name(child); + const char *value = ast_xml_get_text(child); + const char *uom = ast_xml_get_attribute(child, "uom"); + + if (uom) { + /* '20 radians\0' */ + char *newval = ast_malloc(strlen(value) + 1 + strlen(uom) + 1); + sprintf(newval, "%s %s", value, uom); + var = ast_variable_new(name, newval, ""); + ast_free(newval); + } else { + var = ast_variable_new(name, value, ""); + } + + if (!var) { + ast_variables_destroy(list); + SCOPE_EXIT_RTN_VALUE(NULL, "%s: Allocation failure\n", reference_string); + } + ast_variable_list_append(&list, var); + } + + ast_variable_list_join(list, ", ", "=", "\"", &buf); + + SCOPE_EXIT_RTN_VALUE(list, "%s: Done. %s\n", reference_string, ast_str_buffer(buf)); +} + +static struct ast_variable *var_list_from_loc_info(struct ast_xml_node *locinfo, + enum ast_geoloc_format format, const char *reference_string) +{ + struct ast_variable *list = NULL; + struct ast_xml_node *container; + struct ast_variable *var; + RAII_VAR(struct ast_str *, buf, NULL, ast_free); + SCOPE_ENTER(3, "%s\n", reference_string); + + container = ast_xml_node_get_children(locinfo); + if (format == AST_GEOLOC_FORMAT_CIVIC_ADDRESS) { + var = ast_variable_new("lang", ast_xml_get_attribute(container, "lang"), ""); + if (!var) { + SCOPE_EXIT_RTN_VALUE(NULL, "%s: Allocation failure\n", reference_string); + } + ast_variable_list_append(&list, var); + } else { + var = ast_variable_new("shape", ast_xml_node_get_name(container), ""); + if (!var) { + SCOPE_EXIT_RTN_VALUE(NULL, "%s: Allocation failure\n", reference_string); + } + ast_variable_list_append(&list, var); + var = ast_variable_new("crs", ast_xml_get_attribute(container, "srsName"), ""); + if (!var) { + ast_variables_destroy(list); + SCOPE_EXIT_RTN_VALUE(NULL, "%s: Allocation failure\n", reference_string); + } + ast_variable_list_append(&list, var); + } + + ast_variable_list_append(&list, var_list_from_node(container, reference_string)); + + ast_variable_list_join(list, ", ", "=", "\"", &buf); + + SCOPE_EXIT_RTN_VALUE(list, "%s: Done. %s\n", reference_string, ast_str_buffer(buf)); +} + +static struct ast_geoloc_eprofile *geoloc_eprofile_create_from_xslt_result( + struct ast_xml_doc *result_doc, + const char *reference_string) +{ + struct ast_geoloc_eprofile *eprofile; + struct ast_xml_node *presence = NULL; + struct ast_xml_node *pidf_element = NULL; + struct ast_xml_node *location_info = NULL; + struct ast_xml_node *usage_rules = NULL; + struct ast_xml_node *method = NULL; + struct ast_xml_node *note_well = NULL; + char *doc_str; + int doc_len; + const char *id; + const char *format_str; + const char *pidf_element_str; + const char *method_str; + const char *note_well_str; + SCOPE_ENTER(3, "%s\n", reference_string); + + ast_xml_doc_dump_memory(result_doc, &doc_str, &doc_len); + ast_trace(5, "xslt result doc:\n%s\n", doc_str); + ast_xml_free_text(doc_str); + + presence = ast_xml_get_root(result_doc); + pidf_element = ast_xml_node_get_children(presence); + location_info = ast_xml_find_child_element(pidf_element, "location-info", NULL, NULL); + usage_rules = ast_xml_find_child_element(pidf_element, "usage-rules", NULL, NULL); + method = ast_xml_find_child_element(pidf_element, "method", NULL, NULL); + note_well = ast_xml_find_child_element(pidf_element, "note-well", NULL, NULL); + + id = S_OR(ast_xml_get_attribute(pidf_element, "id"), ast_xml_get_attribute(presence, "entity")); + eprofile = ast_geoloc_eprofile_alloc(id); + if (!eprofile) { + SCOPE_EXIT_RTN_VALUE(NULL, "%s: Allocation failure\n", reference_string); + } + + format_str = ast_xml_get_attribute(location_info, "format"); + if (strcasecmp(format_str, "gml") == 0) { + eprofile->format = AST_GEOLOC_FORMAT_GML; + } else if (strcasecmp(format_str, "civicAddress") == 0) { + eprofile->format = AST_GEOLOC_FORMAT_CIVIC_ADDRESS; + } else { + ao2_ref(eprofile, -1); + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unknown format '%s'\n", reference_string, format_str); + } + + pidf_element_str = ast_xml_node_get_name(pidf_element); + eprofile->pidf_element = geoloc_pidf_element_str_to_enum(pidf_element_str); + + eprofile->location_info = var_list_from_loc_info(location_info, eprofile->format, reference_string); + if (!eprofile->location_info) { + ao2_ref(eprofile, -1); + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, + "%s: Unable to create location variables\n", reference_string); + } + + eprofile->usage_rules = var_list_from_node(usage_rules, reference_string); + + method_str = ast_xml_get_text(method); + ast_string_field_set(eprofile, method, method_str); + + note_well_str = ast_xml_get_text(note_well); + ast_string_field_set(eprofile, notes, note_well_str); + + SCOPE_EXIT_RTN_VALUE(eprofile, "%s: Done.\n", reference_string); +} + +struct ast_geoloc_eprofile *ast_geoloc_eprofile_create_from_pidf( + struct ast_xml_doc *pidf_xmldoc, const char *geoloc_uri, const char *ref_str) +{ + RAII_VAR(struct ast_xml_doc *, result_doc, NULL, ast_xml_close); + struct ast_geoloc_eprofile *eprofile; + /* + * The namespace prefixes used here (dm, def, gp, etc) don't have to match + * the ones used in the received PIDF-LO document but they MUST match the + * ones in the embedded pidf_to_eprofile stylesheet. + * + * RFC5491 Rule 8 states that... + * Where a PIDF document contains more than one <geopriv> + * element, the priority of interpretation is given to the first + * <device> element in the document containing a location. If no + * <device> element containing a location is present in the document, + * then priority is given to the first <tuple> element containing a + * location. Locations contained in <person> tuples SHOULD only be + * used as a last resort. + * + * Reminder: xpath arrays are 1-based not 0-based. + */ + const char *find_device[] = { "path", "/def:presence/dm:device[.//gp:location-info][1]", NULL}; + const char *find_tuple[] = { "path", "/def:presence/def:tuple[.//gp:location-info][1]", NULL}; + const char *find_person[] = { "path", "/def:presence/dm:person[.//gp:location-info][1]", NULL}; + SCOPE_ENTER(3, "%s\n", ref_str); + + + result_doc = ast_xslt_apply(pidf_to_eprofile_xslt, pidf_xmldoc, find_device); + if (!result_doc || !ast_xml_node_get_children((struct ast_xml_node *)result_doc)) { + ast_xml_close(result_doc); + result_doc = ast_xslt_apply(pidf_to_eprofile_xslt, pidf_xmldoc, find_tuple); + } + if (!result_doc || !ast_xml_node_get_children((struct ast_xml_node *)result_doc)) { + ast_xml_close(result_doc); + result_doc = ast_xslt_apply(pidf_to_eprofile_xslt, pidf_xmldoc, find_person); + } + if (!result_doc || !ast_xml_node_get_children((struct ast_xml_node *)result_doc)) { + SCOPE_EXIT_RTN_VALUE(NULL, "%s: Not a PIDF-LO. Skipping.\n", ref_str); + } + + /* + * The document returned from the stylesheet application looks like this... + * <presence id="presence-entity"> + * <tuple id="element-id"> + * <location-info format="gml">shape="Ellipsoid", crs="3d", ...</location-info> + * <usage-rules>retransmission-allowed="no", retention-expiry="2010-11-14T20:00:00Z"</usage-rules> + * <method>Hybrid_A-GPS</method> + * </tuple> + * </presence> + * + * Regardless of whether the pidf-element was tuple, device or person and whether + * the format is gml or civicAddress, the presence, pidf-element, location-info, + * usage-rules and method elements should be there although usage-rules and method + * may be empty. + * + * The contents of the location-info and usage-rules elements can be passed directly to + * ast_variable_list_from_string(). + */ + + eprofile = geoloc_eprofile_create_from_xslt_result(result_doc, ref_str); + + if (geoloc_uri) { + set_loc_src(eprofile, geoloc_uri, ref_str); + } + + SCOPE_EXIT_RTN_VALUE(eprofile, "%s: Done.\n", ref_str); +} + +/*! + * \internal + * \brief Create an common, intermediate XML document to pass to the outgoing XSLT process + * + * \param eprofile The eprofile + * \param chan The channel to resolve variables against + * \param ref_string A reference string for error messages + * \return An XML doc + * + * \note Given that the document is simple and static, it was easier to just + * create the elements in a string buffer and call ast_xml_read_memory() + * at the end instead of creating + * + */ +static struct ast_xml_node *geoloc_eprofile_to_intermediate(const char *element_name, struct ast_geoloc_eprofile *eprofile, + struct ast_channel *chan, const char *ref_string) +{ + struct ast_variable *resolved_location = NULL; + struct ast_variable *resolved_usage = NULL; + struct ast_variable *var = NULL; + RAII_VAR(struct ast_xml_node *, pidf_node, NULL, ast_xml_free_node); + struct ast_xml_node *rtn_pidf_node; + struct ast_xml_node *loc_node; + struct ast_xml_node *info_node; + struct ast_xml_node *rules_node; + struct ast_xml_node *method_node; + struct ast_xml_node *notes_node; + struct ast_xml_node *timestamp_node; + struct timeval tv = ast_tvnow(); + struct tm tm = { 0, }; + char timestr[32] = { 0, }; + int rc = 0; + + SCOPE_ENTER(3, "%s\n", ref_string); + + if (!eprofile || !chan) { + SCOPE_EXIT_RTN_VALUE(NULL, "%s: Either or both eprofile or chan were NULL\n", ref_string); + } + + pidf_node = ast_xml_new_node(element_name); + if (!pidf_node) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'pidf' XML node\n", + ref_string); + } + + loc_node = ast_xml_new_child(pidf_node, "location-info"); + if (!loc_node) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'location-info' XML node\n", + ref_string); + } + rc = ast_xml_set_attribute(loc_node, "format", geoloc_format_to_name(eprofile->format)); + if (rc != 0) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to set 'format' XML attribute\n", ref_string); + } + + resolved_location = geoloc_eprofile_resolve_varlist(eprofile->effective_location, + eprofile->location_variables, chan); + if (eprofile->format == AST_GEOLOC_FORMAT_CIVIC_ADDRESS) { + info_node = geoloc_civicaddr_list_to_xml(resolved_location, ref_string); + } else { + info_node = geoloc_gml_list_to_xml(resolved_location, ref_string); + } + if (!info_node) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create XML from '%s' list\n", + ref_string, geoloc_format_to_name(eprofile->format)); + } + if (!ast_xml_add_child(loc_node, info_node)) { + ast_xml_free_node(info_node); + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable add '%s' node to XML document\n", + ref_string, geoloc_format_to_name(eprofile->format)); + } + + rules_node = ast_xml_new_child(pidf_node, "usage-rules"); + if (!rules_node) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'usage-rules' XML node\n", + ref_string); + } + resolved_usage = geoloc_eprofile_resolve_varlist(eprofile->usage_rules, + eprofile->location_variables, chan); + for (var = resolved_usage; var; var = var->next) { + struct ast_xml_node *ur = ast_xml_new_child(rules_node, var->name); + ast_xml_set_text(ur, var->value); + } + + if (!ast_strlen_zero(eprofile->method)) { + method_node = ast_xml_new_child(pidf_node, "method"); + if (!method_node) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'method' XML node\n", + ref_string); + } + ast_xml_set_text(method_node, eprofile->method); + }; + + if (!ast_strlen_zero(eprofile->notes)) { + notes_node = ast_xml_new_child(pidf_node, "note-well"); + if (!notes_node) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'note-well' XML node\n", + ref_string); + } + ast_xml_set_text(notes_node, eprofile->notes); + }; + + gmtime_r(&tv.tv_sec, &tm); + strftime(timestr, sizeof(timestr), "%FT%TZ", &tm); + timestamp_node = ast_xml_new_child(pidf_node, "timestamp"); + if (!timestamp_node) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'timestamp' XML node\n", + ref_string); + } + ast_xml_set_text(timestamp_node, timestr); + + rtn_pidf_node = pidf_node; + pidf_node = NULL; + SCOPE_EXIT_RTN_VALUE(rtn_pidf_node, "%s: Done\n", ref_string); +} + +#define CREATE_NODE_LIST(node) \ + if (!node) { \ + node = ast_xml_new_child(root_node, \ + geoloc_pidf_element_to_name(eprofile->pidf_element)); \ + if (!pidfs[eprofile->pidf_element]) { \ + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create pidf '%s' XML node\n", \ + ref_string, geoloc_pidf_element_to_name(eprofile->pidf_element)); \ + } \ + } + +const char *ast_geoloc_eprofiles_to_pidf(struct ast_datastore *ds, + struct ast_channel *chan, struct ast_str **buf, const char * ref_string) +{ + RAII_VAR(struct ast_xml_doc *, intermediate, NULL, ast_xml_close); + RAII_VAR(struct ast_xml_doc *, pidf_doc, NULL, ast_xml_close); + struct ast_xml_node *root_node; + struct ast_xml_node *pidfs[AST_PIDF_ELEMENT_LAST] = {NULL, }; + struct ast_geoloc_eprofile *eprofile; + int eprofile_count = 0; + int i; + RAII_VAR(char *, doc_str, NULL, ast_xml_free_text); + int doc_len; + int rc = 0; + SCOPE_ENTER(3, "%s\n", ref_string); + + if (!ds || !chan || !buf || !*buf || ast_strlen_zero(ref_string)) { + SCOPE_EXIT_RTN_VALUE(NULL, "%s: Either or both datastore or chan were NULL\n", + ref_string); + } + + intermediate = ast_xml_new(); + if (!intermediate) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create XML document\n", ref_string); + } + root_node = ast_xml_new_node("presence"); + if (!root_node) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create root XML node\n", ref_string); + } + ast_xml_set_root(intermediate, root_node); + + eprofile_count = ast_geoloc_datastore_size(ds); + for (i = 0; i < eprofile_count; i++) { + struct ast_xml_node *temp_node = NULL; + struct ast_xml_node *curr_loc = NULL; + struct ast_xml_node *new_loc = NULL; + struct ast_xml_node *new_loc_child = NULL; + struct ast_xml_node *new_loc_child_dup = NULL; + eprofile = ast_geoloc_datastore_get_eprofile(ds, i); + if (eprofile->format == AST_GEOLOC_FORMAT_URI) { + continue; + } + + if (ast_strlen_zero(ast_xml_get_attribute(root_node, "entity"))) { + rc = ast_xml_set_attribute(root_node, "entity", eprofile->id); + if (rc != 0) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to set 'entity' XML attribute\n", ref_string); + } + } + + temp_node = geoloc_eprofile_to_intermediate(geoloc_pidf_element_to_name(eprofile->pidf_element), + eprofile, chan, ref_string); + + if (!pidfs[eprofile->pidf_element]) { + pidfs[eprofile->pidf_element] = temp_node; + ast_xml_add_child(root_node, temp_node); + continue; + } + + curr_loc = ast_xml_find_child_element(pidfs[eprofile->pidf_element], "location-info", NULL, NULL); + new_loc = ast_xml_find_child_element(temp_node, "location-info", NULL, NULL); + new_loc_child = ast_xml_node_get_children(new_loc); + new_loc_child_dup = ast_xml_copy_node_list(new_loc_child); + ast_xml_add_child_list(curr_loc, new_loc_child_dup); + + ast_xml_free_node(temp_node); + } + + ast_xml_doc_dump_memory(intermediate, &doc_str, &doc_len); + if (doc_len == 0 || !doc_str) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to dump intermediate doc to string\n", + ref_string); + } + + ast_trace(5, "Intermediate doc:\n%s\n", doc_str); + + pidf_doc = ast_xslt_apply(eprofile_to_pidf_xslt, intermediate, NULL); + if (!pidf_doc) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create final PIDF-LO doc from intermediate doc:\n%s\n", + ref_string, doc_str); + } + + ast_xml_doc_dump_memory(pidf_doc, &doc_str, &doc_len); + if (doc_len == 0 || !doc_str) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to dump final PIDF-LO doc to string\n", + ref_string); + } + + rc = ast_str_set(buf, 0, "%s", doc_str); + if (rc <= 0) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to extend buffer (%d)\n", + ref_string, rc); + } + + ast_trace(5, "Final doc:\n%s\n", ast_str_buffer(*buf)); + + SCOPE_EXIT_RTN_VALUE(ast_str_buffer(*buf), "%s: Done\n", ref_string); +} + +#ifdef TEST_FRAMEWORK +static void load_tests(void); +static void unload_tests(void); +#else +static void load_tests(void) {} +static void unload_tests(void) {} +#endif + + +int geoloc_eprofile_unload(void) +{ + unload_tests(); + if (pidf_to_eprofile_xslt) { + ast_xslt_close(pidf_to_eprofile_xslt); + } + + if (eprofile_to_pidf_xslt) { + ast_xslt_close(eprofile_to_pidf_xslt); + } + + if (geoloc_sorcery) { + ast_sorcery_unref(geoloc_sorcery); + } + + return AST_MODULE_LOAD_SUCCESS; +} + +int geoloc_eprofile_load(void) +{ + pidf_to_eprofile_xslt_size = + (_binary_res_geolocation_pidf_to_eprofile_xslt_end - _binary_res_geolocation_pidf_to_eprofile_xslt_start); + + pidf_lo_test_xml_size = + (_binary_res_geolocation_pidf_lo_test_xml_end - _binary_res_geolocation_pidf_lo_test_xml_start); + + pidf_to_eprofile_xslt = ast_xslt_read_memory( + (char *)_binary_res_geolocation_pidf_to_eprofile_xslt_start, pidf_to_eprofile_xslt_size); + if (!pidf_to_eprofile_xslt) { + ast_log(LOG_ERROR, "Unable to read pidf_to_eprofile_xslt from memory\n"); + return AST_MODULE_LOAD_DECLINE; + } + + eprofile_to_pidf_xslt_size = + (_binary_res_geolocation_eprofile_to_pidf_xslt_end - _binary_res_geolocation_eprofile_to_pidf_xslt_start); + + eprofile_to_pidf_xslt = ast_xslt_read_memory( + (char *)_binary_res_geolocation_eprofile_to_pidf_xslt_start, eprofile_to_pidf_xslt_size); + if (!eprofile_to_pidf_xslt) { + ast_log(LOG_ERROR, "Unable to read eprofile_to_pidf_xslt from memory\n"); +// geoloc_eprofile_unload(); + return AST_MODULE_LOAD_DECLINE; + } + + geoloc_sorcery = geoloc_get_sorcery(); + + load_tests(); + + return AST_MODULE_LOAD_SUCCESS; +} + +int geoloc_eprofile_reload(void) +{ + return AST_MODULE_LOAD_SUCCESS; +} + + +#ifdef TEST_FRAMEWORK +#include "asterisk/test.h" + +AST_TEST_DEFINE(test_create_from_uri) +{ + + RAII_VAR(struct ast_geoloc_eprofile *, eprofile, NULL, ao2_cleanup); + const char *uri = NULL; + int rc = AST_TEST_PASS; + + switch (cmd) { + case TEST_INIT: + info->name = "create_from_uri"; + info->category = "/geoloc/"; + info->summary = "Test create from uri"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + eprofile = ast_geoloc_eprofile_create_from_uri("http://some_uri&a=b", __func__); + ast_test_validate(test, eprofile != NULL); + ast_test_validate(test, eprofile->format == AST_GEOLOC_FORMAT_URI); + ast_test_validate(test, eprofile->location_info != NULL); + uri = ast_variable_find_in_list(eprofile->location_info, "URI"); + ast_test_validate(test, uri != NULL); + ast_test_validate(test, strcmp(uri, "http://some_uri&a=b") == 0); + + return rc; +} + +static enum ast_test_result_state validate_eprofile(struct ast_test *test, + struct ast_xml_doc * pidf_xmldoc, + const char *path, + const char *id, + enum ast_geoloc_pidf_element pidf_element, + enum ast_geoloc_format format, + const char *method, + const char *location, + const char *usage + ) +{ + RAII_VAR(struct ast_str *, str, NULL, ast_free); + RAII_VAR(struct ast_geoloc_eprofile *, eprofile, NULL, ao2_cleanup); + RAII_VAR(struct ast_xml_doc *, result_doc, NULL, ast_xml_close); + const char *search[] = { "path", path, NULL }; + + if (!ast_strlen_zero(path)) { + result_doc = ast_xslt_apply(pidf_to_eprofile_xslt, pidf_xmldoc, (const char **)search); + ast_test_validate(test, (result_doc && ast_xml_node_get_children((struct ast_xml_node *)result_doc))); + + eprofile = geoloc_eprofile_create_from_xslt_result(result_doc, "test_create_from_xslt"); + } else { + eprofile = ast_geoloc_eprofile_create_from_pidf(pidf_xmldoc, NULL, "test_create_from_pidf"); + } + + ast_test_validate(test, eprofile != NULL); + ast_test_status_update(test, "ID: '%s' pidf_element: '%s' format: '%s' method: '%s'\n", eprofile->id, + geoloc_pidf_element_to_name(eprofile->pidf_element), + geoloc_format_to_name(eprofile->format), + eprofile->method); + + ast_test_validate(test, ast_strings_equal(eprofile->id, id)); + ast_test_validate(test, eprofile->pidf_element == pidf_element); + ast_test_validate(test, eprofile->format == format); + ast_test_validate(test, ast_strings_equal(eprofile->method, method)); + + str = ast_variable_list_join(eprofile->location_info, ",", "=", NULL, NULL); + ast_test_validate(test, str != NULL); + ast_test_status_update(test, "location_vars expected: %s\n", location); + ast_test_status_update(test, "location_vars received: %s\n", ast_str_buffer(str)); + ast_test_validate(test, ast_strings_equal(ast_str_buffer(str), location)); + ast_free(str); + + str = ast_variable_list_join(eprofile->usage_rules, ",", "=", "'", NULL); + ast_test_validate(test, str != NULL); + ast_test_status_update(test, "usage_rules expected: %s\n", usage); + ast_test_status_update(test, "usage_rules received: %s\n", ast_str_buffer(str)); + ast_test_validate(test, ast_strings_equal(ast_str_buffer(str), usage)); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(test_create_from_pidf) +{ + + RAII_VAR(struct ast_xml_doc *, pidf_xmldoc, NULL, ast_xml_close); + enum ast_test_result_state res = AST_TEST_PASS; + + switch (cmd) { + case TEST_INIT: + info->name = "create_from_pidf"; + info->category = "/geoloc/"; + info->summary = "Test create from pidf scenarios"; + info->description = info->summary; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + pidf_xmldoc = ast_xml_read_memory((char *)_binary_res_geolocation_pidf_lo_test_xml_start, pidf_lo_test_xml_size); + ast_test_validate(test, pidf_xmldoc != NULL); + + res = validate_eprofile(test, pidf_xmldoc, + NULL, + "arcband-2d", + AST_PIDF_ELEMENT_DEVICE, + AST_GEOLOC_FORMAT_GML, + "TA-NMR", + "shape=ArcBand,crs=2d,pos=-43.5723 153.21760,innerRadius=3594," + "outerRadius=4148,startAngle=20 radians,openingAngle=20 radians", + "retransmission-allowed='yes',ruleset-preference='https:/www/more.com'," + "retention-expires='2007-06-22T20:57:29Z'" + ); + ast_test_validate(test, res == AST_TEST_PASS); + + + res = validate_eprofile(test, pidf_xmldoc, + "/def:presence/dm:device[.//ca:civicAddress][1]", + "pres:alice@asterisk.org", + AST_PIDF_ELEMENT_DEVICE, + AST_GEOLOC_FORMAT_CIVIC_ADDRESS, + "GPS", + "lang=en-AU,country=AU,A1=NSW,A3=Wollongong,A4=North Wollongong," + "RD=Flinders,STS=Street,RDBR=Campbell Street,LMK=Gilligan's Island," + "LOC=Corner,NAM=Video Rental Store,PC=2500,ROOM=Westerns and Classics," + "PLC=store,POBOX=Private Box 15", + "retransmission-allowed='yes',ruleset-preference='https:/www/more.com'," + "retention-expires='2007-06-22T20:57:29Z'" + ); + ast_test_validate(test, res == AST_TEST_PASS); + + + return res; +} + +static void load_tests(void) { + AST_TEST_REGISTER(test_create_from_uri); + AST_TEST_REGISTER(test_create_from_pidf); +} +static void unload_tests(void) { + AST_TEST_UNREGISTER(test_create_from_uri); + AST_TEST_UNREGISTER(test_create_from_pidf); +} + +#endif diff --git a/res/res_geolocation/geoloc_gml.c b/res/res_geolocation/geoloc_gml.c new file mode 100644 index 0000000000000000000000000000000000000000..8a661622072ae0a7bb41db9c97c265231698796c --- /dev/null +++ b/res/res_geolocation/geoloc_gml.c @@ -0,0 +1,362 @@ +/* + * 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. + */ + +#include "asterisk.h" +#include "asterisk/config.h" +#include "asterisk/cli.h" +#include "asterisk/res_geolocation.h" +#include "geoloc_private.h" + + +#if 1 //not used yet. +enum geoloc_shape_attrs { + GEOLOC_SHAPE_ATTR_POS = 0, + GEOLOC_SHAPE_ATTR_POS3D, + GEOLOC_SHAPE_ATTR_RADIUS, + GEOLOC_SHAPE_ATTR_SEMI_MAJOR_AXIS, + GEOLOC_SHAPE_ATTR_SEMI_MINOR_AXIS, + GEOLOC_SHAPE_ATTR_VERTICAL_AXIS, + GEOLOC_SHAPE_ATTR_HEIGHT, + GEOLOC_SHAPE_ATTR_ORIENTATION, + GEOLOC_SHAPE_ATTR_ORIENTATION_UOM, + GEOLOC_SHAPE_ATTR_INNER_RADIUS, + GEOLOC_SHAPE_ATTR_OUTER_RADIUS, + GEOLOC_SHAPE_ATTR_STARTING_ANGLE, + GEOLOC_SHAPE_ATTR_OPENING_ANGLE, + GEOLOC_SHAPE_ATTR_ANGLE_UOM, +}; + +struct geoloc_gml_attr_def { + enum geoloc_shape_attrs attr; + const char *name; + int (*validator)(const char *value); + int (*transformer)(struct ast_variable *value); +}; + +struct geoloc_gml_attr_def gml_attr_defs[] = { + { GEOLOC_SHAPE_ATTR_POS, "pos", NULL, NULL}, + { GEOLOC_SHAPE_ATTR_POS3D,"pos3d", NULL, NULL}, + { GEOLOC_SHAPE_ATTR_RADIUS,"radius", NULL, NULL}, + { GEOLOC_SHAPE_ATTR_SEMI_MAJOR_AXIS,"semiMajorAxis", NULL, NULL}, + { GEOLOC_SHAPE_ATTR_SEMI_MINOR_AXIS,"semiMinorAxis", NULL, NULL}, + { GEOLOC_SHAPE_ATTR_VERTICAL_AXIS,"verticalAxis", NULL, NULL}, + { GEOLOC_SHAPE_ATTR_HEIGHT,"height", NULL, NULL}, + { GEOLOC_SHAPE_ATTR_ORIENTATION,"orientation", NULL, NULL}, + { GEOLOC_SHAPE_ATTR_ORIENTATION_UOM,"orientation_uom", NULL, NULL}, + { GEOLOC_SHAPE_ATTR_INNER_RADIUS,"innerRadius", NULL, NULL}, + { GEOLOC_SHAPE_ATTR_OUTER_RADIUS,"outerRadius", NULL, NULL}, + { GEOLOC_SHAPE_ATTR_STARTING_ANGLE,"startingAngle", NULL, NULL}, + { GEOLOC_SHAPE_ATTR_OPENING_ANGLE,"openingAngle", NULL, NULL}, + { GEOLOC_SHAPE_ATTR_ANGLE_UOM,"angle_uom", NULL, NULL}, +}; +#endif //not used yet. + +struct geoloc_gml_attr { + const char *attribute; + int min_required; + int max_allowed; + int (*validator)(const char *value); +}; + +struct geoloc_gml_shape_def { + const char *shape_type; + struct geoloc_gml_attr required_attributes[8]; +}; + +static int pos_validator(const char *value) +{ + float lat; + float lon; + return (sscanf(value, "%f %f", &lat, &lon) == 2); +} + +static int pos3d_validator(const char *value) +{ + float lat; + float lon; + float alt; + return (sscanf(value, "%f %f %f", &lat, &lon, &alt) == 3); +} + +static int float_validator(const char *value) +{ + float val; + return (sscanf(value, "%f", &val) == 1); +} + +static int uom_validator(const char *value) +{ + return (ast_strings_equal(value, "degrees") || ast_strings_equal(value, "radians")); +} + + +static struct geoloc_gml_shape_def gml_shape_defs[8] = { + { "Point", { {"pos", 1, 1, pos_validator}, {NULL, -1, -1} }}, + { "Polygon", { {"pos", 3, -1, pos_validator}, {NULL, -1, -1} }}, + { "Circle", { {"pos", 1, 1, pos_validator}, {"radius", 1, 1, float_validator},{NULL, -1, -1}}}, + { "Ellipse", { {"pos", 1, 1, pos_validator}, {"semiMajorAxis", 1, 1, float_validator}, + {"semiMinorAxis", 1, 1, float_validator}, {"orientation", 1, 1, float_validator}, + {"orientation_uom", 1, 1, uom_validator}, {NULL, -1, -1} }}, + { "ArcBand", { {"pos", 1, 1, pos_validator}, {"innerRadius", 1, 1, float_validator}, + {"outerRadius", 1, 1, float_validator}, {"startAngle", 1, 1, float_validator}, + {"startAngle_uom", 1, 1, uom_validator}, {"openingAngle", 1, 1, float_validator}, + {"openingAngle_uom", 1, 1, uom_validator}, {NULL, -1, -1} }}, + { "Sphere", { {"pos3d", 1, 1, pos3d_validator}, {"radius", 1, 1, float_validator}, {NULL, -1, -1} }}, + { "Ellipse", { {"pos3d", 1, 1, pos3d_validator}, {"semiMajorAxis", 1, 1, float_validator}, + {"semiMinorAxis", 1, 1, float_validator}, {"verticalAxis", 1, 1, float_validator}, + {"orientation", 1, 1, float_validator}, {"orientation_uom", 1, 1, uom_validator}, {NULL, -1, -1} }}, + { "Prism", { {"pos3d", 3, -1, pos_validator}, {"height", 1, 1, float_validator}, {NULL, -1, -1} }}, +}; + +enum ast_geoloc_validate_result ast_geoloc_gml_validate_varlist(const struct ast_variable *varlist, + const char **result) +{ + int def_index = -1; + const struct ast_variable *var; + int i; + const char *shape_type = ast_variable_find_in_list(varlist, "shape"); + + if (!shape_type) { + return AST_GEOLOC_VALIDATE_MISSING_SHAPE; + } + + for (i = 0; i < ARRAY_LEN(gml_shape_defs); i++) { + if (ast_strings_equal(gml_shape_defs[i].shape_type, shape_type)) { + def_index = i; + } + } + if (def_index < 0) { + return AST_GEOLOC_VALIDATE_INVALID_SHAPE; + } + + for (var = varlist; var; var = var->next) { + int vname_index = -1; + if (ast_strings_equal("shape", var->name)) { + continue; + } + for (i = 0; i < ARRAY_LEN(gml_shape_defs[def_index].required_attributes); i++) { + if (gml_shape_defs[def_index].required_attributes[i].attribute == NULL) { + break; + } + if (ast_strings_equal(gml_shape_defs[def_index].required_attributes[i].attribute, var->name)) { + vname_index = i; + break; + } + } + if (vname_index < 0) { + *result = var->name; + return AST_GEOLOC_VALIDATE_INVALID_VARNAME; + } + if (!gml_shape_defs[def_index].required_attributes[vname_index].validator(var->value)) { + *result = var->name; + return AST_GEOLOC_VALIDATE_INVALID_VALUE; + } + } + + for (i = 0; i < ARRAY_LEN(gml_shape_defs[def_index].required_attributes); i++) { + int count = 0; + if (gml_shape_defs[def_index].required_attributes[i].attribute == NULL) { + break; + } + + for (var = varlist; var; var = var->next) { + if (ast_strings_equal(gml_shape_defs[def_index].required_attributes[i].attribute, var->name)) { + count++; + } + } + if (count < gml_shape_defs[def_index].required_attributes[i].min_required) { + *result = gml_shape_defs[def_index].required_attributes[i].attribute; + return AST_GEOLOC_VALIDATE_NOT_ENOUGH_VARNAMES; + } + if (gml_shape_defs[def_index].required_attributes[i].max_allowed > 0 && + count > gml_shape_defs[def_index].required_attributes[i].max_allowed) { + *result = gml_shape_defs[def_index].required_attributes[i].attribute; + return AST_GEOLOC_VALIDATE_TOO_MANY_VARNAMES; + } + } + return AST_GEOLOC_VALIDATE_SUCCESS; +} + +static char *handle_gml_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + int i; + + switch (cmd) { + case CLI_INIT: + e->command = "geoloc show gml_shape_defs"; + e->usage = + "Usage: geoloc show gml_shape_defs\n" + " Show the GML Shape definitions.\n"; + return NULL; + case CLI_GENERATE: + return NULL; + } + + ast_cli(a->fd, "%-16s %-32s\n", "Shape", "Attributes name(min,max)"); + ast_cli(a->fd, "================ ===============================\n"); + + for (i = 0; i < ARRAY_LEN(gml_shape_defs); i++) { + int j; + ast_cli(a->fd, "%-16s", gml_shape_defs[i].shape_type); + for (j = 0; j < ARRAY_LEN(gml_shape_defs[i].required_attributes); j++) { + if (gml_shape_defs[i].required_attributes[j].attribute == NULL) { + break; + } + if (gml_shape_defs[i].required_attributes[j].max_allowed >= 0) { + ast_cli(a->fd, " %s(%d,%d)", gml_shape_defs[i].required_attributes[j].attribute, + gml_shape_defs[i].required_attributes[j].min_required, + gml_shape_defs[i].required_attributes[j].max_allowed); + } else { + ast_cli(a->fd, " %s(%d,unl)", gml_shape_defs[i].required_attributes[j].attribute, + gml_shape_defs[i].required_attributes[j].min_required); + } + } + ast_cli(a->fd, "\n"); + } + ast_cli(a->fd, "\n"); + + return CLI_SUCCESS; +} + +static struct ast_cli_entry geoloc_gml_cli[] = { + AST_CLI_DEFINE(handle_gml_show, "Show the GML Shape definitions"), +}; + +struct ast_xml_node *geoloc_gml_list_to_xml(const struct ast_variable *resolved_location, + const char *ref_string) +{ + const char *shape; + char *crs; + struct ast_variable *var; + struct ast_xml_node *gml_node; + struct ast_xml_node *child_node; + int rc = 0; + + SCOPE_ENTER(3, "%s", ref_string); + + shape = ast_variable_find_in_list(resolved_location, "shape"); + if (ast_strlen_zero(shape)) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: There's no 'shape' parameter\n", + ref_string); + } + crs = (char *)ast_variable_find_in_list(resolved_location, "crs"); + if (ast_strlen_zero(crs)) { + crs = "2d"; + } + + gml_node = ast_xml_new_node(shape); + if (!gml_node) { + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create '%s' XML node\n", shape, ref_string); + } + rc = ast_xml_set_attribute(gml_node, "crs", crs); + if (rc != 0) { + ast_xml_free_node(gml_node); + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'crs' XML attribute\n", ref_string); + } + + for (var = (struct ast_variable *)resolved_location; var; var = var->next) { + RAII_VAR(char *, value, NULL, ast_free); + char *uom = NULL; + + if (ast_strings_equal(var->name, "shape") || ast_strings_equal(var->name, "crs")) { + continue; + } + value = ast_strdup(var->value); + + if (ast_strings_equal(var->name, "orientation") || ast_strings_equal(var->name, "startAngle") + || ast_strings_equal(var->name, "openingAngle")) { + char *a = NULL; + char *junk = NULL; + float angle; + uom = value; + + /* 'a' should now be the angle and 'uom' should be the uom */ + a = strsep(&uom, " "); + angle = strtof(a, &junk); + /* + * strtof sets junk to the first non-valid character so if it's + * not empty after the conversion, there were unrecognized + * characters in the angle. It'll point to the NULL terminator + * if angle was completely converted. + */ + if (!ast_strlen_zero(junk)) { + ast_xml_free_node(gml_node); + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: The angle portion of parameter '%s' ('%s') is malformed\n", + ref_string, var->name, var->value); + } + + if (ast_strlen_zero(uom)) { + uom = "degrees"; + } + + if (ast_begins_with(uom, "deg")) { + if (angle > 360.0) { + ast_xml_free_node(gml_node); + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Parameter '%s': '%s' is malformed. " + "Degrees can't be > 360.0\n", + ref_string, var->name, var->value); + } + } else if (ast_begins_with(uom, "rad")) { + if(angle > 100.0) { + ast_xml_free_node(gml_node); + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Parameter '%s': '%s' is malformed. " + "Radians can't be > 100.0\n", + ref_string, var->name, var->value); + } + } else { + ast_xml_free_node(gml_node); + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Parameter '%s': '%s' is malformed. " + "The unit of measure must be 'deg[rees]' or 'rad[ians]'\n", + ref_string, var->name, var->value); + } + } + + child_node = ast_xml_new_child(gml_node, var->name); + if (!child_node) { + ast_xml_free_node(gml_node); + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create '%s' XML node\n", var->name, ref_string); + } + if (!ast_strlen_zero(uom)) { + rc = ast_xml_set_attribute(child_node, "uom", uom); + if (rc != 0) { + ast_xml_free_node(gml_node); + SCOPE_EXIT_LOG_RTN_VALUE(NULL, LOG_ERROR, "%s: Unable to create 'uom' XML attribute\n", ref_string); + } + } + ast_xml_set_text(child_node, value); + } + + SCOPE_EXIT_RTN_VALUE(gml_node, "%s: Done\n", ref_string); +} + +int geoloc_gml_unload(void) +{ + ast_cli_unregister_multiple(geoloc_gml_cli, ARRAY_LEN(geoloc_gml_cli)); + + return AST_MODULE_LOAD_SUCCESS; +} + +int geoloc_gml_load(void) +{ + ast_cli_register_multiple(geoloc_gml_cli, ARRAY_LEN(geoloc_gml_cli)); + + return AST_MODULE_LOAD_SUCCESS; +} + +int geoloc_gml_reload(void) +{ + return AST_MODULE_LOAD_SUCCESS; +} diff --git a/res/res_geolocation/geoloc_private.h b/res/res_geolocation/geoloc_private.h new file mode 100644 index 0000000000000000000000000000000000000000..860f3b5e596d88d6f15025c5d2601159b0ad1e7d --- /dev/null +++ b/res/res_geolocation/geoloc_private.h @@ -0,0 +1,168 @@ +/* + * 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. + */ + +#ifndef GEOLOC_PRIVATE_H_ +#define GEOLOC_PRIVATE_H_ + +#include "asterisk/module.h" +#include "asterisk/config.h" +#include "asterisk/sorcery.h" +#include "asterisk/lock.h" +#include "asterisk/res_geolocation.h" + +#define CONFIG_STR_TO_ENUM_DECL(_stem) int geoloc_ ## _stem ## _str_to_enum(const char *str); +CONFIG_STR_TO_ENUM_DECL(pidf_element) +CONFIG_STR_TO_ENUM_DECL(format); +CONFIG_STR_TO_ENUM_DECL(action); +#define GEOLOC_ENUM_TO_NAME_DECL(_stem) const char * geoloc_ ## _stem ## _to_name(int ix); +GEOLOC_ENUM_TO_NAME_DECL(pidf_element) +GEOLOC_ENUM_TO_NAME_DECL(format); +GEOLOC_ENUM_TO_NAME_DECL(action); + + +#define CONFIG_STR_TO_ENUM(_stem) \ +int geoloc_ ## _stem ## _str_to_enum(const char *str) \ +{ \ + int i; \ + for (i = 0; i < ARRAY_LEN(_stem ## _names); i++) { \ + if (ast_strings_equal(str, _stem ## _names[i])) { \ + return i; \ + } \ + } \ + return -1; \ +} + +#define CONFIG_ENUM_HANDLER(_object, _stem) \ +static int _stem ## _handler(const struct aco_option *opt, struct ast_variable *var, void *obj) \ +{ \ + struct ast_geoloc_ ## _object *_thisobject = obj; \ + int enumval = geoloc_ ## _stem ## _str_to_enum(var->value); \ + if (enumval == -1) { \ + return -1; \ + } \ + _thisobject->_stem = enumval; \ + return 0; \ +} + + +#define GEOLOC_ENUM_TO_NAME(_stem) \ +const char * geoloc_ ## _stem ## _to_name(int ix) \ +{ \ + if (!ARRAY_IN_BOUNDS(ix, _stem ## _names)) { \ + return "none"; \ + } else { \ + return _stem ## _names[ix]; \ + } \ +} + +#define CONFIG_ENUM_TO_STR(_object, _stem) \ +static int _stem ## _to_str(const void *obj, const intptr_t *args, char **buf) \ +{ \ + const struct ast_geoloc_ ## _object *_thisobject = obj; \ + if (!ARRAY_IN_BOUNDS(_thisobject->_stem, _stem ## _names)) { \ + *buf = ast_strdup("none"); \ + } else { \ + *buf = ast_strdup(_stem ## _names[_thisobject->_stem]); \ + } \ + return 0; \ +} + +#define CONFIG_ENUM(_object, _stem) \ +CONFIG_STR_TO_ENUM(_stem) \ +GEOLOC_ENUM_TO_NAME(_stem) \ +CONFIG_ENUM_HANDLER(_object, _stem) \ +CONFIG_ENUM_TO_STR(_object, _stem) + +#define CONFIG_VAR_LIST_HANDLER(_object, _stem) \ +static int _stem ## _handler(const struct aco_option *opt, struct ast_variable *var, void *obj) \ +{ \ + struct ast_geoloc_ ## _object *_thisobject = obj; \ + struct ast_variable *new_var; \ + char *item_string, *item, *item_name, *item_value; \ + int rc = 0;\ + if (ast_strlen_zero(var->value)) { return 0; } \ + item_string = ast_strdupa(var->value); \ + while ((item = ast_strsep(&item_string, ',', AST_STRSEP_ALL))) { \ + item_name = ast_strsep(&item, '=', AST_STRSEP_ALL); \ + item_value = ast_strsep(&item, '=', AST_STRSEP_ALL); \ + new_var = ast_variable_new(item_name, item_value, ""); \ + if (!new_var) { \ + rc = -1; \ + break; \ + } \ + ast_variable_list_append(&_thisobject->_stem, new_var); \ + } \ + return rc; \ +} + +#define CONFIG_VAR_LIST_DUP(_object, _stem) \ +static int _stem ## _dup(const void *obj, struct ast_variable **fields) \ +{ \ + const struct ast_geoloc_ ## _object *_thisobject = obj; \ + if (_thisobject->_stem) { \ + *fields = ast_variables_dup(_thisobject->_stem); \ + } \ + return 0; \ +} + +#define CONFIG_VAR_LIST_TO_STR(_object, _stem) \ +static int _stem ## _to_str(const void *obj, const intptr_t *args, char **buf) \ +{ \ + const struct ast_geoloc_ ## _object *_thisobject = obj; \ + struct ast_str *str = ast_variable_list_join(_thisobject->_stem, ",", "=", "\"", NULL); \ + *buf = ast_strdup(ast_str_buffer(str)); \ + ast_free(str); \ + return 0; \ +} + +#define CONFIG_VAR_LIST(_object, _stem) \ +CONFIG_VAR_LIST_HANDLER(_object, _stem) \ +CONFIG_VAR_LIST_DUP(_object, _stem) \ +CONFIG_VAR_LIST_TO_STR(_object, _stem) + +int geoloc_config_load(void); +int geoloc_config_reload(void); +int geoloc_config_unload(void); + +struct ast_xml_node *geoloc_civicaddr_list_to_xml(const struct ast_variable *resolved_location, + const char *ref_string); +int geoloc_civicaddr_load(void); +int geoloc_civicaddr_unload(void); +int geoloc_civicaddr_reload(void); + +struct ast_xml_node *geoloc_gml_list_to_xml(const struct ast_variable *resolved_location, + const char *ref_string); +int geoloc_gml_unload(void); +int geoloc_gml_load(void); +int geoloc_gml_reload(void); + +int geoloc_dialplan_unload(void); +int geoloc_dialplan_load(void); +int geoloc_dialplan_reload(void); + +int geoloc_channel_unload(void); +int geoloc_channel_load(void); +int geoloc_channel_reload(void); + +int geoloc_eprofile_unload(void); +int geoloc_eprofile_load(void); +int geoloc_eprofile_reload(void); + +struct ast_sorcery *geoloc_get_sorcery(void); + +#endif /* GEOLOC_PRIVATE_H_ */ diff --git a/res/res_geolocation/pidf_lo_test.xml b/res/res_geolocation/pidf_lo_test.xml new file mode 100644 index 0000000000000000000000000000000000000000..39480636e95dfc66841fa87642d8f54c32842ad4 --- /dev/null +++ b/res/res_geolocation/pidf_lo_test.xml @@ -0,0 +1,312 @@ +<?xml version="1.0" encoding="UTF-8"?> +<presence entity="pres:alice@asterisk.org" + xmlns="urn:ietf:params:xml:ns:pidf" + xmlns:ca="urn:ietf:params:xml:ns:pidf:geopriv10:civicAddr" + xmlns:dm="urn:ietf:params:xml:ns:pidf:data-model" + xmlns:gbp="urn:ietf:params:xml:ns:pidf:geopriv10:basicPolicy" + xmlns:gml="http://www.opengis.net/gml" + xmlns:gp="urn:ietf:params:xml:ns:pidf:geopriv10" + xmlns:gs="http://www.opengis.net/pidflo/1.0"> + <tuple id="point-2d"> + <status> + <gp:geopriv> + <gp:location-info> + <gml:Point srsName="urn:ogc:def:crs:EPSG::4326"> + <gml:pos>-34.410649 150.87651</gml:pos> + </gml:Point> + </gp:location-info> + <gp:usage-rules> + <gbp:retransmission-allowed>no</gbp:retransmission-allowed> + <gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry> + </gp:usage-rules> + <gp:method>Manual</gp:method> + </gp:geopriv> + </status> + <timestamp>2007-06-22T20:57:29Z</timestamp> + </tuple> + <dm:person id="point-3d"> + <gp:geopriv> + <gp:location-info> + <gml:Point srsName="urn:ogc:def:crs:EPSG::4979"> + <gml:pos>-34.410649 150.87651 1800</gml:pos> + </gml:Point> + </gp:location-info> + <gp:usage-rules> + <gbp:retransmission-allowed>no</gbp:retransmission-allowed> + <gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry> + </gp:usage-rules> + <gp:method>802.11</gp:method> + </gp:geopriv> + <dm:timestamp>2007-06-24T12:28:04Z</dm:timestamp> + </dm:person> + <tuple id="circle-2d"> + <status> + <gp:geopriv> + <gp:location-info> + <gs:Circle srsName="urn:ogc:def:crs:EPSG::4326"> + <gml:pos>-34.410649 150.87651</gml:pos> + <gs:radius uom="urn:ogc:def:uom:EPSG::9001">30</gs:radius> + </gs:Circle> + </gp:location-info> + <gp:usage-rules> + <gbp:retransmission-allowed>no</gbp:retransmission-allowed> + <gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry> + </gp:usage-rules> + <gp:method>802.11</gp:method> + </gp:geopriv> + </status> + <timestamp>2007-06-22T20:57:29Z</timestamp> + </tuple> + <tuple id="circle-3d"> + <status> + <gp:geopriv> + <gp:location-info> + <gs:Circle srsName="urn:ogc:def:crs:EPSG::4979"> + <gml:pos>-34.410649 150.87651 1800</gml:pos> + <gs:radius uom="urn:ogc:def:uom:EPSG::9001">30</gs:radius> + </gs:Circle> + </gp:location-info> + <gp:usage-rules> + <gbp:retransmission-allowed>no</gbp:retransmission-allowed> + <gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry> + </gp:usage-rules> + <gp:method>802.11</gp:method> + </gp:geopriv> + </status> + <timestamp>2007-06-22T20:57:29Z</timestamp> + </tuple> + <dm:person id="polygon-2d"> + <gp:geopriv> + <gp:location-info> + <gml:Polygon srsName="urn:ogc:def:crs:EPSG::4326"> + <gml:exterior> + <gml:LinearRing> + <gml:pos>43.311 -73.422</gml:pos> + <gml:pos>43.111 -73.322</gml:pos> + <gml:pos>43.111 -73.222</gml:pos> + <gml:pos>43.311 -73.122</gml:pos> + <gml:pos>43.411 -73.222</gml:pos> + <gml:pos>43.411 -73.322</gml:pos> + <gml:pos>43.311 -73.422</gml:pos> + </gml:LinearRing> + </gml:exterior> + </gml:Polygon> + </gp:location-info> + <gp:usage-rules> + <gbp:retransmission-allowed>no</gbp:retransmission-allowed> + <gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry> + </gp:usage-rules> + <gp:method>802.11</gp:method> + </gp:geopriv> + <dm:timestamp>2007-06-24T12:28:04Z</dm:timestamp> + </dm:person> + <dm:person id="polygon-3d"> + <gp:geopriv> + <gp:location-info> + <gml:Polygon srsName="urn:ogc:def:crs:EPSG::4979"> + <gml:exterior> + <gml:LinearRing> + <gml:pos>43.311 -73.422 1800</gml:pos> + <gml:pos>43.111 -73.322 1800</gml:pos> + <gml:pos>43.111 -73.222 1800</gml:pos> + <gml:pos>43.311 -73.122 1800</gml:pos> + <gml:pos>43.411 -73.222 1800</gml:pos> + <gml:pos>43.411 -73.322 1800</gml:pos> + <gml:pos>43.311 -73.422 1800</gml:pos> + </gml:LinearRing> + </gml:exterior> + </gml:Polygon> + </gp:location-info> + <gp:usage-rules> + <gbp:retransmission-allowed>no</gbp:retransmission-allowed> + <gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry> + </gp:usage-rules> + <gp:method>802.11</gp:method> + </gp:geopriv> + <dm:timestamp>2007-06-24T12:28:04Z</dm:timestamp> + </dm:person> + <tuple id="polygon-poslist-2d"> + <status> + <gp:geopriv> + <gp:location-info> + <gml:Polygon srsName="urn:ogc:def:crs:EPSG::4326"> + <gml:exterior> + <gml:LinearRing> + <gml:posList> + 43.311 -73.422 43.111 -73.322 + 43.111 -73.222 43.311 -73.122 + 43.411 -73.222 43.411 -73.322 + 43.311 -73.422 + </gml:posList> + </gml:LinearRing> + </gml:exterior> + </gml:Polygon> + </gp:location-info> + <gp:usage-rules> + <gbp:retransmission-allowed>no</gbp:retransmission-allowed> + <gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry> + </gp:usage-rules> + <gp:method>Wiremap</gp:method> + </gp:geopriv> + </status> + <timestamp>2007-06-22T20:57:29Z</timestamp> + </tuple> + <tuple id="polygon-poslist-3d"> + <status> + <gp:geopriv> + <gp:location-info> + <gml:Polygon srsName="urn:ogc:def:crs:EPSG::4979"> + <gml:exterior> + <gml:LinearRing> + <gml:posList> + 43.311 -73.422 1800 43.111 -73.322 1800 + 43.111 -73.222 1800 43.311 -73.122 1800 + 43.411 -73.222 1800 43.411 -73.322 1800 + 43.311 -73.422 1800 + </gml:posList> + </gml:LinearRing> + </gml:exterior> + </gml:Polygon> + </gp:location-info> + <gp:usage-rules> + <gbp:retransmission-allowed>no</gbp:retransmission-allowed> + <gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry> + </gp:usage-rules> + <gp:method>Wiremap</gp:method> + </gp:geopriv> + </status> + <timestamp>2007-06-22T20:57:29Z</timestamp> + </tuple> + <tuple id="ellipse-2d"> + <status> + <gp:geopriv> + <gp:location-info> + <gs:Ellipse srsName="urn:ogc:def:crs:EPSG::4326"> + <gml:pos>42.5463 -73.2512</gml:pos> + <gs:semiMajorAxis uom="urn:ogc:def:uom:EPSG::9001">1275</gs:semiMajorAxis> + <gs:semiMinorAxis uom="urn:ogc:def:uom:EPSG::9001">670</gs:semiMinorAxis> + <gs:orientation uom="urn:ogc:def:uom:EPSG::9102">43.2</gs:orientation> + </gs:Ellipse> + </gp:location-info> + <gp:usage-rules/> + <gp:method>Device-Assisted_A-GPS</gp:method> + </gp:geopriv> + </status> + <timestamp>2007-06-22T20:57:29Z</timestamp> + </tuple> + <dm:device id="arcband-2d"> + <gp:geopriv> + <gp:location-info> + <gs:ArcBand srsName="urn:ogc:def:crs:EPSG::4326"> + <gml:pos>-43.5723 153.21760</gml:pos> + <gs:innerRadius uom="urn:ogc:def:uom:EPSG::9001">3594</gs:innerRadius> + <gs:outerRadius uom="urn:ogc:def:uom:EPSG::9001">4148</gs:outerRadius> + <gs:startAngle uom="urn:ogc:def:uom:EPSG::9102">20</gs:startAngle> + <gs:openingAngle uom="urn:ogc:def:uom:EPSG::9102">20</gs:openingAngle> + </gs:ArcBand> + </gp:location-info> + <gp:usage-rules> + <gp:retransmission-allowed>yes</gp:retransmission-allowed> + <gp:ruleset-preference>https:/www/more.com</gp:ruleset-preference> + <gp:retention-expires>2007-06-22T20:57:29Z</gp:retention-expires> + </gp:usage-rules> + <gp:method>TA-NMR</gp:method> + </gp:geopriv> + <dm:deviceID>mac:1234567890ab</dm:deviceID> + <dm:timestamp>2007-06-22T20:57:29Z</dm:timestamp> + </dm:device> + <tuple id="sphere"> + <status> + <gp:geopriv> + <gp:location-info> + <gs:Sphere srsName="urn:ogc:def:crs:EPSG::4979"> + <gml:pos>42.5463 -73.2512 26.3</gml:pos> + <gs:radius uom="urn:ogc:def:uom:EPSG::9001">850.24</gs:radius> + </gs:Sphere> + </gp:location-info> + <gp:usage-rules> + <gbp:retransmission-allowed>no</gbp:retransmission-allowed> + <gbp:retention-expiry>2010-11-14T20:00:00Z</gbp:retention-expiry> + </gp:usage-rules> + <gp:method>Device-Based_A-GPS</gp:method> + </gp:geopriv> + </status> + </tuple> + <tuple id="ellipsoid"> + <status> + <gp:geopriv> + <gp:location-info> + <gs:Ellipsoid srsName="urn:ogc:def:crs:EPSG::4979"> + <gml:pos>42.5463 -73.2512 26.3</gml:pos> + <gs:semiMajorAxis uom="urn:ogc:def:uom:EPSG::9001">7.7156</gs:semiMajorAxis> + <gs:semiMinorAxis uom="urn:ogc:def:uom:EPSG::9001">3.31</gs:semiMinorAxis> + <gs:verticalAxis uom="urn:ogc:def:uom:EPSG::9001">28.7</gs:verticalAxis> + <gs:orientation uom="urn:ogc:def:uom:EPSG::9102">90</gs:orientation> + </gs:Ellipsoid> + </gp:location-info> + <gp:usage-rules/> + <gp:method>Hybrid_A-GPS</gp:method> + </gp:geopriv> + </status> + <timestamp>2007-06-22T20:57:29Z</timestamp> + </tuple> + <tuple id="prism"> + <status> + <gp:geopriv> + <gp:location-info> + <gs:Prism srsName="urn:ogc:def:crs:EPSG::4979"> + <gs:base> + <gml:Polygon> + <gml:exterior> + <gml:LinearRing> + <gml:posList> + 42.556844 -73.248157 36.6 <!--A --> + 42.656844 -73.248157 36.6 <!--B --> + 42.656844 -73.348157 36.6 <!--C --> + 42.556844 -73.348157 36.6 <!--D --> + 42.556844 -73.248157 36.6 <!--A --> + </gml:posList> + </gml:LinearRing> + </gml:exterior> + </gml:Polygon> + </gs:base> + <gs:height uom="urn:ogc:def:uom:EPSG::9001">2.4</gs:height> + </gs:Prism> + </gp:location-info> + <gp:usage-rules/> + <gp:method>Wiremap</gp:method> + </gp:geopriv> + </status> + <timestamp>2007-06-22T20:57:29Z</timestamp> + </tuple> + <dm:device> + <gp:geopriv> + <gp:location-info> + <ca:civicAddress xml:lang="en-AU"> + <ca:country>AU</ca:country> + <ca:A1>NSW</ca:A1> + <ca:A3>Wollongong</ca:A3> + <ca:A4>North Wollongong</ca:A4> + <ca:RD>Flinders</ca:RD> + <ca:STS>Street</ca:STS> + <ca:RDBR>Campbell Street</ca:RDBR> + <ca:LMK>Gilligan's Island</ca:LMK> + <ca:LOC>Corner</ca:LOC> + <ca:NAM> Video Rental Store </ca:NAM> + <ca:PC>2500</ca:PC> + <ca:ROOM> Westerns and Classics </ca:ROOM> + <ca:PLC>store</ca:PLC> + <ca:POBOX>Private Box 15</ca:POBOX> + </ca:civicAddress> + </gp:location-info> + <gp:usage-rules> + <gp:retransmission-allowed>yes</gp:retransmission-allowed> + <gp:ruleset-preference>https:/www/more.com</gp:ruleset-preference> + <gp:retention-expires>2007-06-22T20:57:29Z</gp:retention-expires> + </gp:usage-rules> + <gp:method>GPS</gp:method> + </gp:geopriv> + <dm:deviceID>mac:1234567890ab</dm:deviceID> + <dm:timestamp>2007-06-22T20:57:29Z</dm:timestamp> + </dm:device> +</presence> diff --git a/res/res_geolocation/pidf_to_eprofile.xslt b/res/res_geolocation/pidf_to_eprofile.xslt new file mode 100644 index 0000000000000000000000000000000000000000..05f4df896d024259643733cda3ff529b645325fc --- /dev/null +++ b/res/res_geolocation/pidf_to_eprofile.xslt @@ -0,0 +1,212 @@ +<?xml version="1.0"?> +<xsl:stylesheet version="1.0" + xmlns:ca="urn:ietf:params:xml:ns:pidf:geopriv10:civicAddr" + xmlns:def="urn:ietf:params:xml:ns:pidf" + xmlns:dm="urn:ietf:params:xml:ns:pidf:data-model" + xmlns:fn="http://www.w3.org/2005/xpath-functions" + xmlns:gbp="urn:ietf:params:xml:ns:pidf:geopriv10:basicPolicy" + xmlns:gml="http://www.opengis.net/gml" + xmlns:gp="urn:ietf:params:xml:ns:pidf:geopriv10" + xmlns:gs="http://www.opengis.net/pidflo/1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + + +<!-- + The whole purpose of this stylesheet is to convert a PIDF-LO document into a simple, + common XML document that is easily parsable by geoloc_eprofile into an eprofile. + + For example: + + <presence> + <device> + <location-info format="GML">shape="Point", crs="2d", pos="38.456 -105.678"</location-info> + <usage-rules>retransmission-allowed=no</usage-rules> + <method>GPS</method> + </device> + </presence> + + WARNING: Don't mess with this stylesheet before brushing up your + XPath and XSLT expertise. +--> + + +<!-- + All of the namespaces that could be in the incoming PIDF-LO document + have to be declared above. All matching is done based on the URI, not + the prefix so we can use whatever prefixes we want. For instance, + even if "urn:ietf:params:xml:ns:pidf:data-model" were declared with + the "pdm" prefix in the incoming document and with "dm" here, + "dm:device" would match "pdm:device" in the document. +--> + + <xsl:output method="xml" indent="yes"/> + <xsl:strip-space elements="*"/> + <xsl:param name="path"/> + + <!-- + Even though the "presence", "tuple", and "status" elements won't have namespaces in the + incoming PIDF document, we have to use the pseudo-namespace "def" here because of namespace + processing quirks in libxml2 and libxslt. + + We don't use namespace prefixes in the output document at all. + --> + <xsl:template match="/def:presence"> + <xsl:element name="presence"> + <xsl:attribute name="entity"><xsl:value-of select="@entity"/></xsl:attribute> + <xsl:apply-templates select="$path"/> + </xsl:element> + </xsl:template> + + <xsl:template match="dm:device"> + <xsl:element name="device"> + <xsl:attribute name="id"><xsl:value-of select="@id"/></xsl:attribute> + <xsl:apply-templates select=".//gp:location-info"/> + <xsl:apply-templates select=".//gp:usage-rules"/> + <xsl:apply-templates select=".//gp:method"/> + <xsl:apply-templates select=".//gp:note-well"/> + <xsl:if test="./dm:timestamp"> + <timestamp> + <xsl:value-of select="./dm:timestamp"/> + </timestamp> + </xsl:if> + <xsl:if test="./dm:deviceID"> + <deviceID> + <xsl:value-of select="./dm:deviceID"/> + </deviceID> + </xsl:if> + </xsl:element> + </xsl:template> + + <xsl:template match="def:tuple"> + <xsl:element name="tuple"> + <xsl:attribute name="id"><xsl:value-of select="@id"/></xsl:attribute> + <xsl:apply-templates select=".//gp:location-info"/> + <xsl:apply-templates select=".//gp:usage-rules"/> + <xsl:apply-templates select=".//gp:method"/> + <xsl:apply-templates select=".//gp:note-well"/> + <xsl:if test="./timestamp"> + <timestamp> + <xsl:value-of select="./timestamp"/> + </timestamp> + </xsl:if> + </xsl:element> + </xsl:template> + + <xsl:template match="dm:person"> + <xsl:element name="person"> + <xsl:attribute name="id"><xsl:value-of select="@id"/></xsl:attribute> + <xsl:apply-templates select=".//gp:location-info"/> + <xsl:apply-templates select=".//gp:usage-rules"/> + <xsl:apply-templates select=".//gp:method"/> + <xsl:apply-templates select=".//gp:note-well"/> + <xsl:if test="./dm:timestamp"> + <timestamp> + <xsl:value-of select="./dm:timestamp"/> + </timestamp> + </xsl:if> + </xsl:element> + </xsl:template> + + <xsl:template match="gp:location-info/gml:*"> + <xsl:element name="location-info"> + <xsl:attribute name="format">gml</xsl:attribute> + <xsl:call-template name="shape" /> + </xsl:element> + </xsl:template> + + <xsl:template match="gp:location-info/gs:*"> + <xsl:element name="location-info"> + <xsl:attribute name="format">gml</xsl:attribute> + <xsl:call-template name="shape" /> + </xsl:element> + </xsl:template> + + <xsl:template match="gp:location-info/ca:civicAddress"> + <xsl:element name="location-info"> + <xsl:attribute name="format">civicAddress</xsl:attribute> + <xsl:call-template name="civicAddress" /> + </xsl:element> + </xsl:template> + + <!-- + All of the "following-sibling" things just stick a comma after the value if there's another + element after it. The result should be... + + name1="value1", name2="value2" + --> + <xsl:template name="name-value"> + <xsl:element name="{local-name(.)}"> + <xsl:value-of select="normalize-space(.)"/> + </xsl:element> + </xsl:template> + + <xsl:template name="length"><xsl:call-template name="name-value" /></xsl:template> + + <xsl:template name="angle"> + <xsl:element name="{local-name(.)}"> + <xsl:choose> + <xsl:when test="@uom = 'urn:ogc:def:uom:EPSG::9102'"> + <xsl:attribute name="uom">radians</xsl:attribute></xsl:when> + <xsl:otherwise> + <xsl:attribute name="uom">degrees</xsl:attribute></xsl:otherwise> + </xsl:choose> + <xsl:value-of select="normalize-space(.)"/> + </xsl:element> + </xsl:template> + + <xsl:template match="gs:orientation"><xsl:call-template name="angle" /></xsl:template> + <xsl:template match="gs:radius"><xsl:call-template name="length" /></xsl:template> + <xsl:template match="gs:height"><xsl:call-template name="length" /></xsl:template> + <xsl:template match="gs:semiMajorAxis"><xsl:call-template name="length" /></xsl:template> + <xsl:template match="gs:semiMinorAxis"><xsl:call-template name="length" /></xsl:template> + <xsl:template match="gs:verticalAxis"><xsl:call-template name="length" /></xsl:template> + <xsl:template match="gs:innerRadius"><xsl:call-template name="length" /></xsl:template> + <xsl:template match="gs:outerRadius"><xsl:call-template name="length" /></xsl:template> + <xsl:template match="gs:startAngle"><xsl:call-template name="angle" /></xsl:template> + <xsl:template match="gs:openingAngle"><xsl:call-template name="angle" /></xsl:template> + <xsl:template match="gml:pos"><xsl:call-template name="name-value" /></xsl:template> + <xsl:template match="gml:posList"><xsl:call-template name="name-value" /></xsl:template> + + <xsl:template name="shape"> + <xsl:element name="{local-name(.)}"> + <xsl:choose> + <xsl:when test="@srsName = 'urn:ogc:def:crs:EPSG::4326'"> + <xsl:attribute name="srsName">2d</xsl:attribute> + </xsl:when> + <xsl:when test="@srsName = 'urn:ogc:def:crs:EPSG::4979'"> + <xsl:attribute name="srsName">3d</xsl:attribute> + </xsl:when> + <xsl:otherwise> + <xsl:attribute name="srsName">unknown</xsl:attribute> + </xsl:otherwise> + </xsl:choose> + <xsl:apply-templates /> + </xsl:element> + </xsl:template> + + <xsl:template match="ca:civicAddress/*"><xsl:call-template name="name-value" /></xsl:template> + <xsl:template name="civicAddress"> + <xsl:element name="{local-name(.)}"> + <xsl:attribute name="lang"><xsl:value-of select="@xml:lang"/></xsl:attribute> + <xsl:apply-templates select="./*"/> + </xsl:element> + </xsl:template> + + <xsl:template match="gp:usage-rules/*"> + <xsl:call-template name="name-value" /> + </xsl:template> + + <xsl:template match="gp:usage-rules"> + <xsl:element name="usage-rules"> + <xsl:apply-templates /> + </xsl:element> + </xsl:template> + + <xsl:template match="gp:method"> + <xsl:element name="method"> + <xsl:value-of select="normalize-space(.)" /> + </xsl:element> + </xsl:template> + + +</xsl:stylesheet>