From 89617370ec0e0cd0384ee24c451d2c70b51ec222 Mon Sep 17 00:00:00 2001 From: George Joseph Date: Mon, 15 Dec 2014 17:07:23 +0000 Subject: res_pjsip_config_wizard: Allow streamlined config of common pjsip scenarios res_pjsip_config_wizard ------------------ * This is a new module that adds streamlined configuration capability for chan_pjsip. It's targetted at users who have lots of basic configuration scenarios like 'phone' or 'agent' or 'trunk'. Additional information can be found in the sample configuration file at config/samples/pjsip_wizard.conf.sample. Tested-by: George Joseph Review: https://reviewboard.asterisk.org/r/4190/ git-svn-id: https://origsvn.digium.com/svn/asterisk/branches/13@429592 65c4cc65-6c06-0410-ace0-fbb531ad65f3 --- CHANGES | 8 + configs/samples/pjsip_wizard.conf.sample | 127 ++++ res/res_pjsip_config_wizard.c | 996 +++++++++++++++++++++++++++++++ res/res_pjsip_phoneprov_provider.c | 139 ++--- 4 files changed, 1192 insertions(+), 78 deletions(-) create mode 100644 configs/samples/pjsip_wizard.conf.sample create mode 100644 res/res_pjsip_config_wizard.c diff --git a/CHANGES b/CHANGES index e1ad2b610..a546bf86e 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,14 @@ --- Functionality changes from Asterisk 13.1.0 to Asterisk 13.2.0 ------------ ------------------------------------------------------------------------------ +res_pjsip_config_wizard +------------------ + * This is a new module that adds streamlined configuration capability for + chan_pjsip. It's targetted at users who have lots of basic configuration + scenarios like 'phone' or 'agent' or 'trunk'. Additional information + can be found in the sample configuration file at + config/samples/pjsip_wizard.conf.sample. + ARI ------------------ * The Originate operation now takes in an originator channel. The linked ID of diff --git a/configs/samples/pjsip_wizard.conf.sample b/configs/samples/pjsip_wizard.conf.sample new file mode 100644 index 000000000..b27e146be --- /dev/null +++ b/configs/samples/pjsip_wizard.conf.sample @@ -0,0 +1,127 @@ +; PJSIP Wizard Configuration Samples and Quick Reference +; +; This file has several very basic configuration examples, to serve as a quick +; reference to jog your memory when you need to write up a new configuration. +; It is not intended to teach PJSIP configuration or serve as an exhaustive +; reference of options and potential scenarios. +; +; This file has two main sections. +; First, manually written examples to serve as a handy reference. +; Second, a list of all possible PJSIP config options by section. This is +; pulled from the XML config help. It only shows the synopsis for every item. +; If you want to see more detail please check the documentation sources +; mentioned at the top of this file. + +; Documentation +; +; The official documentation is at http://wiki.asterisk.org +; You can read the XML configuration help via Asterisk command line with +; "config show help res_pjsip_config_wizard", then you can drill down through +; the various sections and their options. +; + + +;============EXAMPLE WIZARD CONFIGURATION FOR A PHONE======================= + +; This config would create an endpoint, aor with dynamic contact, inbound +; auth and a phoneprov object. + +;[myphone] +;type = wizard +;accepts_auth = yes +;accepts_registrations = yes +;has_phoneprov = yes +;transport = ipv4 +;inbound_auth/username = testname +;inbound_auth/password = test password +;endpoint/allow = ulaw +;endpoint/context = default +;phoneprov/MAC = 001122aa4455 +;phoneprov/PROFILE = profile1 + + +;============EXAMPLE WIZARD CONFIGURATION FOR AN ITSP TRUNK================= + +; This ITSP has 2 servers available and requires registration. + +; This config would create an endpoint, an aor with 2 static contacts, an +; outbound auth, an identify with 2 matches, and 2 registrations. + +;[mytrunk] +;type = wizard +;sends_auth = yes +;sends_registrations = yes +;transport = ipv4 +;remote_hosts = sip1.myitsp.com:5060,sip2.myitsp.com:5060 +;outbound_auth/username = testname +;outbound_auth/password = test password +;endpoint/allow = ulaw +;endpoint/context = default + + +;========================WIZARD SECTION OPTIONS=============================== +;[wizard] +; SYNOPSIS: Provides configuration wizard for common scenarios. +;sends_auth= ; Will create an outbound auth object for the endpoint and + ; registration. + ; If yes, outbound/username must be specified. + ; (default = "no") + +;accepts_auth= ; Will create an inbound auth object for the endpoint. + ; If yes, inbound/username must be specified. + ; (default = "no") + +;sends_registrations= ; Will create an outbound registration object and an + ; identify match for each host in remote_hosts (which + ; must be specified). + ; sends_auth must also be specified. + ; (default: "no") + +;accepts_registrations= ; Will create an aor with dynamic contacts which will + ; accept registrations. + ; accepts_auth must also be specified. + ; (default: "no") + +;remote_hosts= ; A comma separated list of remote hosts in the form of + ; [:port] [,[:port] ] ... + ; If specified, a static contact for each host will be created + ; in the aor. If accepts_registrations is no, an identify + ; object is also created with a match line for each remote host. + ; If an aor/contact or match/identify is explicitly supplied, + ; remote_hosts will not be used to automatically create contacts + ; or matches respectively. + ; Hostnames must resolve to A, AAAA or CNAME records. + ; SRV records are not currently supported. + ; (default: "") + +;transport= ; The transport to use for the endpoint and registrations + ; (default: the pjsip default) + +;server_uri_pattern= ; The pattern used to construct the registration + ; server_uri. The replaceable parameter ${REMOTE_HOST} isa + ; available for use. + ; (default: "sip:${REMOTE_HOST}") + +;client_uri_pattern= ; The pattern used to construct the registration client_uri. + ; The replaceable parameters ${REMOTE_HOST} and ${USERNAME} + ; are available for use. + ; (default: "sip:${USERNAME}@${REMOTE_HOST}") + +;contact_pattern= ; The pattern used to construct the aor contact. + ; The replaceable parameter ${REMOTE_HOST} is available + ; for use. + ; (default: "sip:${REMOTE_HOST}") + +;has_phoneprov= ; Will create a phoneprov object. + ; If yes, phoneprov/MAC must be specified. + ; (default: "no") + +;endpoint/ ; Any parameters to be passed directly to and validated +;aor/ ; by their respective objects. +;inbound_auth/ +;outbound_auth/ +;identify/ +;registration/ +;phoneprov/ + +;type= ; Must be of type wizard (default: "") diff --git a/res/res_pjsip_config_wizard.c b/res/res_pjsip_config_wizard.c new file mode 100644 index 000000000..60cbc1077 --- /dev/null +++ b/res/res_pjsip_config_wizard.c @@ -0,0 +1,996 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2014, Fairview 5 Engineering, LLC + * + * George Joseph + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief PJSIP Configuration Wizard + * + * \author George Joseph + */ + +/*! \li \ref res_pjsip_config_wizard.c uses the configuration file \ref pjsip_wizard.conf + */ + +/*! + * \page pjsip_wizard.conf pjsip_wizard.conf + * \verbinclude pjsip_wizard.conf.sample + */ + +/*** MODULEINFO + res_pjsip + core + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include +#include + +#include "asterisk/astobj2.h" +#include "asterisk/res_pjsip.h" +#include "asterisk/module.h" +#include "asterisk/pbx.h" +#include "asterisk/sorcery.h" +#include "asterisk/vector.h" + +/*** DOCUMENTATION + + Module that privides simple configuration wizard capabilities. + + PJSIP Configuration Wizard + + This module allows creation of common PJSIP configuration scenarios + without having to specify individual endpoint, aor, auth, identify and registration objects. + + + + For example, the following configuration snippet would create the + endpoint, aor, contact, auth and phoneprov objects necessary for a phone to + get phone provisioning information, register, and make and receive calls. + + + [myphone] + type = wizard + sends_auth = no + accepts_auth = yes + sends_registrations = no + accepts_registrations = yes + has_phoneprov = yes + transport = ipv4 + inbound_auth/username = testname + inbound_auth/password = test password + endpoint/allow = ulaw + endpoint/context = default + phoneprov/MAC = 001122aa4455 + phoneprov/PROFILE = profile1 + + + The first 7 items are specific to the wizard. The rest of the items + are passed verbatim to the underlying objects. + + + The following configuration snippet would create the + endpoint, aor, contact, auth, identify and registration objects necessary for a trunk + to another pbx or ITSP that requires registration. + + + [mytrunk] + type = wizard + sends_auth = yes + accepts_auth = no + sends_registrations = yes + accepts_registrations = no + transport = ipv4 + remote_hosts = sip1.myitsp.com:5060,sip2.myitsp.com:5060 + outbound_auth/username = testname + outbound_auth/password = test password + endpoint/allow = ulaw + endpoint/context = default + + + Of course, any of the items in either example could be placed into + templates and shared among wizard objects. + + + + + Provides config wizard. + + Must be 'wizard'. + + + The name of a transport to use for this object. + If not specified, + the default will be used. + + + List of remote hosts. + A comma-separated list of remote hosts in the form of + host[:port]. + If set, an aor static contact and an identify match will be created for each + entry in the list. If send_registrations is also set, a registration will + also be created for each. + + + Send outbound authentication to remote hosts. + At least outbound_auth/username is required. + + + Accept incoming authentication from remote hosts. + At least inbound_auth/username is required. + + + Send outbound registrations to remote hosts. + remote_hosts is required and a registration object will + be created for each host in the remote _hosts string. If authentication is required, + sends_auth and an outbound_auth/username must also be supplied. + + + Accept inbound registration from remote hosts. + An AOR with dynamic contacts will be created. If + the number of contacts nneds to be limited, set aor/max_contacts. + + + Create a phoneprov object for this endpoint. + A phoneprov object will be created. phoneprov/MAC + must be specified. + + + A pattern to use for constructing outbound registration server_uris. + + The literal ${REMOTE_HOST} will be substituted with the + appropriate remote_host for each registration. + + + A pattern to use for constructing outbound registration client_uris. + + The literals ${REMOTE_HOST} and ${USERNAME} + will be substituted with the appropriate remote_host and outbound_auth/username. + + + A pattern to use for constructing outbound contact uris. + + The literal ${REMOTE_HOST} will be substituted with the + appropriate remote_host for each contact. + + + Variables to be passed directly to the endpoint. + + + Variables to be passed directly to the aor. + If an aor/contact is explicitly defined then remote_hosts + will not be used to create contacts automatically. + + + Variables to be passed directly to the inbound auth. + + + Variables to be passed directly to the outbound auth. + + + Variables to be passed directly to the identify. + If an identify/match is explicitly defined then remote_hosts + will not be used to create matches automatically. + + + Variables to be passed directly to the outbound registrations. + + + Variables to be passed directly to the phoneprov object. + + To activate phoneprov, at least phoneprov/MAC must be set. + + + + + ***/ + + /*! \brief Defines the maximum number of characters that can be added to a wizard id. */ +#define MAX_ID_SUFFIX 20 + +/*! \brief A generic char * vector definition. */ +AST_VECTOR(string_vector, char *); + +/*! \brief Keeps track of the sorcery wizard and last config for each object type */ +struct object_type_wizard { + struct ast_sorcery *sorcery; + struct ast_sorcery_wizard *wizard; + void *wizard_data; + struct ast_config *last_config; + char object_type[]; +}; +static AST_VECTOR(object_type_wizards, struct object_type_wizard *) object_type_wizards; + +/*! \brief Callbacks for vector deletes */ +#define NOT_EQUALS(a, b) (a != b) +#define OTW_DELETE_CB(otw) ({ \ + ast_config_destroy(otw->last_config); \ + ast_free(otw); \ +}) + +const static char *object_types[] = {"phoneprov", "registration", "identify", "endpoint", "aor", "auth", NULL}; + +static int is_one_of(const char *needle, const char *haystack[]) +{ + int i; + for (i = 0; haystack[i]; i++) { + if (!strcmp(needle, haystack[i])) { + return 1; + } + } + + return 0; +} + +/*! \brief Finds the otw for the object type */ +static struct object_type_wizard *find_wizard(const char *object_type) +{ + int idx; + + for(idx = 0; idx < AST_VECTOR_SIZE(&object_type_wizards); idx++) { + struct object_type_wizard *otw = AST_VECTOR_GET(&object_type_wizards, idx); + if (!strcmp(otw->object_type, object_type)) { + return otw; + } + } + + return NULL; +} + +/*! \brief Creates a sorcery object and applies a variable list */ +static void *create_object(const struct ast_sorcery *sorcery, + const char *id, const char *type, struct ast_variable *vars) +{ + struct ast_sorcery_object *obj = ast_sorcery_alloc(sorcery, type, id); + + if (!obj) { + ast_log(LOG_ERROR, "Unable to allocate an object of type '%s' with id '%s'.\n", type, id); + return NULL; + } + + if (ast_sorcery_objectset_apply(sorcery, obj, vars)) { + ast_log(LOG_ERROR, "Unable to apply object type '%s' with id '%s'. Check preceeding errors.\n", type, id); + ao2_ref(obj, -1); + return NULL; + } + + return obj; +} + +/*! \brief Finds a variable in a list and tests it */ +static int is_variable_true(struct ast_variable *vars, const char *name) +{ + return ast_true(ast_variable_find_in_list(vars, name)); +} + +/*! \brief Appends a variable to the end of an existing list */ +static int variable_list_append(struct ast_variable **existing, const char *name, const char *value) +{ + struct ast_variable *new = ast_variable_new(name, value, ""); + + if (!new) { + ast_log(LOG_ERROR, "Unable to allocate memory for new variable '%s'.\n", name); + return -1; + } + + ast_variable_list_append(existing, new); + + return 0; +} + +/*! \brief Appends a variable to the end of an existing list. On failure, cause the calling + * function to return -1 */ +#define variable_list_append_return(existing, name, value) ({ \ + struct ast_variable *new = ast_variable_new(name, value, ""); \ + if (!new) { \ + ast_log(LOG_ERROR, "Unable to allocate memory for new variable '%s'.\n", name); \ + return -1; \ + } \ + ast_variable_list_append(existing, new); \ +}) + +/*! \brief We need to strip off the prefix from the name of each variable + * so they're suitable for objectset_apply. + * I.E. will transform outbound_auth/username to username. + */ +static struct ast_variable *get_object_variables(struct ast_variable *vars, char *prefix) +{ + struct ast_variable *return_vars = NULL; + struct ast_variable *v = vars; + int plen = strlen(prefix); + + for(; v; v = v->next) { + if (ast_begins_with(v->name, prefix) && strlen(v->name) > plen) { + if (variable_list_append(&return_vars, v->name + plen, v->value)) { + ast_variables_destroy(return_vars); + return NULL; + } + } + } + + return return_vars; +} + +static int handle_auth(const struct ast_sorcery *sorcery, struct object_type_wizard *otw, + struct ast_category *wiz, char *direction) +{ + struct ast_variable *wizvars = ast_category_first(wiz); + struct ast_sorcery_object *obj = NULL; + const char *id = ast_category_get_name(wiz); + char new_id[strlen(id) + MAX_ID_SUFFIX]; + char prefix[strlen(direction) + strlen("_auth/") + 1]; + char *test_variable = NULL; + RAII_VAR(struct ast_variable *, vars, NULL, ast_variables_destroy); + + snprintf(prefix, sizeof(prefix), "%s_auth/", direction); + vars = get_object_variables(wizvars, prefix); + + if (!strcmp(direction, "outbound")) { + snprintf(new_id, sizeof(new_id), "%s-oauth", id); + test_variable = "sends_auth"; + } else { + snprintf(new_id, sizeof(new_id), "%s-iauth", id); + test_variable = "accepts_auth"; + } + + if (is_variable_true(wizvars, test_variable)) { + if (!ast_variable_find_in_list(vars, "username")) { + ast_log(LOG_ERROR, + "Wizard '%s' must have '%s_auth/username' if it %s.\n", id, direction, test_variable); + return -1; + } + } else { + /* Delete auth if sends or accepts is now false. */ + obj = otw->wizard->retrieve_id(sorcery, otw->wizard_data, "auth", new_id); + if (obj) { + otw->wizard->delete(sorcery, otw->wizard_data, obj); + ao2_ref(obj, -1); + } + return 0; + } + + variable_list_append_return(&vars, "@pjsip_wizard", id); + + /* If the user set auth_type, don't override it. */ + if (!ast_variable_find_in_list(vars, "auth_type")) { + variable_list_append_return(&vars, "auth_type", "userpass"); + } + + obj = create_object(sorcery, new_id, "auth", vars); + if (!obj) { + return -1; + } + + if (otw->wizard->update(sorcery, otw->wizard_data, obj)) { + otw->wizard->create(sorcery, otw->wizard_data, obj); + } + ao2_ref(obj, -1); + + return 0; +} + +static int handle_auths(const struct ast_sorcery *sorcery, struct object_type_wizard *otw, + struct ast_category *wiz) +{ + int rc; + + if ((rc = handle_auth(sorcery, otw, wiz, "outbound"))) { + return rc; + } + + return handle_auth(sorcery, otw, wiz, "inbound"); +} + +static int handle_aor(const struct ast_sorcery *sorcery, struct object_type_wizard *otw, + struct ast_category *wiz, struct string_vector *remote_hosts_vector) +{ + struct ast_variable *wizvars = ast_category_first(wiz); + struct ast_sorcery_object *obj = NULL; + const char *id = ast_category_get_name(wiz); + const char *contact_pattern; + int host_count = AST_VECTOR_SIZE(remote_hosts_vector); + RAII_VAR(struct ast_variable *, vars, get_object_variables(wizvars, "aor/"), ast_variables_destroy); + + variable_list_append(&vars, "@pjsip_wizard", id); + + /* If the user explicitly specified an aor/contact, don't use remote hosts. */ + if (!ast_variable_find_in_list(vars, "contact")) { + if (!(contact_pattern = ast_variable_find_in_list(wizvars, "contact_pattern"))) { + contact_pattern = "sip:${REMOTE_HOST}"; + } + + if (host_count > 0 && !ast_strlen_zero(contact_pattern)) { + int host_counter; + + /* ast_str_substitute_variables operate on a varshead list so we have + * to create one to hold the REPORT_HOST substitution, do the substitution, + * then append the result to the ast_variable list. + */ + for (host_counter = 0; host_counter < host_count; host_counter++) { + RAII_VAR(struct ast_str *, new_str, ast_str_create(64), ast_free); + RAII_VAR(struct varshead *, subst_vars, ast_var_list_create(), ast_var_list_destroy); + struct ast_var_t *var = ast_var_assign("REMOTE_HOST", + AST_VECTOR_GET(remote_hosts_vector, host_counter)); + + AST_VAR_LIST_INSERT_TAIL(subst_vars, var); + ast_str_substitute_variables_varshead(&new_str, 0, subst_vars, + contact_pattern); + + variable_list_append_return(&vars, "contact", ast_str_buffer(new_str)); + } + } + } + + obj = create_object(sorcery, id, "aor", vars); + if (!obj) { + return -1; + } + + if (otw->wizard->update(sorcery, otw->wizard_data, obj)) { + otw->wizard->create(sorcery, otw->wizard_data, obj); + } + ao2_ref(obj, -1); + + return 0; +} + +static int handle_endpoint(const struct ast_sorcery *sorcery, struct object_type_wizard *otw, + struct ast_category *wiz) +{ + struct ast_variable *wizvars = ast_category_first(wiz); + struct ast_sorcery_object *obj = NULL; + const char *id = ast_category_get_name(wiz); + const char *transport = ast_variable_find_in_list(wizvars, "transport"); + char new_id[strlen(id) + MAX_ID_SUFFIX]; + RAII_VAR(struct ast_variable *, vars, get_object_variables(wizvars, "endpoint/"), ast_variables_destroy); + + variable_list_append_return(&vars, "@pjsip_wizard", id); + variable_list_append_return(&vars, "aors", id); + + if (!ast_strlen_zero(transport)) { + variable_list_append_return(&vars, "transport", transport); + } + + if (is_variable_true(wizvars, "sends_auth")) { + snprintf(new_id, sizeof(new_id), "%s-oauth", id); + variable_list_append_return(&vars, "outbound_auth", new_id); + } + + if (is_variable_true(wizvars, "accepts_auth")) { + snprintf(new_id, sizeof(new_id), "%s-iauth", id); + variable_list_append_return(&vars, "auth", new_id); + } + + obj = create_object(sorcery, id, "endpoint", vars); + if (!obj) { + return -1; + } + + if (otw->wizard->update(sorcery, otw->wizard_data, obj)) { + otw->wizard->create(sorcery, otw->wizard_data, obj); + } + ao2_ref(obj, -1); + + return 0; +} + +static int handle_identify(const struct ast_sorcery *sorcery, struct object_type_wizard *otw, + struct ast_category *wiz, struct string_vector *remote_hosts_vector) +{ + struct ast_variable *wizvars = ast_category_first(wiz); + struct ast_sorcery_object *obj = NULL; + const char *id = ast_category_get_name(wiz); + char new_id[strlen(id) + MAX_ID_SUFFIX]; + int host_count = AST_VECTOR_SIZE(remote_hosts_vector); + int host_counter; + RAII_VAR(struct ast_variable *, vars, get_object_variables(wizvars, "identify/"), ast_variables_destroy); + + snprintf(new_id, sizeof(new_id), "%s-identify", id); + + /* If accepting registrations, we don't need an identify. */ + if (is_variable_true(wizvars, "accepts_registrations")) { + /* If one exists, delete it. */ + obj = otw->wizard->retrieve_id(sorcery, otw->wizard_data, "identify", new_id); + if (obj) { + otw->wizard->delete(sorcery, otw->wizard_data, obj); + ao2_ref(obj, -1); + } + return 0; + } + + if (!host_count) { + ast_log(LOG_ERROR, + "Wizard '%s' must have 'remote_hosts' if it doesn't accept registrations.\n", id); + return -1; + } + + variable_list_append_return(&vars, "endpoint", id); + variable_list_append_return(&vars, "@pjsip_wizard", id); + + if (!ast_variable_find_in_list(vars, "match")) { + for (host_counter = 0; host_counter < host_count; host_counter++) { + char *rhost = AST_VECTOR_GET(remote_hosts_vector, host_counter); + char host[strlen(rhost) + 1]; + char *colon; + + /* If there's a :port specified, we have to remove it. */ + strcpy(host, rhost); /* Safe */ + colon = strchr(host, ':'); + if (colon) { + *colon = '\0'; + } + + variable_list_append_return(&vars, "match", host); + } + } + + obj = create_object(sorcery, new_id, "identify", vars); + if (!obj) { + return -1; + } + + if (otw->wizard->update(sorcery, otw->wizard_data, obj)) { + otw->wizard->create(sorcery, otw->wizard_data, obj); + } + ao2_ref(obj, -1); + + return 0; +} + +static int handle_phoneprov(const struct ast_sorcery *sorcery, struct object_type_wizard *otw, + struct ast_category *wiz) +{ + struct ast_variable *wizvars = ast_category_first(wiz); + struct ast_sorcery_object *obj = NULL; + const char *id = ast_category_get_name(wiz); + char new_id[strlen(id) + MAX_ID_SUFFIX]; + RAII_VAR(struct ast_variable *, vars, get_object_variables(wizvars, "phoneprov/"), ast_variables_destroy); + + snprintf(new_id, sizeof(new_id), "%s-phoneprov", id); + + if (!is_variable_true(wizvars, "has_phoneprov")) { + obj = otw->wizard->retrieve_id(sorcery, otw->wizard_data, "phoneprov", new_id); + if (obj) { + otw->wizard->delete(sorcery, otw->wizard_data, obj); + ao2_ref(obj, -1); + } + return 0; + } + + if (!ast_variable_find_in_list(wizvars, "phoneprov/MAC")) { + ast_log(LOG_ERROR, + "Wizard '%s' must have 'phoneprov/MAC' if it has_phoneprov.\n", id); + return -1; + } + + variable_list_append_return(&vars, "endpoint", id); + variable_list_append_return(&vars, "@pjsip_wizard", id); + + obj = create_object(sorcery, new_id, "phoneprov", vars); + if (!obj) { + return -1; + } + + if (otw->wizard->update(sorcery, otw->wizard_data, obj)) { + otw->wizard->create(sorcery, otw->wizard_data, obj); + } + ao2_ref(obj, -1); + + return 0; +} + +static int delete_existing_cb(void *obj, void *arg, int flags) +{ + struct object_type_wizard *otw = arg; + + otw->wizard->delete(otw->sorcery, otw->wizard_data, obj); + + return CMP_MATCH; +} + +static int handle_registrations(const struct ast_sorcery *sorcery, struct object_type_wizard *otw, + struct ast_category *wiz, struct string_vector *remote_hosts_vector) +{ + struct ast_variable *search; + struct ast_variable *wizvars = ast_category_first(wiz); + const char *id = ast_category_get_name(wiz); + const char *server_uri_pattern; + const char *client_uri_pattern; + const char *transport = ast_variable_find_in_list(wizvars, "transport"); + const char *username; + char new_id[strlen(id) + MAX_ID_SUFFIX]; + int host_count = AST_VECTOR_SIZE(remote_hosts_vector); + int host_counter; + RAII_VAR(struct ast_variable *, vars, get_object_variables(wizvars, "registration/"), ast_variables_destroy); + RAII_VAR(struct ao2_container *, existing, + ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_NOLOCK, 0, NULL, NULL), ao2_cleanup); + + if (!existing) { + return -1; + } + + /* Find any existing registrations. */ + search = ast_variable_new("@pjsip_wizard", id, ""); + if (!search) { + return -1; + } + + otw->wizard->retrieve_multiple(sorcery, otw->wizard_data, "registration", existing, search); + ast_variables_destroy(search); + + /* If not sending registrations, delete ALL existing registrations for this wizard. */ + if (!is_variable_true(wizvars, "sends_registrations")) { + if (ao2_container_count(existing) > 0) { + ao2_callback(existing, OBJ_NODATA | OBJ_UNLINK | OBJ_MULTIPLE, delete_existing_cb, otw); + } + return 0; + } + + if (!host_count) { + ast_log(LOG_ERROR, "Wizard '%s' must have 'remote_hosts' if it sends registrations.\n", id); + return -1; + } + + variable_list_append_return(&vars, "@pjsip_wizard", id); + + if (!(server_uri_pattern = ast_variable_find_in_list(wizvars, "server_uri_pattern"))) { + server_uri_pattern = "sip:${REMOTE_HOST}"; + } + + if (!(client_uri_pattern = ast_variable_find_in_list(wizvars, "client_uri_pattern"))) { + client_uri_pattern = "sip:${USERNAME}@${REMOTE_HOST}"; + } + + if(is_variable_true(wizvars, "sends_auth")) { + username = ast_variable_find_in_list(wizvars, "outbound_auth/username"); + } else { + username = id; + } + + + /* Unlike aor and identify, we need to create a separate registration object + * for each remote host. + */ + for (host_counter = 0; host_counter < host_count; host_counter++) { + struct ast_var_t *rh = ast_var_assign("REMOTE_HOST", + AST_VECTOR_GET(remote_hosts_vector, host_counter)); + struct ast_var_t *un = ast_var_assign("USERNAME", username); + struct ast_sorcery_object *obj; + RAII_VAR(struct ast_str *, uri, ast_str_create(64), ast_free); + RAII_VAR(struct varshead *, subst_vars, ast_var_list_create(), ast_var_list_destroy); + RAII_VAR(struct ast_variable *, registration_vars, vars ? ast_variables_dup(vars) : NULL, ast_variables_destroy); + + AST_VAR_LIST_INSERT_TAIL(subst_vars, rh); + AST_VAR_LIST_INSERT_TAIL(subst_vars, un); + + if (!ast_strlen_zero(server_uri_pattern)) { + ast_str_substitute_variables_varshead(&uri, 0, subst_vars, + server_uri_pattern); + variable_list_append_return(®istration_vars, "server_uri", ast_str_buffer(uri)); + } + + if (!ast_strlen_zero(client_uri_pattern)) { + ast_str_reset(uri); + ast_str_substitute_variables_varshead(&uri, 0, subst_vars, + client_uri_pattern); + variable_list_append_return(®istration_vars, "client_uri", ast_str_buffer(uri)); + } + + if (is_variable_true(wizvars, "sends_auth")) { + snprintf(new_id, sizeof(new_id), "%s-oauth", id); + variable_list_append_return(®istration_vars, "outbound_auth", new_id); + } + + if (!ast_strlen_zero(transport)) { + variable_list_append_return(®istration_vars, "transport", transport); + } + + snprintf(new_id, sizeof(new_id), "%s-reg-%d", id, host_counter); + + obj = create_object(sorcery, new_id, "registration", registration_vars); + if (!obj) { + return -1; + } + + if (otw->wizard->update(sorcery, otw->wizard_data, obj)) { + otw->wizard->create(sorcery, otw->wizard_data, obj); + } + ao2_ref(obj, -1); + + /* Unlink it from the 'existing' container. Any left will be deleted from + * sorcery. If it wasn't in the existing container, no harm. + */ + ao2_callback(existing, OBJ_NODATA | OBJ_UNLINK | OBJ_SEARCH_KEY, ast_sorcery_object_id_compare, new_id); + } + + /* If there are any excess registrations, delete them. */ + if (ao2_container_count(existing) > 0) { + ao2_callback(existing, OBJ_NODATA | OBJ_UNLINK | OBJ_MULTIPLE, delete_existing_cb, otw); + } + + return 0; +} + +static int wizard_apply_handler(const struct ast_sorcery *sorcery, struct object_type_wizard *otw, + struct ast_category *wiz) +{ + struct ast_variable *wizvars = ast_category_first(wiz); + struct string_vector remote_hosts_vector; + const char *remote_hosts; + int rc = -1; + + AST_VECTOR_INIT(&remote_hosts_vector, 16); + remote_hosts = ast_variable_find_in_list(wizvars, "remote_hosts"); + + if (!ast_strlen_zero(remote_hosts)) { + char *host; + char *hosts = ast_strdupa(remote_hosts); + + while ((host = ast_strsep(&hosts, ',', AST_STRSEP_TRIM))) { + AST_VECTOR_APPEND(&remote_hosts_vector, ast_strdup(host)); + } + } + + ast_debug(4, "%s handler starting.\n", otw->object_type); + + if (!strcmp(otw->object_type, "auth")) { + rc = handle_auths(sorcery, otw, wiz); + } else if (!strcmp(otw->object_type, "aor")) { + rc = handle_aor(sorcery, otw, wiz, &remote_hosts_vector); + } else if (!strcmp(otw->object_type, "endpoint")) { + rc = handle_endpoint(sorcery, otw, wiz); + } else if (!strcmp(otw->object_type, "identify")) { + rc = handle_identify(sorcery, otw, wiz, &remote_hosts_vector); + } else if (!strcmp(otw->object_type, "phoneprov")) { + rc = handle_phoneprov(sorcery, otw, wiz); + } else if (!strcmp(otw->object_type, "registration")) { + rc = handle_registrations(sorcery, otw, wiz, &remote_hosts_vector); + } + + AST_VECTOR_REMOVE_CMP_UNORDERED(&remote_hosts_vector, NULL, NOT_EQUALS, ast_free); + AST_VECTOR_FREE(&remote_hosts_vector); + + ast_debug(4, "%s handler complete. rc: %d\n", otw->object_type, rc); + + return rc; +} + +/* + * Everything below are the sorcery observers. + */ +static void instance_created_observer(const char *name, struct ast_sorcery *sorcery); +static void object_type_loaded_observer(const char *name, + const struct ast_sorcery *sorcery, const char *object_type, int reloaded); +static void wizard_mapped_observer(const char *name, struct ast_sorcery *sorcery, + const char *object_type, struct ast_sorcery_wizard *wizard, + const char *wizard_args, void *wizard_data); +static void object_type_registered_observer(const char *name, + struct ast_sorcery *sorcery, const char *object_type); + +const static struct ast_sorcery_global_observer global_observer = { + .instance_created = instance_created_observer, +}; + +struct ast_sorcery_instance_observer observer = { + .wizard_mapped = wizard_mapped_observer, + .object_type_registered = object_type_registered_observer, + .object_type_loaded = object_type_loaded_observer, +}; + +/*! \brief Called after an object type is loaded/reloaded */ +static void object_type_loaded_observer(const char *name, + const struct ast_sorcery *sorcery, const char *object_type, int reloaded) +{ + struct ast_category *category = NULL; + struct object_type_wizard *otw = NULL; + char *filename = "pjsip_wizard.conf"; + struct ast_flags flags = { reloaded ? CONFIG_FLAG_FILEUNCHANGED : 0 }; + struct ast_config *cfg; + + if (!strstr("auth aor endpoint identify registration phoneprov", object_type)) { + /* Not interested. */ + return; + } + + otw = find_wizard(object_type); + if (!otw) { + ast_log(LOG_ERROR, "There was no wizard for object type '%s'\n", object_type); + return; + } + + cfg = ast_config_load2(filename, object_type, flags); + + if (!cfg) { + ast_log(LOG_ERROR, "Unable to load config file '%s'\n", filename); + return; + } else if (cfg == CONFIG_STATUS_FILEUNCHANGED) { + ast_debug(2, "Config file '%s' was unchanged for '%s'.\n", filename, object_type); + return; + } else if (cfg == CONFIG_STATUS_FILEINVALID) { + ast_log(LOG_ERROR, "Contents of config file '%s' are invalid and cannot be parsed\n", filename); + return; + } + + while ((category = ast_category_browse_filtered(cfg, NULL, category, "type=^wizard$"))) { + const char *id = ast_category_get_name(category); + struct ast_category *last_cat = NULL; + struct ast_variable *change_set = NULL; + + if (otw->last_config) { + last_cat = ast_category_get(otw->last_config, id, "type=^wizard$"); + ast_sorcery_changeset_create(ast_category_first(category), ast_category_first(last_cat), &change_set); + if (last_cat) { + ast_category_delete(otw->last_config, last_cat); + } + } + + if (!last_cat || change_set) { + ast_variables_destroy(change_set); + ast_debug(3, "%s: %s(s) for wizard '%s'\n", reloaded ? "Reload" : "Load", object_type, id); + if (wizard_apply_handler(sorcery, otw, category)) { + ast_log(LOG_ERROR, "Unable to create objects for wizard '%s'\n", id); + } + } + } + + if (!otw->last_config) { + otw->last_config = cfg; + return; + } + + /* Only wizards that weren't in the new config are left in last_config now so we need to delete + * all objects belonging to them. + */ + category = NULL; + while ((category = ast_category_browse_filtered(otw->last_config, NULL, category, "type=^wizard$"))) { + const char *id = ast_category_get_name(category); + struct ast_variable *search; + RAII_VAR(struct ao2_container *, existing, + ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_NOLOCK, 0, NULL, NULL), ao2_cleanup); + + if (!existing) { + ast_log(LOG_ERROR, "Unable to allocate temporary container.\n"); + break; + } + + search = ast_variable_new("@pjsip_wizard", id, ""); + if (!search) { + ast_log(LOG_ERROR, "Unable to allocate memory for vaiable '@pjsip_wizard'.\n"); + break; + } + otw->wizard->retrieve_multiple(sorcery, otw->wizard_data, object_type, existing, search); + ast_variables_destroy(search); + + if (ao2_container_count(existing) > 0) { + ast_debug(3, "Delete on %s: %d %s(s) for wizard: %s\n", + reloaded ? "Reload" : "Load", ao2_container_count(existing), object_type, id); + ao2_callback(existing, OBJ_NODATA | OBJ_UNLINK | OBJ_MULTIPLE, + delete_existing_cb, otw); + } + } + + ast_config_destroy(otw->last_config); + otw->last_config = cfg; +} + +/*! \brief When each wizard is mapped, save it off to the vector. */ +static void wizard_mapped_observer(const char *name, struct ast_sorcery *sorcery, + const char *object_type, struct ast_sorcery_wizard *wizard, + const char *wizard_args, void *wizard_data) +{ + struct object_type_wizard *otw; + + if (!is_one_of(object_type, object_types)) { + /* Not interested. */ + return; + } + + /* We're only interested in memory wizards with the pjsip_wizard tag. */ + if (wizard_args && !strcmp(wizard_args, "pjsip_wizard")) { + otw = ast_malloc(sizeof(*otw) + strlen(object_type) + 1); + otw->sorcery = sorcery; + otw->wizard = wizard; + otw->wizard_data = wizard_data; + otw->last_config = NULL; + strcpy(otw->object_type, object_type); /* Safe */ + AST_VECTOR_APPEND(&object_type_wizards, otw); + ast_debug(1, "Wizard mapped for object_type '%s'\n", object_type); + } +} + +/*! \brief When each object type is registered, map a memory wizard to it. */ +static void object_type_registered_observer(const char *name, + struct ast_sorcery *sorcery, const char *object_type) +{ + if (is_one_of(object_type, object_types)) { + ast_sorcery_apply_wizard_mapping(sorcery, object_type, "memory", "pjsip_wizard", 0); + } +} + +/*! \brief When the res_pjsip instance is created, add an observer to it and initialize the wizard vector. + * Since you can't unload res_pjsip, this will only ever be called once. + */ +static void instance_created_observer(const char *name, struct ast_sorcery *sorcery) +{ + if (strcmp(name, "res_pjsip")) { + return; + } + + ast_sorcery_instance_observer_add(sorcery, &observer); +} + +static int load_module(void) +{ + struct ast_sorcery *sorcery = ast_sip_get_sorcery(); + int i; + + AST_VECTOR_INIT(&object_type_wizards, 12); + ast_sorcery_global_observer_add(&global_observer); + + /* If this module is loading AFTER res_pjsip, we need to manually add the instance observer + * and map the wizards because the observers will never get triggered. + * The we neeed to schedule a reload. + */ + if (sorcery) { + /* CLean up and add the observer. */ + ast_sorcery_instance_observer_remove(sorcery, &observer); + ast_sorcery_instance_observer_add(sorcery, &observer); + + for (i = 0; object_types[i]; i++) { + ast_sorcery_apply_wizard_mapping(sorcery, object_types[i], "memory", "pjsip_wizard", 0); + } + + ast_module_reload("res_pjsip"); + } + + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + struct object_type_wizard *otw; + int i; + + ast_sorcery_global_observer_remove(&global_observer); + + for (i = 0; object_types[i]; i++) { + RAII_VAR(struct ao2_container *, existing, + ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_NOLOCK, 0, NULL, NULL), ao2_cleanup); + + otw = find_wizard(object_types[i]); + if (otw->sorcery) { + ast_sorcery_instance_observer_remove(otw->sorcery, &observer); + } + + otw->wizard->retrieve_multiple(otw->sorcery, otw->wizard_data, object_types[i], existing, NULL); + ao2_callback(existing, OBJ_NODATA | OBJ_UNLINK | OBJ_MULTIPLE, delete_existing_cb, otw); + } + + AST_VECTOR_REMOVE_CMP_UNORDERED(&object_type_wizards, NULL, NOT_EQUALS, OTW_DELETE_CB); + AST_VECTOR_FREE(&object_type_wizards); + + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "PJSIP Config Wizard", + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_REALTIME_DRIVER, + ); diff --git a/res/res_pjsip_phoneprov_provider.c b/res/res_pjsip_phoneprov_provider.c index 61b026f7c..3b6594057 100644 --- a/res/res_pjsip_phoneprov_provider.c +++ b/res/res_pjsip_phoneprov_provider.c @@ -135,6 +135,11 @@ struct phoneprov { static void phoneprov_destroy(void *obj) { struct phoneprov *pp = obj; + char *mac = ast_var_find(pp->vars, "MAC"); + + if (mac) { + ast_phoneprov_delete_extension(AST_MODULE, mac); + } ast_var_list_destroy(pp->vars); } @@ -196,7 +201,7 @@ static int fields_handler(const void *obj, struct ast_variable **fields) struct ast_variable *var; AST_VAR_LIST_TRAVERSE(pp->vars, pvar) { - var = ast_variable_new(pvar->name, pvar->value, NULL); + var = ast_variable_new(pvar->name, pvar->value, ""); if (!var) { ast_variables_destroy(head); return -1; @@ -228,7 +233,7 @@ static int load_endpoint(const char *id, const char *endpoint_name, struct varsh /* We need to use res_pjsip's sorcery instance instead of our own to * get endpoint and auth. */ - endpoint = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", + endpoint = ast_sorcery_retrieve_by_id(sorcery, "endpoint", endpoint_name); if (!endpoint) { ast_log(LOG_ERROR, "phoneprov %s contained invalid endpoint %s.\n", id, @@ -250,7 +255,7 @@ static int load_endpoint(const char *id, const char *endpoint_name, struct varsh endpoint->id.self.name.str, vars); } - transport = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "transport", + transport = ast_sorcery_retrieve_by_id(sorcery, "transport", endpoint->transport); if (!transport) { ast_log(LOG_ERROR, "Endpoint %s contained invalid transport %s.\n", endpoint_name, @@ -264,7 +269,7 @@ static int load_endpoint(const char *id, const char *endpoint_name, struct varsh } auth_name = AST_VECTOR_GET(&endpoint->inbound_auths, 0); - auth = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "auth", auth_name); + auth = ast_sorcery_retrieve_by_id(sorcery, "auth", auth_name); if (!auth) { ast_log(LOG_ERROR, "phoneprov %s contained invalid auth %s.\n", id, auth_name); return -1; @@ -283,68 +288,8 @@ static int load_endpoint(const char *id, const char *endpoint_name, struct varsh /*! \brief Callback that loads the users from phoneprov sections */ static int load_users(void) { - struct phoneprov *pp; - struct ao2_container *c; - struct ao2_iterator i; - int user_count = 0; - char port_string[6]; - - c = ast_sorcery_retrieve_by_fields(sorcery, "phoneprov", AST_RETRIEVE_FLAG_MULTIPLE, NULL); - if (!c) { - ast_log(LOG_ERROR, "Retrieve by regex failed to allocate a container.\n"); - return -1; - } - if (ao2_container_count(c) == 0) { - ast_log(LOG_ERROR, "Unable to find any phoneprov users.\n"); - ao2_cleanup(c); - return -1; - } - - i = ao2_iterator_init(c, 0); - while ((pp = ao2_iterator_next(&i))) { - const char *endpoint_name; - const char *id = ast_sorcery_object_get_id(pp); - - endpoint_name = ast_var_find(pp->vars, "endpoint"); - if (endpoint_name) { - if (load_endpoint(id, endpoint_name, pp->vars, port_string)) { - goto cleanup; - } - } - - if (!ast_var_find(pp->vars, - ast_phoneprov_std_variable_lookup(AST_PHONEPROV_STD_USERNAME))) { - assign_and_insert( - ast_phoneprov_std_variable_lookup(AST_PHONEPROV_STD_USERNAME), id, - pp->vars); - } - - if (!ast_var_find(pp->vars, - ast_phoneprov_std_variable_lookup(AST_PHONEPROV_STD_LABEL))) { - assign_and_insert(ast_phoneprov_std_variable_lookup(AST_PHONEPROV_STD_LABEL), - id, pp->vars); - } - - if (!ast_var_find(pp->vars, - ast_phoneprov_std_variable_lookup(AST_PHONEPROV_STD_SERVER_PORT))) { - assign_and_insert("SERVER_PORT", S_OR(port_string, "5060"), pp->vars); - } - - if (!ast_var_find(pp->vars, - ast_phoneprov_std_variable_lookup(AST_PHONEPROV_STD_PROFILE))) { - ast_log(LOG_ERROR, "phoneprov %s didn't contain a PROFILE entry.\n", id); - } else if (!ast_phoneprov_add_extension(AST_MODULE, pp->vars)) { - user_count++; - } - ao2_ref(pp, -1); - } - -cleanup: - ao2_iterator_destroy(&i); - ao2_cleanup(pp); - ao2_cleanup(c); - - return user_count > 0 ? 0 : -1; + ast_sorcery_reload_object(sorcery, "phoneprov"); + return 0; } /*! \brief Callback that validates the phoneprov object */ @@ -352,6 +297,8 @@ static int users_apply_handler(const struct ast_sorcery *sorcery, void *obj) { struct phoneprov *pp = obj; const char *id = ast_sorcery_object_get_id(pp); + const char *endpoint_name; + char port_string[6]; if (!ast_var_find(pp->vars, ast_phoneprov_std_variable_lookup(AST_PHONEPROV_STD_MAC))) { @@ -365,30 +312,63 @@ static int users_apply_handler(const struct ast_sorcery *sorcery, void *obj) return -1; } - return 0; + endpoint_name = ast_var_find(pp->vars, "endpoint"); + if (endpoint_name) { + if (load_endpoint(id, endpoint_name, pp->vars, port_string)) { + return -1; + } + } + + if (!ast_var_find(pp->vars, + ast_phoneprov_std_variable_lookup(AST_PHONEPROV_STD_USERNAME))) { + assign_and_insert( + ast_phoneprov_std_variable_lookup(AST_PHONEPROV_STD_USERNAME), id, + pp->vars); + } + + if (!ast_var_find(pp->vars, + ast_phoneprov_std_variable_lookup(AST_PHONEPROV_STD_LABEL))) { + assign_and_insert(ast_phoneprov_std_variable_lookup(AST_PHONEPROV_STD_LABEL), + id, pp->vars); + } + + if (!ast_var_find(pp->vars, + ast_phoneprov_std_variable_lookup(AST_PHONEPROV_STD_SERVER_PORT))) { + assign_and_insert("SERVER_PORT", S_OR(port_string, "5060"), pp->vars); + } + + if (!ast_var_find(pp->vars, + ast_phoneprov_std_variable_lookup(AST_PHONEPROV_STD_PROFILE))) { + ast_log(LOG_ERROR, "phoneprov %s didn't contain a PROFILE entry.\n", id); + } + + if (!ast_phoneprov_add_extension(AST_MODULE, pp->vars)) { + return 0; + } + + return -1; } static int load_module(void) { CHECK_PJSIP_MODULE_LOADED(); - if (!(sorcery = ast_sorcery_open())) { - ast_log(LOG_ERROR, "Unable to open a sorcery instance.\n"); - return AST_MODULE_LOAD_DECLINE; - } + sorcery = ast_sip_get_sorcery(); - ast_sorcery_apply_default(sorcery, "phoneprov", "config", "pjsip.conf,criteria=type=phoneprov"); + ast_sorcery_apply_config(sorcery, "res_pjsip_phoneprov_provider"); + ast_sorcery_apply_default(sorcery, "phoneprov", "config", + "pjsip.conf,criteria=type=phoneprov"); ast_sorcery_object_register(sorcery, "phoneprov", phoneprov_alloc, NULL, users_apply_handler); - ast_sorcery_object_field_register(sorcery, "phoneprov", "type", "", OPT_NOOP_T, 0, 0); + + ast_sorcery_object_field_register(sorcery, "phoneprov", "type", "", OPT_NOOP_T, 0, + 0); ast_sorcery_object_fields_register(sorcery, "phoneprov", "^", aco_handler, fields_handler); - ast_sorcery_reload_object(sorcery, "phoneprov"); if (ast_phoneprov_provider_register(AST_MODULE, load_users)) { ast_log(LOG_ERROR, "Unable to register pjsip phoneprov provider.\n"); - ast_sorcery_unref(sorcery); return AST_MODULE_LOAD_DECLINE; } @@ -398,15 +378,18 @@ static int load_module(void) static int unload_module(void) { ast_phoneprov_provider_unregister(AST_MODULE); - ast_sorcery_unref(sorcery); return 0; } static int reload_module(void) { - unload_module(); - load_module(); + ast_phoneprov_provider_unregister(AST_MODULE); + + if (ast_phoneprov_provider_register(AST_MODULE, load_users)) { + ast_log(LOG_ERROR, "Unable to register pjsip phoneprov provider.\n"); + return AST_MODULE_LOAD_DECLINE; + } return 0; } -- cgit v1.2.3