diff options
Diffstat (limited to 'res')
32 files changed, 10518 insertions, 2 deletions
diff --git a/res/Makefile b/res/Makefile index fec20a2e0..35d09275c 100644 --- a/res/Makefile +++ b/res/Makefile @@ -43,6 +43,9 @@ snmp/agent.o: _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_snmp) $(if $(filter res_ael_share,$(EMBEDDED_MODS)),modules.link,res_ael_share.so): ael/ael_lex.o ael/ael.tab.o ael/pval.o ael/ael_lex.o ael/ael.tab.o ael/pval.o: _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_ael_share) +$(if $(filter res_sip,$(EMBEDDED_MODS)),modules.link,res_sip.so): $(subst .c,.o,$(wildcard res_sip/*.c)) +$(subst .c,.o,$(wildcard res_sip/*.c)): _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_sip) + ifneq ($(findstring REBUILD_PARSERS,$(MENUSELECT_CFLAGS)),) ael/ael_lex.c: ael/ael.flex else @@ -67,7 +70,7 @@ endif ael/pval.o: ael/pval.c clean:: - rm -f snmp/*.[oi] ael/*.[oi] ais/*.[oi] stasis_http/*.[oi] + rm -f snmp/*.[oi] ael/*.[oi] ais/*.[oi] stasis_http/*.[oi] res_sip/*.[oi] # Dependencies for res_stasis_http_*.so are generated, so they're in this file include stasis_http.make diff --git a/res/res_sip.c b/res/res_sip.c new file mode 100644 index 000000000..18eead992 --- /dev/null +++ b/res/res_sip.c @@ -0,0 +1,906 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Mark Michelson <mmichelson@digium.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 <pjsip.h> +/* Needed for SUBSCRIBE, NOTIFY, and PUBLISH method definitions */ +#include <pjsip_simple.h> +#include <pjlib.h> + +#include "asterisk/res_sip.h" +#include "res_sip/include/res_sip_private.h" +#include "asterisk/linkedlists.h" +#include "asterisk/logger.h" +#include "asterisk/lock.h" +#include "asterisk/utils.h" +#include "asterisk/astobj2.h" +#include "asterisk/module.h" +#include "asterisk/threadpool.h" +#include "asterisk/taskprocessor.h" +#include "asterisk/uuid.h" +#include "asterisk/sorcery.h" + +/*** MODULEINFO + <depend>pjproject</depend> + <depend>res_sorcery_config</depend> + <support_level>core</support_level> + ***/ + +static pjsip_endpoint *ast_pjsip_endpoint; + +static struct ast_threadpool *sip_threadpool; + +static int register_service(void *data) +{ + pjsip_module **module = data; + if (!ast_pjsip_endpoint) { + ast_log(LOG_ERROR, "There is no PJSIP endpoint. Unable to register services\n"); + return -1; + } + if (pjsip_endpt_register_module(ast_pjsip_endpoint, *module) != PJ_SUCCESS) { + ast_log(LOG_ERROR, "Unable to register module %.*s\n", (int) pj_strlen(&(*module)->name), pj_strbuf(&(*module)->name)); + return -1; + } + ast_debug(1, "Registered SIP service %.*s (%p)\n", (int) pj_strlen(&(*module)->name), pj_strbuf(&(*module)->name), *module); + ast_module_ref(ast_module_info->self); + return 0; +} + +int ast_sip_register_service(pjsip_module *module) +{ + return ast_sip_push_task_synchronous(NULL, register_service, &module); +} + +static int unregister_service(void *data) +{ + pjsip_module **module = data; + ast_module_unref(ast_module_info->self); + if (!ast_pjsip_endpoint) { + return -1; + } + pjsip_endpt_unregister_module(ast_pjsip_endpoint, *module); + ast_debug(1, "Unregistered SIP service %.*s\n", (int) pj_strlen(&(*module)->name), pj_strbuf(&(*module)->name)); + return 0; +} + +void ast_sip_unregister_service(pjsip_module *module) +{ + ast_sip_push_task_synchronous(NULL, unregister_service, &module); +} + +static struct ast_sip_authenticator *registered_authenticator; + +int ast_sip_register_authenticator(struct ast_sip_authenticator *auth) +{ + if (registered_authenticator) { + ast_log(LOG_WARNING, "Authenticator %p is already registered. Cannot register a new one\n", registered_authenticator); + return -1; + } + registered_authenticator = auth; + ast_debug(1, "Registered SIP authenticator module %p\n", auth); + ast_module_ref(ast_module_info->self); + return 0; +} + +void ast_sip_unregister_authenticator(struct ast_sip_authenticator *auth) +{ + if (registered_authenticator != auth) { + ast_log(LOG_WARNING, "Trying to unregister authenticator %p but authenticator %p registered\n", + auth, registered_authenticator); + return; + } + registered_authenticator = NULL; + ast_debug(1, "Unregistered SIP authenticator %p\n", auth); + ast_module_unref(ast_module_info->self); +} + +int ast_sip_requires_authentication(struct ast_sip_endpoint *endpoint, pjsip_rx_data *rdata) +{ + if (!registered_authenticator) { + ast_log(LOG_WARNING, "No SIP authenticator registered. Assuming authentication is not required\n"); + return 0; + } + + return registered_authenticator->requires_authentication(endpoint, rdata); +} + +enum ast_sip_check_auth_result ast_sip_check_authentication(struct ast_sip_endpoint *endpoint, + pjsip_rx_data *rdata, pjsip_tx_data *tdata) +{ + if (!registered_authenticator) { + ast_log(LOG_WARNING, "No SIP authenticator registered. Assuming authentication is successful\n"); + return 0; + } + return registered_authenticator->check_authentication(endpoint, rdata, tdata); +} + +static struct ast_sip_outbound_authenticator *registered_outbound_authenticator; + +int ast_sip_register_outbound_authenticator(struct ast_sip_outbound_authenticator *auth) +{ + if (registered_outbound_authenticator) { + ast_log(LOG_WARNING, "Outbound authenticator %p is already registered. Cannot register a new one\n", registered_outbound_authenticator); + return -1; + } + registered_outbound_authenticator = auth; + ast_debug(1, "Registered SIP outbound authenticator module %p\n", auth); + ast_module_ref(ast_module_info->self); + return 0; +} + +void ast_sip_unregister_outbound_authenticator(struct ast_sip_outbound_authenticator *auth) +{ + if (registered_outbound_authenticator != auth) { + ast_log(LOG_WARNING, "Trying to unregister outbound authenticator %p but outbound authenticator %p registered\n", + auth, registered_outbound_authenticator); + return; + } + registered_outbound_authenticator = NULL; + ast_debug(1, "Unregistered SIP outbound authenticator %p\n", auth); + ast_module_unref(ast_module_info->self); +} + +int ast_sip_create_request_with_auth(const char **auths, size_t num_auths, pjsip_rx_data *challenge, + pjsip_transaction *tsx, pjsip_tx_data **new_request) +{ + if (!registered_outbound_authenticator) { + ast_log(LOG_WARNING, "No SIP outbound authenticator registered. Cannot respond to authentication challenge\n"); + return -1; + } + return registered_outbound_authenticator->create_request_with_auth(auths, num_auths, challenge, tsx, new_request); +} + +struct endpoint_identifier_list { + struct ast_sip_endpoint_identifier *identifier; + AST_RWLIST_ENTRY(endpoint_identifier_list) list; +}; + +static AST_RWLIST_HEAD_STATIC(endpoint_identifiers, endpoint_identifier_list); + +int ast_sip_register_endpoint_identifier(struct ast_sip_endpoint_identifier *identifier) +{ + struct endpoint_identifier_list *id_list_item; + SCOPED_LOCK(lock, &endpoint_identifiers, AST_RWLIST_WRLOCK, AST_RWLIST_UNLOCK); + + id_list_item = ast_calloc(1, sizeof(*id_list_item)); + if (!id_list_item) { + ast_log(LOG_ERROR, "Unabled to add endpoint identifier. Out of memory.\n"); + return -1; + } + id_list_item->identifier = identifier; + + AST_RWLIST_INSERT_TAIL(&endpoint_identifiers, id_list_item, list); + ast_debug(1, "Registered endpoint identifier %p\n", identifier); + + ast_module_ref(ast_module_info->self); + return 0; +} + +void ast_sip_unregister_endpoint_identifier(struct ast_sip_endpoint_identifier *identifier) +{ + struct endpoint_identifier_list *iter; + SCOPED_LOCK(lock, &endpoint_identifiers, AST_RWLIST_WRLOCK, AST_RWLIST_UNLOCK); + AST_RWLIST_TRAVERSE_SAFE_BEGIN(&endpoint_identifiers, iter, list) { + if (iter->identifier == identifier) { + AST_RWLIST_REMOVE_CURRENT(list); + ast_free(iter); + ast_debug(1, "Unregistered endpoint identifier %p\n", identifier); + ast_module_unref(ast_module_info->self); + break; + } + } + AST_RWLIST_TRAVERSE_SAFE_END; +} + +struct ast_sip_endpoint *ast_sip_identify_endpoint(pjsip_rx_data *rdata) +{ + struct endpoint_identifier_list *iter; + struct ast_sip_endpoint *endpoint = NULL; + SCOPED_LOCK(lock, &endpoint_identifiers, AST_RWLIST_RDLOCK, AST_RWLIST_UNLOCK); + AST_RWLIST_TRAVERSE(&endpoint_identifiers, iter, list) { + ast_assert(iter->identifier->identify_endpoint != NULL); + endpoint = iter->identifier->identify_endpoint(rdata); + if (endpoint) { + break; + } + } + return endpoint; +} + +pjsip_endpoint *ast_sip_get_pjsip_endpoint(void) +{ + return ast_pjsip_endpoint; +} + +static int sip_dialog_create_from(pj_pool_t *pool, pj_str_t *from, const char *user, const pj_str_t *target, pjsip_tpselector *selector) +{ + pj_str_t tmp, local_addr; + pjsip_uri *uri; + pjsip_sip_uri *sip_uri; + pjsip_transport_type_e type = PJSIP_TRANSPORT_UNSPECIFIED; + int local_port; + char uuid_str[AST_UUID_STR_LEN]; + + if (!user) { + RAII_VAR(struct ast_uuid *, uuid, ast_uuid_generate(), ast_free_ptr); + if (!uuid) { + return -1; + } + user = ast_uuid_to_str(uuid, uuid_str, sizeof(uuid_str)); + } + + /* Parse the provided target URI so we can determine what transport it will end up using */ + pj_strdup_with_null(pool, &tmp, target); + + if (!(uri = pjsip_parse_uri(pool, tmp.ptr, tmp.slen, 0)) || + (!PJSIP_URI_SCHEME_IS_SIP(uri) && !PJSIP_URI_SCHEME_IS_SIPS(uri))) { + return -1; + } + + sip_uri = pjsip_uri_get_uri(uri); + + /* Determine the transport type to use */ + if (PJSIP_URI_SCHEME_IS_SIPS(sip_uri)) { + type = PJSIP_TRANSPORT_TLS; + } else if (!sip_uri->transport_param.slen) { + type = PJSIP_TRANSPORT_UDP; + } else { + type = pjsip_transport_get_type_from_name(&sip_uri->transport_param); + } + + if (type == PJSIP_TRANSPORT_UNSPECIFIED) { + return -1; + } + + /* If the host is IPv6 turn the transport into an IPv6 version */ + if (pj_strchr(&sip_uri->host, ':')) { + type = (pjsip_transport_type_e)(((int)type) + PJSIP_TRANSPORT_IPV6); + } + + /* Get the local bound address for the transport that will be used when communicating with the provided URI */ + if (pjsip_tpmgr_find_local_addr(pjsip_endpt_get_tpmgr(ast_sip_get_pjsip_endpoint()), pool, type, selector, + &local_addr, &local_port) != PJ_SUCCESS) { + return -1; + } + + /* If IPv6 was not specified in the host but is in the transport, set the proper type */ + if (!pj_strchr(&sip_uri->host, ':') && pj_strchr(&local_addr, ':')) { + type = (pjsip_transport_type_e)(((int)type) + PJSIP_TRANSPORT_IPV6); + } + + from->ptr = pj_pool_alloc(pool, PJSIP_MAX_URL_SIZE); + from->slen = pj_ansi_snprintf(from->ptr, PJSIP_MAX_URL_SIZE, + "<%s:%s@%s%.*s%s:%d%s%s>", + (pjsip_transport_get_flag_from_type(type) & PJSIP_TRANSPORT_SECURE) ? "sips" : "sip", + user, + (type & PJSIP_TRANSPORT_IPV6) ? "[" : "", + (int)local_addr.slen, + local_addr.ptr, + (type & PJSIP_TRANSPORT_IPV6) ? "]" : "", + local_port, + (type != PJSIP_TRANSPORT_UDP && type != PJSIP_TRANSPORT_UDP6) ? ";transport=" : "", + (type != PJSIP_TRANSPORT_UDP && type != PJSIP_TRANSPORT_UDP6) ? pjsip_transport_get_type_name(type) : ""); + + return 0; +} + +static int sip_get_tpselector_from_endpoint(const struct ast_sip_endpoint *endpoint, pjsip_tpselector *selector) +{ + RAII_VAR(struct ast_sip_transport *, transport, NULL, ao2_cleanup); + const char *transport_name = endpoint->transport; + + if (ast_strlen_zero(transport_name)) { + return 0; + } + + transport = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "transport", transport_name); + + if (!transport || !transport->state) { + return -1; + } + + if (transport->type == AST_SIP_TRANSPORT_UDP) { + selector->type = PJSIP_TPSELECTOR_TRANSPORT; + selector->u.transport = transport->state->transport; + } else if (transport->type == AST_SIP_TRANSPORT_TCP || transport->type == AST_SIP_TRANSPORT_TLS) { + selector->type = PJSIP_TPSELECTOR_LISTENER; + selector->u.listener = transport->state->factory; + } else { + return -1; + } + + return 0; +} + +pjsip_dialog *ast_sip_create_dialog(const struct ast_sip_endpoint *endpoint, const char *uri, const char *request_user) +{ + pj_str_t local_uri = { "sip:temp@temp", 13 }, remote_uri; + pjsip_dialog *dlg = NULL; + const char *outbound_proxy = endpoint->outbound_proxy; + pjsip_tpselector selector = { .type = PJSIP_TPSELECTOR_NONE, }; + static const pj_str_t HCONTACT = { "Contact", 7 }; + + pj_cstr(&remote_uri, uri); + + if (pjsip_dlg_create_uac(pjsip_ua_instance(), &local_uri, NULL, &remote_uri, NULL, &dlg) != PJ_SUCCESS) { + return NULL; + } + + if (sip_get_tpselector_from_endpoint(endpoint, &selector)) { + pjsip_dlg_terminate(dlg); + return NULL; + } + + if (sip_dialog_create_from(dlg->pool, &local_uri, NULL, &remote_uri, &selector)) { + pjsip_dlg_terminate(dlg); + return NULL; + } + + /* Update the dialog with the new local URI, we do it afterwards so we can use the dialog pool for construction */ + pj_strdup_with_null(dlg->pool, &dlg->local.info_str, &local_uri); + dlg->local.info->uri = pjsip_parse_uri(dlg->pool, dlg->local.info_str.ptr, dlg->local.info_str.slen, 0); + dlg->local.contact = pjsip_parse_hdr(dlg->pool, &HCONTACT, local_uri.ptr, local_uri.slen, NULL); + + /* If a request user has been specified and we are permitted to change it, do so */ + if (!ast_strlen_zero(request_user) && (PJSIP_URI_SCHEME_IS_SIP(dlg->target) || PJSIP_URI_SCHEME_IS_SIPS(dlg->target))) { + pjsip_sip_uri *target = pjsip_uri_get_uri(dlg->target); + pj_strdup2(dlg->pool, &target->user, request_user); + } + + /* We have to temporarily bump up the sess_count here so the dialog is not prematurely destroyed */ + dlg->sess_count++; + + pjsip_dlg_set_transport(dlg, &selector); + + if (!ast_strlen_zero(outbound_proxy)) { + pjsip_route_hdr route_set, *route; + static const pj_str_t ROUTE_HNAME = { "Route", 5 }; + pj_str_t tmp; + + pj_list_init(&route_set); + + pj_strdup2_with_null(dlg->pool, &tmp, outbound_proxy); + if (!(route = pjsip_parse_hdr(dlg->pool, &ROUTE_HNAME, tmp.ptr, tmp.slen, NULL))) { + pjsip_dlg_terminate(dlg); + return NULL; + } + pj_list_push_back(&route_set, route); + + pjsip_dlg_set_route_set(dlg, &route_set); + } + + dlg->sess_count--; + + return dlg; +} + +/* PJSIP doesn't know about the INFO method, so we have to define it ourselves */ +const pjsip_method pjsip_info_method = {PJSIP_OTHER_METHOD, {"INFO", 4} }; + +static struct { + const char *method; + const pjsip_method *pmethod; +} methods [] = { + { "INVITE", &pjsip_invite_method }, + { "CANCEL", &pjsip_cancel_method }, + { "ACK", &pjsip_ack_method }, + { "BYE", &pjsip_bye_method }, + { "REGISTER", &pjsip_register_method }, + { "OPTIONS", &pjsip_options_method }, + { "SUBSCRIBE", &pjsip_subscribe_method }, + { "NOTIFY", &pjsip_notify_method }, + { "PUBLISH", &pjsip_publish_method }, + { "INFO", &pjsip_info_method }, +}; + +static const pjsip_method *get_pjsip_method(const char *method) +{ + int i; + for (i = 0; i < ARRAY_LEN(methods); ++i) { + if (!strcmp(method, methods[i].method)) { + return methods[i].pmethod; + } + } + return NULL; +} + +static int create_in_dialog_request(const pjsip_method *method, struct pjsip_dialog *dlg, pjsip_tx_data **tdata) +{ + if (pjsip_dlg_create_request(dlg, method, -1, tdata) != PJ_SUCCESS) { + ast_log(LOG_WARNING, "Unable to create in-dialog request.\n"); + return -1; + } + + return 0; +} + +static int create_out_of_dialog_request(const pjsip_method *method, struct ast_sip_endpoint *endpoint, + const char *uri, pjsip_tx_data **tdata) +{ + RAII_VAR(struct ast_sip_contact *, contact, NULL, ao2_cleanup); + pj_str_t remote_uri; + pj_str_t from; + pj_pool_t *pool; + pjsip_tpselector selector = { .type = PJSIP_TPSELECTOR_NONE, }; + + if (ast_strlen_zero(uri)) { + contact = ast_sip_location_retrieve_contact_from_aor_list(endpoint->aors); + if (!contact || ast_strlen_zero(contact->uri)) { + ast_log(LOG_ERROR, "Unable to retrieve contact for endpoint %s\n", + ast_sorcery_object_get_id(endpoint)); + return -1; + } + + pj_cstr(&remote_uri, contact->uri); + } else { + pj_cstr(&remote_uri, uri); + } + + if (sip_get_tpselector_from_endpoint(endpoint, &selector)) { + ast_log(LOG_ERROR, "Unable to retrieve PJSIP transport selector for endpoint %s\n", + ast_sorcery_object_get_id(endpoint)); + return -1; + } + + pool = pjsip_endpt_create_pool(ast_sip_get_pjsip_endpoint(), "Outbound request", 256, 256); + + if (!pool) { + ast_log(LOG_ERROR, "Unable to create PJLIB memory pool\n"); + return -1; + } + + if (sip_dialog_create_from(pool, &from, NULL, &remote_uri, &selector)) { + ast_log(LOG_ERROR, "Unable to create From header for %.*s request to endpoint %s\n", + (int) pj_strlen(&method->name), pj_strbuf(&method->name), ast_sorcery_object_get_id(endpoint)); + pjsip_endpt_release_pool(ast_sip_get_pjsip_endpoint(), pool); + return -1; + } + + if (pjsip_endpt_create_request(ast_sip_get_pjsip_endpoint(), method, &remote_uri, + &from, &remote_uri, &from, NULL, -1, NULL, tdata) != PJ_SUCCESS) { + ast_log(LOG_ERROR, "Unable to create outbound %.*s request to endpoint %s\n", + (int) pj_strlen(&method->name), pj_strbuf(&method->name), ast_sorcery_object_get_id(endpoint)); + pjsip_endpt_release_pool(ast_sip_get_pjsip_endpoint(), pool); + return -1; + } + + /* We can release this pool since request creation copied all the necessary + * data into the outbound request's pool + */ + pjsip_endpt_release_pool(ast_sip_get_pjsip_endpoint(), pool); + return 0; +} + +int ast_sip_create_request(const char *method, struct pjsip_dialog *dlg, + struct ast_sip_endpoint *endpoint, const char *uri, pjsip_tx_data **tdata) +{ + const pjsip_method *pmethod = get_pjsip_method(method); + + if (!pmethod) { + ast_log(LOG_WARNING, "Unknown method '%s'. Cannot send request\n", method); + return -1; + } + + if (dlg) { + return create_in_dialog_request(pmethod, dlg, tdata); + } else { + return create_out_of_dialog_request(pmethod, endpoint, uri, tdata); + } +} + +static int send_in_dialog_request(pjsip_tx_data *tdata, struct pjsip_dialog *dlg) +{ + if (pjsip_dlg_send_request(dlg, tdata, -1, NULL) != PJ_SUCCESS) { + ast_log(LOG_WARNING, "Unable to send in-dialog request.\n"); + return -1; + } + return 0; +} + +static void send_request_cb(void *token, pjsip_event *e) +{ + RAII_VAR(struct ast_sip_endpoint *, endpoint, token, ao2_cleanup); + pjsip_transaction *tsx = e->body.tsx_state.tsx; + pjsip_rx_data *challenge = e->body.tsx_state.src.rdata; + pjsip_tx_data *tdata; + + if (tsx->status_code != 401 && tsx->status_code != 407) { + return; + } + + if (!ast_sip_create_request_with_auth(endpoint->sip_outbound_auths, endpoint->num_outbound_auths, challenge, tsx, &tdata)) { + pjsip_endpt_send_request(ast_sip_get_pjsip_endpoint(), tdata, -1, NULL, NULL); + } +} + +static int send_out_of_dialog_request(pjsip_tx_data *tdata, struct ast_sip_endpoint *endpoint) +{ + ao2_ref(endpoint, +1); + if (pjsip_endpt_send_request(ast_sip_get_pjsip_endpoint(), tdata, -1, endpoint, send_request_cb) != PJ_SUCCESS) { + ast_log(LOG_ERROR, "Error attempting to send outbound %.*s request to endpoint %s\n", + (int) pj_strlen(&tdata->msg->line.req.method.name), + pj_strbuf(&tdata->msg->line.req.method.name), + ast_sorcery_object_get_id(endpoint)); + ao2_ref(endpoint, -1); + return -1; + } + + return 0; +} + +int ast_sip_send_request(pjsip_tx_data *tdata, struct pjsip_dialog *dlg, struct ast_sip_endpoint *endpoint) +{ + ast_assert(tdata->msg->type == PJSIP_REQUEST_MSG); + + if (dlg) { + return send_in_dialog_request(tdata, dlg); + } else { + return send_out_of_dialog_request(tdata, endpoint); + } +} + +int ast_sip_add_header(pjsip_tx_data *tdata, const char *name, const char *value) +{ + pj_str_t hdr_name; + pj_str_t hdr_value; + pjsip_generic_string_hdr *hdr; + + pj_cstr(&hdr_name, name); + pj_cstr(&hdr_value, value); + + hdr = pjsip_generic_string_hdr_create(tdata->pool, &hdr_name, &hdr_value); + + pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr *) hdr); + return 0; +} + +static pjsip_msg_body *ast_body_to_pjsip_body(pj_pool_t *pool, const struct ast_sip_body *body) +{ + pj_str_t type; + pj_str_t subtype; + pj_str_t body_text; + + pj_cstr(&type, body->type); + pj_cstr(&subtype, body->subtype); + pj_cstr(&body_text, body->body_text); + + return pjsip_msg_body_create(pool, &type, &subtype, &body_text); +} + +int ast_sip_add_body(pjsip_tx_data *tdata, const struct ast_sip_body *body) +{ + pjsip_msg_body *pjsip_body = ast_body_to_pjsip_body(tdata->pool, body); + tdata->msg->body = pjsip_body; + return 0; +} + +int ast_sip_add_body_multipart(pjsip_tx_data *tdata, const struct ast_sip_body *bodies[], int num_bodies) +{ + int i; + /* NULL for type and subtype automatically creates "multipart/mixed" */ + pjsip_msg_body *body = pjsip_multipart_create(tdata->pool, NULL, NULL); + + for (i = 0; i < num_bodies; ++i) { + pjsip_multipart_part *part = pjsip_multipart_create_part(tdata->pool); + part->body = ast_body_to_pjsip_body(tdata->pool, bodies[i]); + pjsip_multipart_add_part(tdata->pool, body, part); + } + + tdata->msg->body = body; + return 0; +} + +int ast_sip_append_body(pjsip_tx_data *tdata, const char *body_text) +{ + size_t combined_size = strlen(body_text) + tdata->msg->body->len; + struct ast_str *body_buffer = ast_str_alloca(combined_size); + + ast_str_set(&body_buffer, 0, "%.*s%s", (int) tdata->msg->body->len, (char *) tdata->msg->body->data, body_text); + + tdata->msg->body->data = pj_pool_alloc(tdata->pool, combined_size); + pj_memcpy(tdata->msg->body->data, ast_str_buffer(body_buffer), combined_size); + tdata->msg->body->len = combined_size; + + return 0; +} + +struct ast_taskprocessor *ast_sip_create_serializer(void) +{ + struct ast_taskprocessor *serializer; + RAII_VAR(struct ast_uuid *, uuid, ast_uuid_generate(), ast_free_ptr); + char name[AST_UUID_STR_LEN]; + + if (!uuid) { + return NULL; + } + + ast_uuid_to_str(uuid, name, sizeof(name)); + + serializer = ast_threadpool_serializer(name, sip_threadpool); + if (!serializer) { + return NULL; + } + return serializer; +} + +int ast_sip_push_task(struct ast_taskprocessor *serializer, int (*sip_task)(void *), void *task_data) +{ + if (serializer) { + return ast_taskprocessor_push(serializer, sip_task, task_data); + } else { + return ast_threadpool_push(sip_threadpool, sip_task, task_data); + } +} + +struct sync_task_data { + ast_mutex_t lock; + ast_cond_t cond; + int complete; + int fail; + int (*task)(void *); + void *task_data; +}; + +static int sync_task(void *data) +{ + struct sync_task_data *std = data; + std->fail = std->task(std->task_data); + + ast_mutex_lock(&std->lock); + std->complete = 1; + ast_cond_signal(&std->cond); + ast_mutex_unlock(&std->lock); + return std->fail; +} + +int ast_sip_push_task_synchronous(struct ast_taskprocessor *serializer, int (*sip_task)(void *), void *task_data) +{ + /* This method is an onion */ + struct sync_task_data std; + ast_mutex_init(&std.lock); + ast_cond_init(&std.cond, NULL); + std.fail = std.complete = 0; + std.task = sip_task; + std.task_data = task_data; + + if (serializer) { + if (ast_taskprocessor_push(serializer, sync_task, &std)) { + return -1; + } + } else { + if (ast_threadpool_push(sip_threadpool, sync_task, &std)) { + return -1; + } + } + + ast_mutex_lock(&std.lock); + while (!std.complete) { + ast_cond_wait(&std.cond, &std.lock); + } + ast_mutex_unlock(&std.lock); + + ast_mutex_destroy(&std.lock); + ast_cond_destroy(&std.cond); + return std.fail; +} + +void ast_copy_pj_str(char *dest, pj_str_t *src, size_t size) +{ + size_t chars_to_copy = MIN(size - 1, pj_strlen(src)); + memcpy(dest, pj_strbuf(src), chars_to_copy); + dest[chars_to_copy] = '\0'; +} + +pj_caching_pool caching_pool; +pj_pool_t *memory_pool; +pj_thread_t *monitor_thread; +static int monitor_continue; + +static void *monitor_thread_exec(void *endpt) +{ + while (monitor_continue) { + const pj_time_val delay = {0, 10}; + pjsip_endpt_handle_events(ast_pjsip_endpoint, &delay); + } + return NULL; +} + +static void stop_monitor_thread(void) +{ + monitor_continue = 0; + pj_thread_join(monitor_thread); +} + +AST_THREADSTORAGE(pj_thread_storage); +AST_THREADSTORAGE(servant_id_storage); +#define SIP_SERVANT_ID 0xDEFECA7E + +static void sip_thread_start(void) +{ + pj_thread_desc *desc; + pj_thread_t *thread; + uint32_t *servant_id; + + servant_id = ast_threadstorage_get(&servant_id_storage, sizeof(*servant_id)); + if (!servant_id) { + ast_log(LOG_ERROR, "Could not set SIP servant ID in thread-local storage.\n"); + return; + } + *servant_id = SIP_SERVANT_ID; + + desc = ast_threadstorage_get(&pj_thread_storage, sizeof(pj_thread_desc)); + if (!desc) { + ast_log(LOG_ERROR, "Could not get thread desc from thread-local storage. Expect awful things to occur\n"); + return; + } + pj_bzero(*desc, sizeof(*desc)); + + if (pj_thread_register("Asterisk Thread", *desc, &thread) != PJ_SUCCESS) { + ast_log(LOG_ERROR, "Couldn't register thread with PJLIB.\n"); + } +} + +int ast_sip_thread_is_servant(void) +{ + uint32_t *servant_id; + + servant_id = ast_threadstorage_get(&servant_id_storage, sizeof(*servant_id)); + if (!servant_id) { + return 0; + } + + return *servant_id == SIP_SERVANT_ID; +} + +static int load_module(void) +{ + /* The third parameter is just copied from + * example code from PJLIB. This can be adjusted + * if necessary. + */ + pj_status_t status; + + /* XXX For the time being, create hard-coded threadpool + * options. Just bump up by five threads every time we + * don't have any available threads. Idle threads time + * out after a minute. No maximum size + */ + struct ast_threadpool_options options = { + .version = AST_THREADPOOL_OPTIONS_VERSION, + .auto_increment = 5, + .max_size = 0, + .idle_timeout = 60, + .initial_size = 0, + .thread_start = sip_thread_start, + }; + sip_threadpool = ast_threadpool_create("SIP", NULL, &options); + + if (pj_init() != PJ_SUCCESS) { + return AST_MODULE_LOAD_DECLINE; + } + + if (pjlib_util_init() != PJ_SUCCESS) { + pj_shutdown(); + return AST_MODULE_LOAD_DECLINE; + } + + pj_caching_pool_init(&caching_pool, NULL, 1024 * 1024); + if (pjsip_endpt_create(&caching_pool.factory, "SIP", &ast_pjsip_endpoint) != PJ_SUCCESS) { + ast_log(LOG_ERROR, "Failed to create PJSIP endpoint structure. Aborting load\n"); + goto error; + } + memory_pool = pj_pool_create(&caching_pool.factory, "SIP", 1024, 1024, NULL); + if (!memory_pool) { + ast_log(LOG_ERROR, "Failed to create memory pool for SIP. Aborting load\n"); + goto error; + } + + pjsip_tsx_layer_init_module(ast_pjsip_endpoint); + pjsip_ua_init_module(ast_pjsip_endpoint, NULL); + + monitor_continue = 1; + status = pj_thread_create(memory_pool, "SIP", (pj_thread_proc *) &monitor_thread_exec, + NULL, PJ_THREAD_DEFAULT_STACK_SIZE * 2, 0, &monitor_thread); + if (status != PJ_SUCCESS) { + ast_log(LOG_ERROR, "Failed to start SIP monitor thread. Aborting load\n"); + goto error; + } + + if (ast_res_sip_initialize_configuration()) { + ast_log(LOG_ERROR, "Failed to initialize SIP configuration. Aborting load\n"); + goto error; + } + + if (ast_sip_initialize_distributor()) { + ast_log(LOG_ERROR, "Failed to register distributor module. Aborting load\n"); + goto error; + } + + if (ast_sip_initialize_outbound_authentication()) { + ast_log(LOG_ERROR, "Failed to initialize outbound authentication. Aborting load\n"); + goto error; + } + + ast_res_sip_init_options_handling(0); + +return AST_MODULE_LOAD_SUCCESS; + +error: + ast_res_sip_destroy_configuration(); + if (monitor_thread) { + stop_monitor_thread(); + } + if (memory_pool) { + pj_pool_release(memory_pool); + memory_pool = NULL; + } + if (ast_pjsip_endpoint) { + pjsip_endpt_destroy(ast_pjsip_endpoint); + ast_pjsip_endpoint = NULL; + } + pj_caching_pool_destroy(&caching_pool); + /* XXX Should have a way of stopping monitor thread */ + return AST_MODULE_LOAD_DECLINE; +} + +static int reload_module(void) +{ + if (ast_res_sip_reload_configuration()) { + return AST_MODULE_LOAD_DECLINE; + } + ast_res_sip_init_options_handling(1); + return 0; +} + +static int unload_pjsip(void *data) +{ + if (memory_pool) { + pj_pool_release(memory_pool); + memory_pool = NULL; + } + if (ast_pjsip_endpoint) { + pjsip_endpt_destroy(ast_pjsip_endpoint); + ast_pjsip_endpoint = NULL; + } + pj_caching_pool_destroy(&caching_pool); + return 0; +} + +static int unload_module(void) +{ + ast_res_sip_destroy_configuration(); + if (monitor_thread) { + stop_monitor_thread(); + } + /* The thread this is called from cannot call PJSIP/PJLIB functions, + * so we have to push the work to the threadpool to handle + */ + ast_sip_push_task_synchronous(NULL, unload_pjsip, NULL); + + ast_threadpool_shutdown(sip_threadpool); + + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "Basic SIP resource", + .load = load_module, + .unload = unload_module, + .reload = reload_module, + .load_pri = AST_MODPRI_CHANNEL_DEPEND, +); diff --git a/res/res_sip.exports.in b/res/res_sip.exports.in new file mode 100644 index 000000000..010f90cb1 --- /dev/null +++ b/res/res_sip.exports.in @@ -0,0 +1,52 @@ +{ + global: + LINKER_SYMBOL_PREFIXast_sip_register_service; + LINKER_SYMBOL_PREFIXast_sip_unregister_service; + LINKER_SYMBOL_PREFIXast_sip_register_authenticator; + LINKER_SYMBOL_PREFIXast_sip_unregister_authenticator; + LINKER_SYMBOL_PREFIXast_sip_register_outbound_authenticator; + LINKER_SYMBOL_PREFIXast_sip_unregister_outbound_authenticator; + LINKER_SYMBOL_PREFIXast_sip_register_endpoint_identifier; + LINKER_SYMBOL_PREFIXast_sip_unregister_endpoint_identifier; + LINKER_SYMBOL_PREFIXast_sip_create_serializer; + LINKER_SYMBOL_PREFIXast_sip_push_task; + LINKER_SYMBOL_PREFIXast_sip_push_task_synchronous; + LINKER_SYMBOL_PREFIXast_sip_create_request; + LINKER_SYMBOL_PREFIXast_sip_create_request_with_auth; + LINKER_SYMBOL_PREFIXast_sip_send_request; + LINKER_SYMBOL_PREFIXast_sip_requires_authentication; + LINKER_SYMBOL_PREFIXast_sip_authenticate_request; + LINKER_SYMBOL_PREFIXast_sip_get_authentication_credentials; + LINKER_SYMBOL_PREFIXast_sip_check_authentication; + LINKER_SYMBOL_PREFIXast_sip_create_auth_challenge_response; + LINKER_SYMBOL_PREFIXast_sip_set_outbound_authentication_credentials; + LINKER_SYMBOL_PREFIXast_sip_dialog_setup_outbound_authentication; + LINKER_SYMBOL_PREFIXast_sip_add_digest_to_challenge; + LINKER_SYMBOL_PREFIXast_sip_identify_endpoint; + LINKER_SYMBOL_PREFIXast_sip_add_header; + LINKER_SYMBOL_PREFIXast_sip_add_body; + LINKER_SYMBOL_PREFIXast_sip_add_body_multipart; + LINKER_SYMBOL_PREFIXast_sip_append_body; + LINKER_SYMBOL_PREFIXast_sip_get_pjsip_endpoint; + LINKER_SYMBOL_PREFIXast_sip_endpoint_alloc; + LINKER_SYMBOL_PREFIXast_copy_pj_str; + LINKER_SYMBOL_PREFIXast_sip_get_sorcery; + LINKER_SYMBOL_PREFIXast_sip_create_dialog; + LINKER_SYMBOL_PREFIXast_sip_location_retrieve_aor; + LINKER_SYMBOL_PREFIXast_sip_location_retrieve_first_aor_contact; + LINKER_SYMBOL_PREFIXast_sip_location_retrieve_contact_from_aor_list; + LINKER_SYMBOL_PREFIXast_sip_location_retrieve_aor_contacts; + LINKER_SYMBOL_PREFIXast_sip_location_retrieve_contact; + LINKER_SYMBOL_PREFIXast_sip_location_add_contact; + LINKER_SYMBOL_PREFIXast_sip_location_update_contact; + LINKER_SYMBOL_PREFIXast_sip_location_delete_contact; + LINKER_SYMBOL_PREFIXast_pjsip_rdata_get_endpoint; + LINKER_SYMBOL_PREFIXast_sip_thread_is_servant; + LINKER_SYMBOL_PREFIXast_sip_dialog_set_serializer; + LINKER_SYMBOL_PREFIXast_sip_dialog_set_endpoint; + LINKER_SYMBOL_PREFIXast_sip_dialog_get_endpoint; + LINKER_SYMBOL_PREFIXast_sip_retrieve_auths; + LINKER_SYMBOL_PREFIXast_sip_cleanup_auths; + local: + *; +}; diff --git a/res/res_sip/config_auth.c b/res/res_sip/config_auth.c new file mode 100644 index 000000000..9881dd8c6 --- /dev/null +++ b/res/res_sip/config_auth.c @@ -0,0 +1,120 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Mark Michelson <mmichelson@digium.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 <pjsip.h> +#include <pjlib.h> +#include "asterisk/res_sip.h" +#include "asterisk/logger.h" +#include "asterisk/sorcery.h" + +static void auth_destroy(void *obj) +{ + struct ast_sip_auth *auth = obj; + ast_string_field_free_memory(auth); +} + +static void *auth_alloc(const char *name) +{ + struct ast_sip_auth *auth = ao2_alloc(sizeof(*auth), auth_destroy); + + if (!auth) { + return NULL; + } + + if (ast_string_field_init(auth, 64)) { + ao2_cleanup(auth); + return NULL; + } + + return auth; +} + +static int auth_type_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_auth *auth = obj; + if (!strcasecmp(var->value, "userpass")) { + auth->type = AST_SIP_AUTH_TYPE_USER_PASS; + } else if (!strcasecmp(var->value, "md5")) { + auth->type = AST_SIP_AUTH_TYPE_MD5; + } else { + ast_log(LOG_WARNING, "Unknown authentication storage type '%s' specified for %s\n", + var->value, var->name); + return -1; + } + return 0; +} + +static int auth_apply(const struct ast_sorcery *sorcery, void *obj) +{ + struct ast_sip_auth *auth = obj; + int res = 0; + + if (ast_strlen_zero(auth->auth_user)) { + ast_log(LOG_ERROR, "No authentication username for auth '%s'\n", + ast_sorcery_object_get_id(auth)); + return -1; + } + + switch (auth->type) { + case AST_SIP_AUTH_TYPE_USER_PASS: + if (ast_strlen_zero(auth->auth_pass)) { + ast_log(LOG_ERROR, "'userpass' authentication specified but no" + "password specified for auth '%s'\n", ast_sorcery_object_get_id(auth)); + res = -1; + } + break; + case AST_SIP_AUTH_TYPE_MD5: + if (ast_strlen_zero(auth->md5_creds)) { + ast_log(LOG_ERROR, "'md5' authentication specified but no md5_cred" + "specified for auth '%s'\n", ast_sorcery_object_get_id(auth)); + res = -1; + } + break; + } + + return res; +} + +/*! \brief Initialize sorcery with auth support */ +int ast_sip_initialize_sorcery_auth(struct ast_sorcery *sorcery) +{ + ast_sorcery_apply_default(sorcery, SIP_SORCERY_AUTH_TYPE, "config", "res_sip.conf,criteria=type=auth"); + + if (ast_sorcery_object_register(sorcery, SIP_SORCERY_AUTH_TYPE, auth_alloc, NULL, auth_apply)) { + return -1; + } + + ast_sorcery_object_field_register(sorcery, SIP_SORCERY_AUTH_TYPE, "type", "", + OPT_NOOP_T, 0, 0); + ast_sorcery_object_field_register(sorcery, SIP_SORCERY_AUTH_TYPE, "username", + "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_auth, auth_user)); + ast_sorcery_object_field_register(sorcery, SIP_SORCERY_AUTH_TYPE, "password", + "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_auth, auth_pass)); + ast_sorcery_object_field_register(sorcery, SIP_SORCERY_AUTH_TYPE, "md5_cred", + "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_auth, md5_creds)); + ast_sorcery_object_field_register(sorcery, SIP_SORCERY_AUTH_TYPE, "realm", + "asterisk", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_auth, realm)); + ast_sorcery_object_field_register(sorcery, SIP_SORCERY_AUTH_TYPE, "nonce_lifetime", + "32", OPT_UINT_T, 0, FLDSET(struct ast_sip_auth, nonce_lifetime)); + ast_sorcery_object_field_register_custom(sorcery, SIP_SORCERY_AUTH_TYPE, "auth_type", + "userpass", auth_type_handler, NULL, 0, 0); + + return 0; +} diff --git a/res/res_sip/config_domain_aliases.c b/res/res_sip/config_domain_aliases.c new file mode 100644 index 000000000..86b4636ea --- /dev/null +++ b/res/res_sip/config_domain_aliases.c @@ -0,0 +1,65 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Joshua Colp <jcolp@digium.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 "pjsip.h" +#include "pjlib.h" +#include "asterisk/res_sip.h" +#include "asterisk/logger.h" +#include "asterisk/sorcery.h" + +static void domain_alias_destroy(void *obj) +{ + struct ast_sip_domain_alias *alias = obj; + + ast_string_field_free_memory(alias); +} + +static void *domain_alias_alloc(const char *name) +{ + struct ast_sip_domain_alias *alias = ao2_alloc(sizeof(*alias), domain_alias_destroy); + + if (!alias) { + return NULL; + } + + if (ast_string_field_init(alias, 256)) { + ao2_cleanup(alias); + return NULL; + } + + return alias; +} + +/*! \brief Initialize sorcery with domain alias support */ +int ast_sip_initialize_sorcery_domain_alias(struct ast_sorcery *sorcery) +{ + ast_sorcery_apply_default(sorcery, SIP_SORCERY_DOMAIN_ALIAS_TYPE, "config", "res_sip.conf,criteria=type=domain_alias"); + + if (ast_sorcery_object_register(sorcery, SIP_SORCERY_DOMAIN_ALIAS_TYPE, domain_alias_alloc, NULL, NULL)) { + return -1; + } + + ast_sorcery_object_field_register(sorcery, SIP_SORCERY_DOMAIN_ALIAS_TYPE, "type", "", + OPT_NOOP_T, 0, 0); + ast_sorcery_object_field_register(sorcery, SIP_SORCERY_DOMAIN_ALIAS_TYPE, "domain", + "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_domain_alias, domain)); + + return 0; +} diff --git a/res/res_sip/config_transport.c b/res/res_sip/config_transport.c new file mode 100644 index 000000000..eb89ee44e --- /dev/null +++ b/res/res_sip/config_transport.c @@ -0,0 +1,299 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Joshua Colp <jcolp@digium.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 <pjsip.h> +#include <pjlib.h> + +#include "asterisk/res_sip.h" +#include "asterisk/logger.h" +#include "asterisk/astobj2.h" +#include "asterisk/sorcery.h" +#include "asterisk/acl.h" + +static int destroy_transport_state(void *data) +{ + pjsip_transport *transport = data; + pjsip_transport_shutdown(transport); + return 0; +} + +/*! \brief Destructor for transport state information */ +static void transport_state_destroy(void *obj) +{ + struct ast_sip_transport_state *state = obj; + + if (state->transport) { + ast_sip_push_task_synchronous(NULL, destroy_transport_state, state->transport); + } +} + +/*! \brief Destructor for transport */ +static void transport_destroy(void *obj) +{ + struct ast_sip_transport *transport = obj; + + ast_string_field_free_memory(transport); + ast_free_ha(transport->localnet); + + if (transport->external_address_refresher) { + ast_dnsmgr_release(transport->external_address_refresher); + } + + ao2_cleanup(transport->state); +} + +/*! \brief Allocator for transport */ +static void *transport_alloc(const char *name) +{ + struct ast_sip_transport *transport = ao2_alloc(sizeof(*transport), transport_destroy); + + if (!transport) { + return NULL; + } + + if (ast_string_field_init(transport, 256)) { + ao2_cleanup(transport); + return NULL; + } + + pjsip_tls_setting_default(&transport->tls); + transport->tls.ciphers = transport->ciphers; + + return transport; +} + +/*! \brief Apply handler for transports */ +static int transport_apply(const struct ast_sorcery *sorcery, void *obj) +{ + struct ast_sip_transport *transport = obj; + RAII_VAR(struct ast_sip_transport *, existing, ast_sorcery_retrieve_by_id(sorcery, "transport", ast_sorcery_object_get_id(obj)), ao2_cleanup); + pj_status_t res = -1; + + if (!existing || !existing->state) { + if (!(transport->state = ao2_alloc(sizeof(*transport->state), transport_state_destroy))) { + ast_log(LOG_ERROR, "Transport state for '%s' could not be allocated\n", ast_sorcery_object_get_id(obj)); + return -1; + } + } else { + transport->state = existing->state; + ao2_ref(transport->state, +1); + } + + /* Once active a transport can not be reconfigured */ + if (transport->state->transport || transport->state->factory) { + return -1; + } + + /* Set default port if not present */ + if (!pj_sockaddr_get_port(&transport->host)) { + pj_sockaddr_set_port(&transport->host, (transport->type == AST_SIP_TRANSPORT_TLS) ? 5061 : 5060); + } + + /* Now that we know what address family we can set up a dnsmgr refresh for the external media address if present */ + if (!ast_strlen_zero(transport->external_signaling_address)) { + if (transport->host.addr.sa_family == pj_AF_INET()) { + transport->external_address.ss.ss_family = AF_INET; + } else if (transport->host.addr.sa_family == pj_AF_INET6()) { + transport->external_address.ss.ss_family = AF_INET6; + } else { + ast_log(LOG_ERROR, "Unknown address family for transport '%s', could not get external signaling address\n", + ast_sorcery_object_get_id(obj)); + return -1; + } + + if (ast_dnsmgr_lookup(transport->external_signaling_address, &transport->external_address, &transport->external_address_refresher, NULL) < 0) { + ast_log(LOG_ERROR, "Could not create dnsmgr for external signaling address on '%s'\n", ast_sorcery_object_get_id(obj)); + return -1; + } + } + + if (transport->type == AST_SIP_TRANSPORT_UDP) { + if (transport->host.addr.sa_family == pj_AF_INET()) { + res = pjsip_udp_transport_start(ast_sip_get_pjsip_endpoint(), &transport->host.ipv4, NULL, transport->async_operations, &transport->state->transport); + } else if (transport->host.addr.sa_family == pj_AF_INET6()) { + res = pjsip_udp_transport_start6(ast_sip_get_pjsip_endpoint(), &transport->host.ipv6, NULL, transport->async_operations, &transport->state->transport); + } + } else if (transport->type == AST_SIP_TRANSPORT_TCP) { + pjsip_tcp_transport_cfg cfg; + + pjsip_tcp_transport_cfg_default(&cfg, transport->host.addr.sa_family); + cfg.bind_addr = transport->host; + cfg.async_cnt = transport->async_operations; + + res = pjsip_tcp_transport_start3(ast_sip_get_pjsip_endpoint(), &cfg, &transport->state->factory); + } else if (transport->type == AST_SIP_TRANSPORT_TLS) { + transport->tls.ca_list_file = pj_str((char*)transport->ca_list_file); + transport->tls.cert_file = pj_str((char*)transport->cert_file); + transport->tls.privkey_file = pj_str((char*)transport->privkey_file); + transport->tls.password = pj_str((char*)transport->password); + + res = pjsip_tls_transport_start2(ast_sip_get_pjsip_endpoint(), &transport->tls, &transport->host, NULL, transport->async_operations, &transport->state->factory); + } + + if (res != PJ_SUCCESS) { + char msg[PJ_ERR_MSG_SIZE]; + + pjsip_strerror(res, msg, sizeof(msg)); + ast_log(LOG_ERROR, "Transport '%s' could not be started: %s\n", ast_sorcery_object_get_id(obj), msg); + return -1; + } + return 0; +} + +/*! \brief Custom handler for turning a string protocol into an enum */ +static int transport_protocol_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_transport *transport = obj; + + if (!strcasecmp(var->value, "udp")) { + transport->type = AST_SIP_TRANSPORT_UDP; + } else if (!strcasecmp(var->value, "tcp")) { + transport->type = AST_SIP_TRANSPORT_TCP; + } else if (!strcasecmp(var->value, "tls")) { + transport->type = AST_SIP_TRANSPORT_TLS; + } else { + /* TODO: Implement websockets */ + return -1; + } + + return 0; +} + +/*! \brief Custom handler for turning a string bind into a pj_sockaddr */ +static int transport_bind_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_transport *transport = obj; + pj_str_t buf; + + return (pj_sockaddr_parse(pj_AF_UNSPEC(), 0, pj_cstr(&buf, var->value), &transport->host) != PJ_SUCCESS) ? -1 : 0; +} + +/*! \brief Custom handler for TLS boolean settings */ +static int transport_tls_bool_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_transport *transport = obj; + + if (!strcasecmp(var->name, "verify_server")) { + transport->tls.verify_server = ast_true(var->value) ? PJ_TRUE : PJ_FALSE; + } else if (!strcasecmp(var->name, "verify_client")) { + transport->tls.verify_client = ast_true(var->value) ? PJ_TRUE : PJ_FALSE; + } else if (!strcasecmp(var->name, "require_client_cert")) { + transport->tls.require_client_cert = ast_true(var->value) ? PJ_TRUE : PJ_FALSE; + } else { + return -1; + } + + return 0; +} + +/*! \brief Custom handler for TLS method setting */ +static int transport_tls_method_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_transport *transport = obj; + + if (!strcasecmp(var->value, "default")) { + transport->tls.method = PJSIP_SSL_DEFAULT_METHOD; + } else if (!strcasecmp(var->value, "unspecified")) { + transport->tls.method = PJSIP_SSL_UNSPECIFIED_METHOD; + } else if (!strcasecmp(var->value, "tlsv1")) { + transport->tls.method = PJSIP_TLSV1_METHOD; + } else if (!strcasecmp(var->value, "sslv2")) { + transport->tls.method = PJSIP_SSLV2_METHOD; + } else if (!strcasecmp(var->value, "sslv3")) { + transport->tls.method = PJSIP_SSLV3_METHOD; + } else if (!strcasecmp(var->value, "sslv23")) { + transport->tls.method = PJSIP_SSLV23_METHOD; + } else { + return -1; + } + + return 0; +} + +/*! \brief Custom handler for TLS cipher setting */ +static int transport_tls_cipher_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_transport *transport = obj; + pj_ssl_cipher cipher; + + if (transport->tls.ciphers_num == (SIP_TLS_MAX_CIPHERS - 1)) { + return -1; + } + + /* TODO: Check this over/tweak - it's taken from pjsua for now */ + if (!strnicmp(var->value, "0x", 2)) { + pj_str_t cipher_st = pj_str((char*)var->value + 2); + cipher = pj_strtoul2(&cipher_st, NULL, 16); + } else { + cipher = atoi(var->value); + } + + if (pj_ssl_cipher_is_supported(cipher)) { + transport->ciphers[transport->tls.ciphers_num++] = cipher; + return 0; + } else { + ast_log(LOG_ERROR, "Cipher '%s' is unsupported\n", var->value); + return -1; + } +} + +/*! \brief Custom handler for localnet setting */ +static int transport_localnet_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_transport *transport = obj; + int error = 0; + + if (!(transport->localnet = ast_append_ha("d", var->value, transport->localnet, &error))) { + return -1; + } + + return error; +} + +/*! \brief Initialize sorcery with transport support */ +int ast_sip_initialize_sorcery_transport(struct ast_sorcery *sorcery) +{ + ast_sorcery_apply_default(sorcery, "transport", "config", "res_sip.conf,criteria=type=transport"); + + if (ast_sorcery_object_register(sorcery, "transport", transport_alloc, NULL, transport_apply)) { + return -1; + } + + ast_sorcery_object_field_register(sorcery, "transport", "type", "", OPT_NOOP_T, 0, 0); + ast_sorcery_object_field_register_custom(sorcery, "transport", "protocol", "udp", transport_protocol_handler, NULL, 0, 0); + ast_sorcery_object_field_register_custom(sorcery, "transport", "bind", "", transport_bind_handler, NULL, 0, 0); + ast_sorcery_object_field_register(sorcery, "transport", "async_operations", "1", OPT_UINT_T, 0, FLDSET(struct ast_sip_transport, async_operations)); + ast_sorcery_object_field_register(sorcery, "transport", "ca_list_file", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_transport, ca_list_file)); + ast_sorcery_object_field_register(sorcery, "transport", "cert_file", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_transport, cert_file)); + ast_sorcery_object_field_register(sorcery, "transport", "privkey_file", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_transport, privkey_file)); + ast_sorcery_object_field_register(sorcery, "transport", "password", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_transport, password)); + ast_sorcery_object_field_register(sorcery, "transport", "external_signaling_address", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_transport, external_signaling_address)); + ast_sorcery_object_field_register(sorcery, "transport", "external_signaling_port", "0", OPT_UINT_T, PARSE_IN_RANGE, FLDSET(struct ast_sip_transport, external_signaling_port), 0, 65535); + ast_sorcery_object_field_register(sorcery, "transport", "external_media_address", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_transport, external_media_address)); + ast_sorcery_object_field_register(sorcery, "transport", "domain", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_transport, domain)); + ast_sorcery_object_field_register_custom(sorcery, "transport", "verify_server", "", transport_tls_bool_handler, NULL, 0, 0); + ast_sorcery_object_field_register_custom(sorcery, "transport", "verify_client", "", transport_tls_bool_handler, NULL, 0, 0); + ast_sorcery_object_field_register_custom(sorcery, "transport", "require_client_cert", "", transport_tls_bool_handler, NULL, 0, 0); + ast_sorcery_object_field_register_custom(sorcery, "transport", "method", "", transport_tls_method_handler, NULL, 0, 0); + ast_sorcery_object_field_register_custom(sorcery, "transport", "cipher", "", transport_tls_cipher_handler, NULL, 0, 0); + ast_sorcery_object_field_register_custom(sorcery, "transport", "localnet", "", transport_localnet_handler, NULL, 0, 0); + + return 0; +} diff --git a/res/res_sip/include/res_sip_private.h b/res/res_sip/include/res_sip_private.h new file mode 100644 index 000000000..318510aae --- /dev/null +++ b/res/res_sip/include/res_sip_private.h @@ -0,0 +1,57 @@ +/* + * res_sip.h + * + * Created on: Jan 25, 2013 + * Author: mjordan + */ + +#ifndef RES_SIP_PRIVATE_H_ +#define RES_SIP_PRIVATE_H_ + +struct ao2_container; + +/*! + * \brief Initialize the configuration for res_sip + */ +int ast_res_sip_initialize_configuration(void); + +/*! + * \brief Annihilate the configuration objects + */ +void ast_res_sip_destroy_configuration(void); + +/*! + * \brief Reload the configuration + */ +int ast_res_sip_reload_configuration(void); + +/*! + * \brief Initialize OPTIONS request handling. + * + * XXX This currently includes qualifying peers. It shouldn't. + * That should go into a registrar. When that occurs, we won't + * need the reload stuff. + * + * \param reload Reload options handling + * + * \retval 0 on success + * \retval other on failure + */ +int ast_res_sip_init_options_handling(int reload); + +/*! + * \brief Initialize outbound authentication support + * + * \retval 0 Success + * \retval non-zero Failure + */ +int ast_sip_initialize_outbound_authentication(void); + +/*! + * \brief Get the current defined endpoints + * + * \retval The current endpoints loaded by res_sip + */ +struct ao2_container *ast_res_sip_get_endpoints(void); + +#endif /* RES_SIP_PRIVATE_H_ */ diff --git a/res/res_sip/location.c b/res/res_sip/location.c new file mode 100644 index 000000000..91521c813 --- /dev/null +++ b/res/res_sip/location.c @@ -0,0 +1,262 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Joshua Colp <jcolp@digium.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 "pjsip.h" +#include "pjlib.h" + +#include "asterisk/res_sip.h" +#include "asterisk/logger.h" +#include "asterisk/astobj2.h" +#include "asterisk/sorcery.h" + +/*! \brief Destructor for AOR */ +static void aor_destroy(void *obj) +{ + struct ast_sip_aor *aor = obj; + + ao2_cleanup(aor->permanent_contacts); + ast_string_field_free_memory(aor); +} + +/*! \brief Allocator for AOR */ +static void *aor_alloc(const char *name) +{ + struct ast_sip_aor *aor = ao2_alloc_options(sizeof(struct ast_sip_aor), aor_destroy, AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!aor) { + return NULL; + } + ast_string_field_init(aor, 128); + return aor; +} + +/*! \brief Destructor for contact */ +static void contact_destroy(void *obj) +{ + struct ast_sip_contact *contact = obj; + + ast_string_field_free_memory(contact); +} + +/*! \brief Allocator for contact */ +static void *contact_alloc(const char *name) +{ + struct ast_sip_contact *contact = ao2_alloc_options(sizeof(*contact), contact_destroy, AO2_ALLOC_OPT_LOCK_NOLOCK); + + if (!contact) { + return NULL; + } + + if (ast_string_field_init(contact, 256)) { + ao2_cleanup(contact); + return NULL; + } + + return contact; +} + +struct ast_sip_aor *ast_sip_location_retrieve_aor(const char *aor_name) +{ + return ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "aor", aor_name); +} + +/*! \brief Internal callback function which deletes and unlinks any expired contacts */ +static int contact_expire(void *obj, void *arg, int flags) +{ + struct ast_sip_contact *contact = obj; + + /* If the contact has not yet expired it is valid */ + if (ast_tvdiff_ms(contact->expiration_time, ast_tvnow()) > 0) { + return 0; + } + + ast_sip_location_delete_contact(contact); + + return CMP_MATCH; +} + +/*! \brief Internal callback function which links static contacts into another container */ +static int contact_link_static(void *obj, void *arg, int flags) +{ + struct ao2_container *dest = arg; + + ao2_link_flags(dest, obj, OBJ_NOLOCK); + return 0; +} + +/*! \brief Simple callback function which returns immediately, used to grab the first contact of an AOR */ +static int contact_find_first(void *obj, void *arg, int flags) +{ + return CMP_MATCH | CMP_STOP; +} + +struct ast_sip_contact *ast_sip_location_retrieve_first_aor_contact(const struct ast_sip_aor *aor) +{ + RAII_VAR(struct ao2_container *, contacts, NULL, ao2_cleanup); + struct ast_sip_contact *contact; + + contacts = ast_sip_location_retrieve_aor_contacts(aor); + if (!contacts || (ao2_container_count(contacts) == 0)) { + return NULL; + } + + contact = ao2_callback(contacts, OBJ_NOLOCK, contact_find_first, NULL); + return contact; +} + +struct ao2_container *ast_sip_location_retrieve_aor_contacts(const struct ast_sip_aor *aor) +{ + /* Give enough space for ^ at the beginning and ;@ at the end, since that is our object naming scheme */ + char regex[strlen(ast_sorcery_object_get_id(aor)) + 4]; + struct ao2_container *contacts; + + snprintf(regex, sizeof(regex), "^%s;@", ast_sorcery_object_get_id(aor)); + + if (!(contacts = ast_sorcery_retrieve_by_regex(ast_sip_get_sorcery(), "contact", regex))) { + return NULL; + } + + /* Prune any expired contacts and delete them, we do this first because static contacts can never expire */ + ao2_callback(contacts, OBJ_NOLOCK | OBJ_NODATA | OBJ_MULTIPLE | OBJ_UNLINK, contact_expire, NULL); + + /* Add any permanent contacts from the AOR */ + if (aor->permanent_contacts) { + ao2_callback(aor->permanent_contacts, OBJ_NOLOCK | OBJ_NODATA, contact_link_static, contacts); + } + + return contacts; +} + +struct ast_sip_contact *ast_sip_location_retrieve_contact_from_aor_list(const char *aor_list) +{ + char *aor_name; + char *rest; + struct ast_sip_contact *contact = NULL; + + /* If the location is still empty we have nowhere to go */ + if (ast_strlen_zero(aor_list) || !(rest = ast_strdupa(aor_list))) { + ast_log(LOG_WARNING, "Unable to determine contacts from empty aor list\n"); + return NULL; + } + + while ((aor_name = strsep(&rest, ","))) { + RAII_VAR(struct ast_sip_aor *, aor, ast_sip_location_retrieve_aor(aor_name), ao2_cleanup); + RAII_VAR(struct ao2_container *, contacts, NULL, ao2_cleanup); + + if (!aor) { + continue; + } + contact = ast_sip_location_retrieve_first_aor_contact(aor); + /* If a valid contact is available use its URI for dialing */ + if (contact) { + break; + } + } + + return contact; +} + +struct ast_sip_contact *ast_sip_location_retrieve_contact(const char *contact_name) +{ + return ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "contact", contact_name); +} + +int ast_sip_location_add_contact(struct ast_sip_aor *aor, const char *uri, struct timeval expiration_time) +{ + char name[AST_UUID_STR_LEN]; + RAII_VAR(struct ast_sip_contact *, contact, NULL, ao2_cleanup); + + snprintf(name, sizeof(name), "%s;@%s", ast_sorcery_object_get_id(aor), uri); + + if (!(contact = ast_sorcery_alloc(ast_sip_get_sorcery(), "contact", name))) { + return -1; + } + + ast_string_field_set(contact, uri, uri); + contact->expiration_time = expiration_time; + + return ast_sorcery_create(ast_sip_get_sorcery(), contact); +} + +int ast_sip_location_update_contact(struct ast_sip_contact *contact) +{ + return ast_sorcery_update(ast_sip_get_sorcery(), contact); +} + +int ast_sip_location_delete_contact(struct ast_sip_contact *contact) +{ + return ast_sorcery_delete(ast_sip_get_sorcery(), contact); +} + +/*! \brief Custom handler for translating from a string timeval to actual structure */ +static int expiration_str2struct(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_contact *contact = obj; + return ast_get_timeval(var->value, &contact->expiration_time, ast_tv(0, 0), NULL); +} + +/*! \brief Custom handler for translating from an actual structure timeval to string */ +static int expiration_struct2str(const void *obj, const intptr_t *args, char **buf) +{ + const struct ast_sip_contact *contact = obj; + return (ast_asprintf(buf, "%lu", contact->expiration_time.tv_sec) < 0) ? -1 : 0; +} + +/*! \brief Custom handler for permanent URIs */ +static int permanent_uri_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_aor *aor = obj; + RAII_VAR(struct ast_sip_contact *, contact, NULL, ao2_cleanup); + + if ((!aor->permanent_contacts && !(aor->permanent_contacts = ao2_container_alloc_options(AO2_ALLOC_OPT_LOCK_NOLOCK, 1, NULL, NULL))) || + !(contact = ast_sorcery_alloc(ast_sip_get_sorcery(), "contact", NULL))) { + return -1; + } + + ast_string_field_set(contact, uri, var->value); + ao2_link_flags(aor->permanent_contacts, contact, OBJ_NOLOCK); + + return 0; +} + +/*! \brief Initialize sorcery with location support */ +int ast_sip_initialize_sorcery_location(struct ast_sorcery *sorcery) +{ + ast_sorcery_apply_default(sorcery, "contact", "astdb", "registrar"); + ast_sorcery_apply_default(sorcery, "aor", "config", "res_sip.conf,criteria=type=aor"); + + if (ast_sorcery_object_register(sorcery, "contact", contact_alloc, NULL, NULL) || + ast_sorcery_object_register(sorcery, "aor", aor_alloc, NULL, NULL)) { + return -1; + } + + ast_sorcery_object_field_register(sorcery, "contact", "type", "", OPT_NOOP_T, 0, 0); + ast_sorcery_object_field_register(sorcery, "contact", "uri", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_contact, uri)); + ast_sorcery_object_field_register_custom(sorcery, "contact", "expiration_time", "", expiration_str2struct, expiration_struct2str, 0, 0); + + ast_sorcery_object_field_register(sorcery, "aor", "type", "", OPT_NOOP_T, 0, 0); + ast_sorcery_object_field_register(sorcery, "aor", "minimum_expiration", "60", OPT_UINT_T, 0, FLDSET(struct ast_sip_aor, minimum_expiration)); + ast_sorcery_object_field_register(sorcery, "aor", "maximum_expiration", "7200", OPT_UINT_T, 0, FLDSET(struct ast_sip_aor, maximum_expiration)); + ast_sorcery_object_field_register(sorcery, "aor", "default_expiration", "3600", OPT_UINT_T, 0, FLDSET(struct ast_sip_aor, default_expiration)); + ast_sorcery_object_field_register(sorcery, "aor", "max_contacts", "0", OPT_UINT_T, 0, FLDSET(struct ast_sip_aor, max_contacts)); + ast_sorcery_object_field_register(sorcery, "aor", "remove_existing", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_aor, remove_existing)); + ast_sorcery_object_field_register_custom(sorcery, "aor", "contact", "", permanent_uri_handler, NULL, 0, 0); + ast_sorcery_object_field_register(sorcery, "aor", "mailboxes", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_aor, mailboxes)); + + return 0; +} diff --git a/res/res_sip/sip_configuration.c b/res/res_sip/sip_configuration.c new file mode 100644 index 000000000..489ba6aec --- /dev/null +++ b/res/res_sip/sip_configuration.c @@ -0,0 +1,463 @@ +/* + * sip_cli_commands.c + * + * Created on: Jan 25, 2013 + * Author: mjordan + */ + +#include "asterisk.h" + +#include <pjsip.h> +#include <pjsip_ua.h> + +#include "asterisk/res_sip.h" +#include "include/res_sip_private.h" +#include "asterisk/cli.h" +#include "asterisk/astobj2.h" +#include "asterisk/utils.h" +#include "asterisk/sorcery.h" +#include "asterisk/callerid.h" + +static struct ast_sorcery *sip_sorcery; + +static char *handle_cli_show_endpoints(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + RAII_VAR(struct ao2_container *, endpoints, NULL, ao2_cleanup); + struct ao2_iterator it_endpoints; + struct ast_sip_endpoint *endpoint; + + switch (cmd) { + case CLI_INIT: + e->command = "sip show endpoints"; + e->usage = + "Usage: sip show endpoints\n" + " Show the registered SIP endpoints\n"; + return NULL; + case CLI_GENERATE: + return NULL; + } + + endpoints = ast_res_sip_get_endpoints(); + if (!endpoints) { + return CLI_FAILURE; + } + + if (!ao2_container_count(endpoints)) { + ast_cli(a->fd, "No endpoints found\n"); + return CLI_SUCCESS; + } + + ast_cli(a->fd, "Endpoints:\n"); + it_endpoints = ao2_iterator_init(endpoints, 0); + while ((endpoint = ao2_iterator_next(&it_endpoints))) { + ast_cli(a->fd, "%s\n", ast_sorcery_object_get_id(endpoint)); + ao2_ref(endpoint, -1); + } + ao2_iterator_destroy(&it_endpoints); + return CLI_SUCCESS; +} + +static struct ast_cli_entry cli_commands[] = { + AST_CLI_DEFINE(handle_cli_show_endpoints, "Show SIP Endpoints"), +}; + +static int dtmf_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_endpoint *endpoint = obj; + + if (!strcasecmp(var->value, "rfc4733")) { + endpoint->dtmf = AST_SIP_DTMF_RFC_4733; + } else if (!strcasecmp(var->value, "inband")) { + endpoint->dtmf = AST_SIP_DTMF_INBAND; + } else if (!strcasecmp(var->value, "info")) { + endpoint->dtmf = AST_SIP_DTMF_INFO; + } else if (!strcasecmp(var->value, "none")) { + endpoint->dtmf = AST_SIP_DTMF_NONE; + } else { + return -1; + } + + return 0; +} + +static int prack_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_endpoint *endpoint = obj; + + if (ast_true(var->value)) { + endpoint->extensions |= PJSIP_INV_SUPPORT_100REL; + } else if (ast_false(var->value)) { + endpoint->extensions &= PJSIP_INV_SUPPORT_100REL; + } else if (!strcasecmp(var->value, "required")) { + endpoint->extensions |= PJSIP_INV_REQUIRE_100REL; + } else { + return -1; + } + + return 0; +} + +static int timers_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_endpoint *endpoint = obj; + + if (ast_true(var->value)) { + endpoint->extensions |= PJSIP_INV_SUPPORT_TIMER; + } else if (ast_false(var->value)) { + endpoint->extensions &= PJSIP_INV_SUPPORT_TIMER; + } else if (!strcasecmp(var->value, "required")) { + endpoint->extensions |= PJSIP_INV_REQUIRE_TIMER; + } else if (!strcasecmp(var->value, "always")) { + endpoint->extensions |= PJSIP_INV_ALWAYS_USE_TIMER; + } else { + return -1; + } + + return 0; +} + +static void destroy_auths(const char **auths, size_t num_auths) +{ + int i; + for (i = 0; i < num_auths; ++i) { + ast_free((char *) auths[i]); + } + ast_free(auths); +} + +#define AUTH_INCREMENT 4 + +static const char **auth_alloc(const char *value, size_t *num_auths) +{ + char *auths = ast_strdupa(value); + char *val; + int num_alloced = 0; + const char **alloced_auths = NULL; + + while ((val = strsep(&auths, ","))) { + if (*num_auths >= num_alloced) { + size_t size; + num_alloced += AUTH_INCREMENT; + size = num_alloced * sizeof(char *); + alloced_auths = ast_realloc(alloced_auths, size); + if (!alloced_auths) { + goto failure; + } + } + alloced_auths[*num_auths] = ast_strdup(val); + if (!alloced_auths[*num_auths]) { + goto failure; + } + ++(*num_auths); + } + return alloced_auths; + +failure: + destroy_auths(alloced_auths, *num_auths); + return NULL; +} + +static int inbound_auth_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_endpoint *endpoint = obj; + + endpoint->sip_inbound_auths = auth_alloc(var->value, &endpoint->num_inbound_auths); + if (!endpoint->sip_inbound_auths) { + return -1; + } + return 0; +} +static int outbound_auth_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_endpoint *endpoint = obj; + + endpoint->sip_outbound_auths = auth_alloc(var->value, &endpoint->num_outbound_auths); + if (!endpoint->sip_outbound_auths) { + return -1; + } + return 0; +} + +static int ident_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_endpoint *endpoint = obj; + char *idents = ast_strdupa(var->value); + char *val; + + while ((val = strsep(&idents, ","))) { + if (!strcasecmp(val, "username")) { + endpoint->ident_method |= AST_SIP_ENDPOINT_IDENTIFY_BY_USERNAME; + } else if (!strcasecmp(val, "location")) { + endpoint->ident_method |= AST_SIP_ENDPOINT_IDENTIFY_BY_LOCATION; + } else { + ast_log(LOG_ERROR, "Unrecognized identification method %s specified for endpoint %s\n", + val, ast_sorcery_object_get_id(endpoint)); + return -1; + } + } + return 0; +} + +static int direct_media_method_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_endpoint *endpoint = obj; + + if (!strcasecmp(var->value, "invite") || !strcasecmp(var->value, "reinvite")) { + endpoint->direct_media_method = AST_SIP_SESSION_REFRESH_METHOD_INVITE; + } else if (!strcasecmp(var->value, "update")) { + endpoint->direct_media_method = AST_SIP_SESSION_REFRESH_METHOD_UPDATE; + } else { + ast_log(LOG_NOTICE, "Unrecognized option value %s for %s on endpoint %s\n", + var->value, var->name, ast_sorcery_object_get_id(endpoint)); + return -1; + } + return 0; +} + +static int direct_media_glare_mitigation_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_endpoint *endpoint = obj; + + if (!strcasecmp(var->value, "none")) { + endpoint->direct_media_glare_mitigation = AST_SIP_DIRECT_MEDIA_GLARE_MITIGATION_NONE; + } else if (!strcasecmp(var->value, "outgoing")) { + endpoint->direct_media_glare_mitigation = AST_SIP_DIRECT_MEDIA_GLARE_MITIGATION_OUTGOING; + } else if (!strcasecmp(var->value, "incoming")) { + endpoint->direct_media_glare_mitigation = AST_SIP_DIRECT_MEDIA_GLARE_MITIGATION_INCOMING; + } else { + ast_log(LOG_NOTICE, "Unrecognized option value %s for %s on endpoint %s\n", + var->value, var->name, ast_sorcery_object_get_id(endpoint)); + return -1; + } + + return 0; +} + +static int caller_id_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_endpoint *endpoint = obj; + char cid_name[80] = { '\0' }; + char cid_num[80] = { '\0' }; + + ast_callerid_split(var->value, cid_name, sizeof(cid_name), cid_num, sizeof(cid_num)); + if (!ast_strlen_zero(cid_name)) { + endpoint->id.name.str = ast_strdup(cid_name); + if (!endpoint->id.name.str) { + return -1; + } + endpoint->id.name.valid = 1; + } + if (!ast_strlen_zero(cid_num)) { + endpoint->id.number.str = ast_strdup(cid_num); + if (!endpoint->id.number.str) { + return -1; + } + endpoint->id.number.valid = 1; + } + return 0; +} + +static int caller_id_privacy_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_endpoint *endpoint = obj; + int callingpres = ast_parse_caller_presentation(var->value); + if (callingpres == -1 && sscanf(var->value, "%d", &callingpres) != 1) { + return -1; + } + endpoint->id.number.presentation = callingpres; + endpoint->id.name.presentation = callingpres; + return 0; +} + +static int caller_id_tag_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ast_sip_endpoint *endpoint = obj; + endpoint->id.tag = ast_strdup(var->value); + return endpoint->id.tag ? 0 : -1; +} + +static void *sip_nat_hook_alloc(const char *name) +{ + return ao2_alloc(sizeof(struct ast_sip_nat_hook), NULL); +} + +int ast_res_sip_initialize_configuration(void) +{ + if (ast_cli_register_multiple(cli_commands, ARRAY_LEN(cli_commands))) { + return -1; + } + + if (!(sip_sorcery = ast_sorcery_open())) { + ast_log(LOG_ERROR, "Failed to open SIP sorcery failed to open\n"); + return -1; + } + + ast_sorcery_apply_config(sip_sorcery, "res_sip"); + + if (ast_sip_initialize_sorcery_auth(sip_sorcery)) { + ast_log(LOG_ERROR, "Failed to register SIP authentication support\n"); + ast_sorcery_unref(sip_sorcery); + sip_sorcery = NULL; + return -1; + } + + ast_sorcery_apply_default(sip_sorcery, "endpoint", "config", "res_sip.conf,criteria=type=endpoint"); + + ast_sorcery_apply_default(sip_sorcery, "nat_hook", "memory", NULL); + + if (ast_sorcery_object_register(sip_sorcery, "endpoint", ast_sip_endpoint_alloc, NULL, NULL)) { + ast_log(LOG_ERROR, "Failed to register SIP endpoint object with sorcery\n"); + ast_sorcery_unref(sip_sorcery); + sip_sorcery = NULL; + return -1; + } + + ast_sorcery_object_register(sip_sorcery, "nat_hook", sip_nat_hook_alloc, NULL, NULL); + + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "type", "", OPT_NOOP_T, 0, 0); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "context", "default", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, context)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "disallow", "", OPT_CODEC_T, 0, FLDSET(struct ast_sip_endpoint, prefs, codecs)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "allow", "", OPT_CODEC_T, 1, FLDSET(struct ast_sip_endpoint, prefs, codecs)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "qualify_frequency", 0, OPT_UINT_T, PARSE_IN_RANGE, FLDSET(struct ast_sip_endpoint, qualify_frequency), 0, 86400); + ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "dtmfmode", "rfc4733", dtmf_handler, NULL, 0, 0); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "rtp_ipv6", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, rtp_ipv6)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "rtp_symmetric", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, rtp_symmetric)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "ice_support", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, ice_support)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "use_ptime", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, use_ptime)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "force_rport", "yes", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, force_rport)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "rewrite_contact", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, rewrite_contact)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "transport", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, transport)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "outbound_proxy", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, outbound_proxy)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "mohsuggest", "default", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, mohsuggest)); + ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "100rel", "yes", prack_handler, NULL, 0, 0); + ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "timers", "yes", timers_handler, NULL, 0, 0); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "timers_min_se", "90", OPT_UINT_T, 0, FLDSET(struct ast_sip_endpoint, min_se)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "timers_sess_expires", "1800", OPT_UINT_T, 0, FLDSET(struct ast_sip_endpoint, sess_expires)); + ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "auth", "", inbound_auth_handler, NULL, 0, 0); + ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "outbound_auth", "", outbound_auth_handler, NULL, 0, 0); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "aors", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, aors)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "external_media_address", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, external_media_address)); + ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "identify_by", "username,location", ident_handler, NULL, 0, 0); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "direct_media", "yes", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, direct_media)); + ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "direct_media_method", "invite", direct_media_method_handler, NULL, 0, 0); + ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "direct_media_glare_mitigation", "none", direct_media_glare_mitigation_handler, NULL, 0, 0); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "disable_direct_media_on_nat", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, disable_direct_media_on_nat)); + ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "callerid", "", caller_id_handler, NULL, 0, 0); + ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "callerid_privacy", "", caller_id_privacy_handler, NULL, 0, 0); + ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "callerid_tag", "", caller_id_tag_handler, NULL, 0, 0); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "trust_id_inbound", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, trust_id_inbound)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "trust_id_outbound", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, trust_id_outbound)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "send_pai", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, send_pai)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "send_rpid", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, send_rpid)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "mailboxes", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_endpoint, mailboxes)); + ast_sorcery_object_field_register(sip_sorcery, "endpoint", "aggregate_mwi", "yes", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, aggregate_mwi)); + + if (ast_sip_initialize_sorcery_transport(sip_sorcery)) { + ast_log(LOG_ERROR, "Failed to register SIP transport support with sorcery\n"); + ast_sorcery_unref(sip_sorcery); + sip_sorcery = NULL; + return -1; + } + + if (ast_sip_initialize_sorcery_location(sip_sorcery)) { + ast_log(LOG_ERROR, "Failed to register SIP location support with sorcery\n"); + ast_sorcery_unref(sip_sorcery); + sip_sorcery = NULL; + return -1; + } + + if (ast_sip_initialize_sorcery_domain_alias(sip_sorcery)) { + ast_log(LOG_ERROR, "Failed to register SIP domain aliases support with sorcery\n"); + ast_sorcery_unref(sip_sorcery); + sip_sorcery = NULL; + return -1; + } + + ast_sorcery_load(sip_sorcery); + + return 0; +} + +void ast_res_sip_destroy_configuration(void) +{ + ast_cli_unregister_multiple(cli_commands, ARRAY_LEN(cli_commands)); + ast_sorcery_unref(sip_sorcery); +} + +int ast_res_sip_reload_configuration(void) +{ + if (sip_sorcery) { + ast_sorcery_reload(sip_sorcery); + } + return 0; +} + +static void endpoint_destructor(void* obj) +{ + struct ast_sip_endpoint *endpoint = obj; + + ast_string_field_free_memory(endpoint); + + if (endpoint->codecs) { + ast_format_cap_destroy(endpoint->codecs); + } + destroy_auths(endpoint->sip_inbound_auths, endpoint->num_inbound_auths); + destroy_auths(endpoint->sip_outbound_auths, endpoint->num_outbound_auths); + ast_party_id_free(&endpoint->id); +} + +void *ast_sip_endpoint_alloc(const char *name) +{ + struct ast_sip_endpoint *endpoint = ao2_alloc(sizeof(*endpoint), endpoint_destructor); + if (!endpoint) { + return NULL; + } + if (ast_string_field_init(endpoint, 64)) { + ao2_cleanup(endpoint); + return NULL; + } + if (!(endpoint->codecs = ast_format_cap_alloc_nolock())) { + ao2_cleanup(endpoint); + return NULL; + } + ast_party_id_init(&endpoint->id); + return endpoint; +} + +struct ao2_container *ast_res_sip_get_endpoints(void) +{ + struct ao2_container *endpoints; + + endpoints = ast_sorcery_retrieve_by_fields(sip_sorcery, "endpoint", AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL); + + return endpoints; +} + +int ast_sip_retrieve_auths(const char *auth_names[], size_t num_auths, struct ast_sip_auth **out) +{ + int i; + + for (i = 0; i < num_auths; ++i) { + out[i] = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), SIP_SORCERY_AUTH_TYPE, auth_names[i]); + if (!out[i]) { + ast_log(LOG_NOTICE, "Couldn't find auth '%s'. Cannot authenticate\n", auth_names[i]); + return -1; + } + } + + return 0; +} + +void ast_sip_cleanup_auths(struct ast_sip_auth *auths[], size_t num_auths) +{ + int i; + for (i = 0; i < num_auths; ++i) { + ao2_cleanup(auths[i]); + } +} + +struct ast_sorcery *ast_sip_get_sorcery(void) +{ + return sip_sorcery; +} + diff --git a/res/res_sip/sip_distributor.c b/res/res_sip/sip_distributor.c new file mode 100644 index 000000000..766261089 --- /dev/null +++ b/res/res_sip/sip_distributor.c @@ -0,0 +1,239 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Mark Michelson <mmichelson@digium.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 <pjsip.h> + +#include "asterisk/res_sip.h" + +static int distribute(void *data); +static pj_bool_t distributor(pjsip_rx_data *rdata); + +static pjsip_module distributor_mod = { + .name = {"Request Distributor", 19}, + .priority = PJSIP_MOD_PRIORITY_TSX_LAYER - 6, + .on_rx_request = distributor, + .on_rx_response = distributor, +}; + +/*! Dialog-specific information the distributor uses */ +struct distributor_dialog_data { + /* Serializer to distribute tasks to for this dialog */ + struct ast_taskprocessor *serializer; + /* Endpoint associated with this dialog */ + struct ast_sip_endpoint *endpoint; +}; + +/*! + * \internal + * + * \note Call this with the dialog locked + */ +static struct distributor_dialog_data *distributor_dialog_data_alloc(pjsip_dialog *dlg) +{ + struct distributor_dialog_data *dist; + + dist = PJ_POOL_ZALLOC_T(dlg->pool, struct distributor_dialog_data); + pjsip_dlg_set_mod_data(dlg, distributor_mod.id, dist); + + return dist; +} + +void ast_sip_dialog_set_serializer(pjsip_dialog *dlg, struct ast_taskprocessor *serializer) +{ + struct distributor_dialog_data *dist; + SCOPED_LOCK(lock, dlg, pjsip_dlg_inc_lock, pjsip_dlg_dec_lock); + + dist = pjsip_dlg_get_mod_data(dlg, distributor_mod.id); + if (!dist) { + dist = distributor_dialog_data_alloc(dlg); + } + dist->serializer = serializer; +} + +void ast_sip_dialog_set_endpoint(pjsip_dialog *dlg, struct ast_sip_endpoint *endpoint) +{ + struct distributor_dialog_data *dist; + SCOPED_LOCK(lock, dlg, pjsip_dlg_inc_lock, pjsip_dlg_dec_lock); + + dist = pjsip_dlg_get_mod_data(dlg, distributor_mod.id); + if (!dist) { + dist = distributor_dialog_data_alloc(dlg); + } + dist->endpoint = endpoint; +} + +struct ast_sip_endpoint *ast_sip_dialog_get_endpoint(pjsip_dialog *dlg) +{ + struct distributor_dialog_data *dist; + SCOPED_LOCK(lock, dlg, pjsip_dlg_inc_lock, pjsip_dlg_dec_lock); + + dist = pjsip_dlg_get_mod_data(dlg, distributor_mod.id); + if (!dist || !dist->endpoint) { + return NULL; + } + ao2_ref(dist->endpoint, +1); + return dist->endpoint; +} + +static pj_bool_t distributor(pjsip_rx_data *rdata) +{ + pjsip_dialog *dlg = pjsip_ua_find_dialog(&rdata->msg_info.cid->id, &rdata->msg_info.to->tag, &rdata->msg_info.from->tag, PJ_TRUE); + struct distributor_dialog_data *dist = NULL; + struct ast_taskprocessor *serializer = NULL; + pjsip_rx_data *clone; + + pjsip_rx_data_clone(rdata, 0, &clone); + if (dlg) { + dist = pjsip_dlg_get_mod_data(dlg, distributor_mod.id); + if (dist) { + serializer = dist->serializer; + clone->endpt_info.mod_data[distributor_mod.id] = dist->endpoint; + } + } + + ast_sip_push_task(serializer, distribute, clone); + + if (dlg) { + pjsip_dlg_dec_lock(dlg); + } + + return PJ_TRUE; +} + +static pj_bool_t endpoint_lookup(pjsip_rx_data *rdata); + +static pjsip_module endpoint_mod = { + .name = {"Endpoint Identifier", 19}, + .priority = PJSIP_MOD_PRIORITY_TSX_LAYER - 3, + .on_rx_request = endpoint_lookup, +}; + +static pj_bool_t endpoint_lookup(pjsip_rx_data *rdata) +{ + struct ast_sip_endpoint *endpoint; + int is_ack = rdata->msg_info.msg->line.req.method.id == PJSIP_ACK_METHOD; + + endpoint = rdata->endpt_info.mod_data[distributor_mod.id]; + if (endpoint) { + /* Bumping the refcount makes refcounting consistent whether an endpoint + * is looked up or not */ + ao2_ref(endpoint, +1); + } else { + endpoint = ast_sip_identify_endpoint(rdata); + } + + if (!endpoint && !is_ack) { + /* XXX When we do an alwaysauthreject-like option, we'll need to take that into account + * for this response. Either that, or have a pseudo-endpoint to pass along so that authentication + * will fail + */ + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 403, NULL, NULL, NULL); + return PJ_TRUE; + } + rdata->endpt_info.mod_data[endpoint_mod.id] = endpoint; + return PJ_FALSE; +} + +static pj_bool_t authenticate(pjsip_rx_data *rdata) +{ + RAII_VAR(struct ast_sip_endpoint *, endpoint, ast_pjsip_rdata_get_endpoint(rdata), ao2_cleanup); + int is_ack = rdata->msg_info.msg->line.req.method.id == PJSIP_ACK_METHOD; + + ast_assert(endpoint != NULL); + + if (!is_ack && ast_sip_requires_authentication(endpoint, rdata)) { + pjsip_tx_data *tdata; + pjsip_endpt_create_response(ast_sip_get_pjsip_endpoint(), rdata, 401, NULL, &tdata); + switch (ast_sip_check_authentication(endpoint, rdata, tdata)) { + case AST_SIP_AUTHENTICATION_CHALLENGE: + /* Send the 401 we created for them */ + pjsip_endpt_send_response2(ast_sip_get_pjsip_endpoint(), rdata, tdata, NULL, NULL); + return PJ_TRUE; + case AST_SIP_AUTHENTICATION_SUCCESS: + pjsip_tx_data_dec_ref(tdata); + return PJ_FALSE; + case AST_SIP_AUTHENTICATION_FAILED: + pjsip_tx_data_dec_ref(tdata); + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 403, NULL, NULL, NULL); + return PJ_TRUE; + case AST_SIP_AUTHENTICATION_ERROR: + pjsip_tx_data_dec_ref(tdata); + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 500, NULL, NULL, NULL); + return PJ_TRUE; + } + } + + return PJ_FALSE; +} + +static pjsip_module auth_mod = { + .name = {"Request Authenticator", 21}, + .priority = PJSIP_MOD_PRIORITY_APPLICATION - 1, + .on_rx_request = authenticate, +}; + +static int distribute(void *data) +{ + static pjsip_process_rdata_param param = { + .start_mod = &distributor_mod, + .idx_after_start = 1, + }; + pj_bool_t handled; + pjsip_rx_data *rdata = data; + int is_request = rdata->msg_info.msg->type == PJSIP_REQUEST_MSG; + int is_ack = is_request ? rdata->msg_info.msg->line.req.method.id == PJSIP_ACK_METHOD : 0; + struct ast_sip_endpoint *endpoint; + + pjsip_endpt_process_rx_data(ast_sip_get_pjsip_endpoint(), rdata, ¶m, &handled); + if (!handled && is_request && !is_ack) { + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 501, NULL, NULL, NULL); + } + + /* The endpoint_mod stores an endpoint reference in the mod_data of rdata. This + * is the only appropriate spot to actually decrement the reference. + */ + endpoint = rdata->endpt_info.mod_data[endpoint_mod.id]; + ao2_cleanup(endpoint); + pjsip_rx_data_free_cloned(rdata); + return 0; +} + +struct ast_sip_endpoint *ast_pjsip_rdata_get_endpoint(pjsip_rx_data *rdata) +{ + struct ast_sip_endpoint *endpoint = rdata->endpt_info.mod_data[endpoint_mod.id]; + if (endpoint) { + ao2_ref(endpoint, +1); + } + return endpoint; +} + +int ast_sip_initialize_distributor(void) +{ + if (ast_sip_register_service(&distributor_mod)) { + return -1; + } + if (ast_sip_register_service(&endpoint_mod)) { + return -1; + } + if (ast_sip_register_service(&auth_mod)) { + return -1; + } + return 0; +} diff --git a/res/res_sip/sip_options.c b/res/res_sip/sip_options.c new file mode 100644 index 000000000..5e3f8edca --- /dev/null +++ b/res/res_sip/sip_options.c @@ -0,0 +1,378 @@ +/* + * sip_options.c + * + * Created on: Jan 25, 2013 + * Author: mjordan + */ + +#include "asterisk.h" + +#include <pjsip.h> +#include <pjsip_ua.h> +#include <pjlib.h> + +#include "asterisk/res_sip.h" +#include "asterisk/channel.h" +#include "asterisk/pbx.h" +#include "asterisk/astobj2.h" +#include "asterisk/cli.h" +#include "include/res_sip_private.h" + +#define DEFAULT_LANGUAGE "en" +#define DEFAULT_ENCODING "text/plain" +#define QUALIFIED_BUCKETS 211 + +/*! \brief Scheduling context for qualifies */ +static struct ast_sched_context *sched; /* XXX move this to registrar */ + +struct ao2_container *scheduled_qualifies; + +struct qualify_info { + AST_DECLARE_STRING_FIELDS( + AST_STRING_FIELD(endpoint_id); + ); + char *scheduler_data; + int scheduler_id; +}; + +static pj_bool_t options_module_start(void); +static pj_bool_t options_module_stop(void); +static pj_bool_t options_module_on_rx_request(pjsip_rx_data *rdata); +static pj_bool_t options_module_on_rx_response(pjsip_rx_data *rdata); + +static pjsip_module options_module = { + .name = {"Options Module", 14}, + .id = -1, + .priority = PJSIP_MOD_PRIORITY_APPLICATION, + .start = options_module_start, + .stop = options_module_stop, + .on_rx_request = options_module_on_rx_request, + .on_rx_response = options_module_on_rx_response, +}; + +static pj_bool_t options_module_start(void) +{ + if (!(sched = ast_sched_context_create()) || + ast_sched_start_thread(sched)) { + return -1; + } + + return PJ_SUCCESS; +} + +static pj_bool_t options_module_stop(void) +{ + ao2_t_ref(scheduled_qualifies, -1, "Remove scheduled qualifies on module stop"); + + if (sched) { + ast_sched_context_destroy(sched); + } + + return PJ_SUCCESS; +} + +static pj_status_t send_options_response(pjsip_rx_data *rdata, pjsip_dialog *pj_dlg, int code) +{ + pjsip_endpoint *endpt = ast_sip_get_pjsip_endpoint(); + pjsip_transaction *pj_trans = pjsip_rdata_get_tsx(rdata); + pjsip_tx_data *tdata; + const pjsip_hdr *hdr; + pjsip_response_addr res_addr; + pj_status_t status; + + /* Make the response object */ + status = pjsip_endpt_create_response(endpt, rdata, code, NULL, &tdata); + if (status != PJ_SUCCESS) { + return status; + } + + /* Add appropriate headers */ + if ((hdr = pjsip_endpt_get_capability(endpt, PJSIP_H_ACCEPT, NULL))) { + pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr*)pjsip_hdr_clone(tdata->pool, hdr)); + } + if ((hdr = pjsip_endpt_get_capability(endpt, PJSIP_H_ALLOW, NULL))) { + pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr*)pjsip_hdr_clone(tdata->pool, hdr)); + } + if ((hdr = pjsip_endpt_get_capability(endpt, PJSIP_H_SUPPORTED, NULL))) { + pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr*)pjsip_hdr_clone(tdata->pool, hdr)); + } + + /* + * XXX TODO: pjsip doesn't care a lot about either of these headers - + * while it provides specific methods to create them, they are defined + * to be the standard string header creation. We never did add them + * in chan_sip, although RFC 3261 says they SHOULD. Hard coded here. + */ + ast_sip_add_header(tdata, "Accept-Encoding", DEFAULT_ENCODING); + ast_sip_add_header(tdata, "Accept-Language", DEFAULT_LANGUAGE); + + if (pj_dlg && pj_trans) { + status = pjsip_dlg_send_response(pj_dlg, pj_trans, tdata); + } else { + /* Get where to send request. */ + status = pjsip_get_response_addr(tdata->pool, rdata, &res_addr); + if (status != PJ_SUCCESS) { + pjsip_tx_data_dec_ref(tdata); + return status; + } + status = pjsip_endpt_send_response(endpt, &res_addr, tdata, NULL, NULL); + } + + return status; +} + +static pj_bool_t options_module_on_rx_request(pjsip_rx_data *rdata) +{ + RAII_VAR(struct ast_sip_endpoint *, endpoint, NULL, ao2_cleanup); + pjsip_dialog *dlg = pjsip_rdata_get_dlg(rdata); + pjsip_uri *ruri; + pjsip_sip_uri *sip_ruri; + char exten[AST_MAX_EXTENSION]; + + if (pjsip_method_cmp(&rdata->msg_info.msg->line.req.method, &pjsip_options_method)) { + return PJ_FALSE; + } + endpoint = ast_pjsip_rdata_get_endpoint(rdata); + ast_assert(endpoint != NULL); + + ruri = rdata->msg_info.msg->line.req.uri; + if (!PJSIP_URI_SCHEME_IS_SIP(ruri) && !PJSIP_URI_SCHEME_IS_SIPS(ruri)) { + send_options_response(rdata, dlg, 416); + return -1; + } + + sip_ruri = pjsip_uri_get_uri(ruri); + ast_copy_pj_str(exten, &sip_ruri->user, sizeof(exten)); + + if (ast_shutting_down()) { + send_options_response(rdata, dlg, 503); + } else if (!ast_exists_extension(NULL, endpoint->context, exten, 1, NULL)) { + send_options_response(rdata, dlg, 404); + } else { + send_options_response(rdata, dlg, 200); + } + return PJ_TRUE; +} + +static pj_bool_t options_module_on_rx_response(pjsip_rx_data *rdata) +{ + + return PJ_SUCCESS; +} + +static int qualify_info_hash_fn(const void *obj, int flags) +{ + const struct qualify_info *info = obj; + const char *endpoint_id = flags & OBJ_KEY ? obj : info->endpoint_id; + + return ast_str_hash(endpoint_id); +} + +static int qualify_info_cmp_fn(void *obj, void *arg, int flags) +{ + struct qualify_info *left = obj; + struct qualify_info *right = arg; + const char *right_endpoint_id = flags & OBJ_KEY ? arg : right->endpoint_id; + + return strcmp(left->endpoint_id, right_endpoint_id) ? 0 : CMP_MATCH | CMP_STOP; +} + + +static void qualify_info_destructor(void *obj) +{ + struct qualify_info *info = obj; + if (!info) { + return; + } + ast_string_field_free_memory(info); + /* Cancel the qualify */ + if (!AST_SCHED_DEL(sched, info->scheduler_id)) { + /* If we successfully deleted the qualify, we got it before it + * fired. We can safely delete the data that was passed to it. + * Otherwise, we're getting deleted while this is firing - don't + * touch that memory! + */ + ast_free(info->scheduler_data); + } +} + +static struct qualify_info *create_qualify_info(struct ast_sip_endpoint *endpoint) +{ + struct qualify_info *info; + + info = ao2_alloc(sizeof(*info), qualify_info_destructor); + if (!info) { + return NULL; + } + + if (ast_string_field_init(info, 64)) { + ao2_ref(info, -1); + return NULL; + } + ast_string_field_set(info, endpoint_id, ast_sorcery_object_get_id(endpoint)); + + return info; +} + +static int send_qualify_request(void *data) +{ + struct ast_sip_endpoint *endpoint = data; + pjsip_tx_data *tdata; + /* YAY! Send an OPTIONS request. */ + + ast_sip_create_request("OPTIONS", NULL, endpoint, NULL, &tdata); + ast_sip_send_request(tdata, NULL, endpoint); + + ao2_cleanup(endpoint); + return 0; +} + +static int qualify_endpoint_scheduler_cb(const void *data) +{ + RAII_VAR(struct ast_sip_endpoint *, endpoint, NULL, ao2_cleanup); + struct ast_sorcery *sorcery; + char *endpoint_id = (char *)data; + + sorcery = ast_sip_get_sorcery(); + if (!sorcery) { + ast_free(endpoint_id); + return 0; + } + + endpoint = ast_sorcery_retrieve_by_id(sorcery, "endpoint", endpoint_id); + if (!endpoint) { + /* Whoops, endpoint went away */ + ast_free(endpoint_id); + return 0; + } + + ast_sip_push_task(NULL, send_qualify_request, endpoint); + + return 1; +} + +static void schedule_qualifies(void) +{ + RAII_VAR(struct ao2_container *, endpoints, NULL, ao2_cleanup); + struct ao2_iterator it_endpoints; + struct ast_sip_endpoint *endpoint; + struct qualify_info *info; + char *endpoint_id; + + endpoints = ast_res_sip_get_endpoints(); + if (!endpoints) { + return; + } + + it_endpoints = ao2_iterator_init(endpoints, 0); + while ((endpoint = ao2_iterator_next(&it_endpoints))) { + if (endpoint->qualify_frequency) { + /* XXX TODO: This really should only qualify registered peers, + * which means we need a registrar. We should check the + * registrar to see if this endpoint has registered and, if + * not, pass on it. + * + * Actually, all of this should just get moved into the registrar. + * Otherwise, the registar will have to kick this off when a + * new endpoint registers, so it just makes sense to have it + * all live there. + */ + info = create_qualify_info(endpoint); + if (!info) { + ao2_ref(endpoint, -1); + break; + } + endpoint_id = ast_strdup(info->endpoint_id); + if (!endpoint_id) { + ao2_t_ref(info, -1, "Dispose of info on off nominal"); + ao2_ref(endpoint, -1); + break; + } + info->scheduler_data = endpoint_id; + info->scheduler_id = ast_sched_add_variable(sched, endpoint->qualify_frequency * 1000, qualify_endpoint_scheduler_cb, endpoint_id, 1); + ao2_t_link(scheduled_qualifies, info, "Link scheduled qualify information into container"); + ao2_t_ref(info, -1, "Dispose of creation ref"); + } + ao2_t_ref(endpoint, -1, "Dispose of iterator ref"); + } + ao2_iterator_destroy(&it_endpoints); +} + +static char *send_options(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + RAII_VAR(struct ast_sip_endpoint *, endpoint, NULL, ao2_cleanup); + const char *endpoint_name; + pjsip_tx_data *tdata; + + switch (cmd) { + case CLI_INIT: + e->command = "sip send options"; + e->usage = + "Usage: sip send options <endpoint>\n" + " Send a SIP OPTIONS request to the specified endpoint.\n"; + return NULL; + case CLI_GENERATE: + return NULL; + } + + if (a->argc != 4) { + return CLI_SHOWUSAGE; + } + + endpoint_name = a->argv[3]; + + endpoint = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", endpoint_name); + if (!endpoint) { + ast_log(LOG_ERROR, "Unable to retrieve endpoint %s\n", endpoint_name); + return CLI_FAILURE; + } + + if (ast_sip_create_request("OPTIONS", NULL, endpoint, NULL, &tdata)) { + ast_log(LOG_ERROR, "Unable to create OPTIONS request to endpoint %s\n", endpoint_name); + return CLI_FAILURE; + } + + if (ast_sip_send_request(tdata, NULL, endpoint)) { + ast_log(LOG_ERROR, "Unable to send OPTIONS request to endpoint %s\n", endpoint_name); + return CLI_FAILURE; + } + + return CLI_SUCCESS; +} + +static struct ast_cli_entry cli_options[] = { + AST_CLI_DEFINE(send_options, "Send an OPTIONS requst to an arbitrary SIP URI"), +}; + +int ast_res_sip_init_options_handling(int reload) +{ + const pj_str_t STR_OPTIONS = { "OPTIONS", 7 }; + + if (scheduled_qualifies) { + ao2_t_ref(scheduled_qualifies, -1, "Remove old scheduled qualifies"); + } + scheduled_qualifies = ao2_t_container_alloc(QUALIFIED_BUCKETS, qualify_info_hash_fn, qualify_info_cmp_fn, "Create container for scheduled qualifies"); + if (!scheduled_qualifies) { + return -1; + } + + if (reload) { + return 0; + } + + if (pjsip_endpt_register_module(ast_sip_get_pjsip_endpoint(), &options_module) != PJ_SUCCESS) { + options_module_stop(); + return -1; + } + + if (pjsip_endpt_add_capability(ast_sip_get_pjsip_endpoint(), NULL, PJSIP_H_ALLOW, NULL, 1, &STR_OPTIONS) != PJ_SUCCESS) { + pjsip_endpt_unregister_module(ast_sip_get_pjsip_endpoint(), &options_module); + return -1; + } + + ast_cli_register_multiple(cli_options, ARRAY_LEN(cli_options)); + + schedule_qualifies(); + + return 0; +} diff --git a/res/res_sip/sip_outbound_auth.c b/res/res_sip/sip_outbound_auth.c new file mode 100644 index 000000000..94352a521 --- /dev/null +++ b/res/res_sip/sip_outbound_auth.c @@ -0,0 +1,94 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Mark Michelson <mmichelson@digium.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" +#undef bzero +#define bzero bzero +#include "pjsip.h" + +#include "asterisk/res_sip.h" +#include "asterisk/module.h" +#include "include/res_sip_private.h" + +static pj_bool_t outbound_auth(pjsip_rx_data *rdata); + +static pjsip_module outbound_auth_mod = { + .name = {"Outbound Authentication", 19}, + .priority = PJSIP_MOD_PRIORITY_DIALOG_USAGE, + .on_rx_response = outbound_auth, +}; + +struct outbound_auth_cb_data { + ast_sip_dialog_outbound_auth_cb cb; + void *user_data; +}; + +static pj_bool_t outbound_auth(pjsip_rx_data *rdata) +{ + RAII_VAR(struct ast_sip_endpoint *, endpoint, NULL, ao2_cleanup); + pjsip_transaction *tsx; + pjsip_dialog *dlg; + struct outbound_auth_cb_data *cb_data; + pjsip_tx_data *tdata; + + if (rdata->msg_info.msg->line.status.code != 401 && + rdata->msg_info.msg->line.status.code != 407) { + /* Doesn't pertain to us. Move on */ + return PJ_FALSE; + } + + tsx = pjsip_rdata_get_tsx(rdata); + dlg = pjsip_rdata_get_dlg(rdata); + ast_assert(dlg != NULL && tsx != NULL); + endpoint = ast_sip_dialog_get_endpoint(dlg); + + if (!endpoint) { + return PJ_FALSE; + } + + if (ast_sip_create_request_with_auth(endpoint->sip_outbound_auths, endpoint->num_outbound_auths, rdata, tsx, &tdata)) { + return PJ_FALSE; + } + + cb_data = dlg->mod_data[outbound_auth_mod.id]; + if (cb_data) { + cb_data->cb(dlg, tdata, cb_data->user_data); + return PJ_TRUE; + } + + pjsip_dlg_send_request(dlg, tdata, -1, NULL); + return PJ_TRUE; +} + +int ast_sip_dialog_setup_outbound_authentication(pjsip_dialog *dlg, const struct ast_sip_endpoint *endpoint, + ast_sip_dialog_outbound_auth_cb cb, void *user_data) +{ + struct outbound_auth_cb_data *cb_data = PJ_POOL_ZALLOC_T(dlg->pool, struct outbound_auth_cb_data); + cb_data->cb = cb; + cb_data->user_data = user_data; + + dlg->sess_count++; + pjsip_dlg_add_usage(dlg, &outbound_auth_mod, cb_data); + dlg->sess_count--; + + return 0; +} + +int ast_sip_initialize_outbound_authentication(void) { + return ast_sip_register_service(&outbound_auth_mod); +} diff --git a/res/res_sip_acl.c b/res/res_sip_acl.c new file mode 100644 index 000000000..405c3c1bc --- /dev/null +++ b/res/res_sip_acl.c @@ -0,0 +1,222 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Mark Michelson <mmichelson@digium.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>pjproject</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include <pjsip.h> + +#include "asterisk/res_sip.h" +#include "asterisk/module.h" +#include "asterisk/logger.h" +#include "asterisk/sorcery.h" +#include "asterisk/acl.h" + +struct sip_acl { + SORCERY_OBJECT(details); + struct ast_acl_list *acl; + struct ast_acl_list *contact_acl; +}; + +static int apply_acl(pjsip_rx_data *rdata, struct ast_acl_list *acl) +{ + struct ast_sockaddr addr; + + if (ast_acl_list_is_empty(acl)) { + return 0; + } + + memset(&addr, 0, sizeof(addr)); + ast_sockaddr_parse(&addr, rdata->pkt_info.src_name, PARSE_PORT_FORBID); + ast_sockaddr_set_port(&addr, rdata->pkt_info.src_port); + + if (ast_apply_acl(acl, &addr, "SIP ACL: ") != AST_SENSE_ALLOW) { + ast_log(LOG_WARNING, "Incoming SIP message from %s did not pass ACL test\n", ast_sockaddr_stringify(&addr)); + return 1; + } + return 0; +} + +static int extract_contact_addr(pjsip_contact_hdr *contact, struct ast_sockaddr **addrs) +{ + pjsip_sip_uri *sip_uri; + char host[256]; + + if (!contact) { + return 0; + } + if (!PJSIP_URI_SCHEME_IS_SIP(contact->uri) && !PJSIP_URI_SCHEME_IS_SIPS(contact->uri)) { + return 0; + } + sip_uri = pjsip_uri_get_uri(contact->uri); + ast_copy_pj_str(host, &sip_uri->host, sizeof(host)); + return ast_sockaddr_resolve(addrs, host, PARSE_PORT_FORBID, AST_AF_UNSPEC); +} + +static int apply_contact_acl(pjsip_rx_data *rdata, struct ast_acl_list *contact_acl) +{ + int num_contact_addrs; + int forbidden = 0; + struct ast_sockaddr *contact_addrs; + int i; + pjsip_contact_hdr *contact = (pjsip_contact_hdr *)&rdata->msg_info.msg->hdr; + + if (ast_acl_list_is_empty(contact_acl)) { + return 0; + } + + while ((contact = pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_CONTACT, contact->next))) { + num_contact_addrs = extract_contact_addr(contact, &contact_addrs); + if (num_contact_addrs <= 0) { + continue; + } + for (i = 0; i < num_contact_addrs; ++i) { + if (ast_apply_acl(contact_acl, &contact_addrs[i], "SIP Contact ACL: ") != AST_SENSE_ALLOW) { + ast_log(LOG_WARNING, "Incoming SIP message from %s did not pass ACL test\n", ast_sockaddr_stringify(&contact_addrs[i])); + forbidden = 1; + break; + } + } + ast_free(contact_addrs); + if (forbidden) { + /* No use checking other contacts if we already have failed ACL check */ + break; + } + } + + return forbidden; +} + +static int check_acls(void *obj, void *arg, int flags) +{ + struct sip_acl *acl = obj; + pjsip_rx_data *rdata = arg; + + if (apply_acl(rdata, acl->acl) || apply_contact_acl(rdata, acl->contact_acl)) { + return CMP_MATCH | CMP_STOP; + } + return 0; +} + +static pj_bool_t acl_on_rx_msg(pjsip_rx_data *rdata) +{ + int forbidden = 0; + struct ao2_container *acls = ast_sorcery_retrieve_by_fields(ast_sip_get_sorcery(), "acl", AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL); + struct sip_acl *matched_acl; + if (!acls) { + ast_log(LOG_ERROR, "Unable to retrieve ACL sorcery data\n"); + return PJ_FALSE; + } + + matched_acl = ao2_callback(acls, 0, check_acls, rdata); + if (matched_acl) { + forbidden = 1; + ao2_ref(matched_acl, -1); + } + ao2_ref(acls, -1); + + if (forbidden) { + if (rdata->msg_info.msg->line.req.method.id != PJSIP_ACK_METHOD) { + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 403, NULL, NULL, NULL); + } + return PJ_TRUE; + } + + return PJ_FALSE; +} + +static pjsip_module acl_module = { + .name = { "ACL Module", 14 }, + /* This should run after a logger but before anything else */ + .priority = 1, + .on_rx_request = acl_on_rx_msg, +}; + +static int acl_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct sip_acl *acl = obj; + int error; + int ignore; + if (!strncmp(var->name, "contact", 7)) { + ast_append_acl(var->name + 7, var->value, &acl->contact_acl, &error, &ignore); + } else { + ast_append_acl(var->name, var->value, &acl->acl, &error, &ignore); + } + return error; +} + +static void sip_acl_destructor(void *obj) +{ + struct sip_acl *acl = obj; + acl->acl = ast_free_acl_list(acl->acl); + acl->contact_acl = ast_free_acl_list(acl->contact_acl); +} + +static void *sip_acl_alloc(const char *name) +{ + struct sip_acl *acl = ao2_alloc(sizeof(*acl), sip_acl_destructor); + if (!acl) { + return NULL; + } + return acl; +} + +static int load_acls(void) +{ + ast_sorcery_apply_default(ast_sip_get_sorcery(), "acl", "config", "res_sip.conf,criteria=type=acl"); + if (ast_sorcery_object_register(ast_sip_get_sorcery(), "acl", sip_acl_alloc, NULL, NULL)) { + ast_log(LOG_ERROR, "Failed to register SIP ACL object with sorcery\n"); + return -1; + } + ast_sorcery_object_field_register(ast_sip_get_sorcery(), "acl", "type", "", OPT_NOOP_T, 0, 0); + ast_sorcery_object_field_register_custom(ast_sip_get_sorcery(), "acl", "permit", "", acl_handler, NULL, 0, 0); + ast_sorcery_object_field_register_custom(ast_sip_get_sorcery(), "acl", "deny", "", acl_handler, NULL, 0, 0); + ast_sorcery_object_field_register_custom(ast_sip_get_sorcery(), "acl", "acl", "", acl_handler, NULL, 0, 0); + ast_sorcery_object_field_register_custom(ast_sip_get_sorcery(), "acl", "contactpermit", "", acl_handler, NULL, 0, 0); + ast_sorcery_object_field_register_custom(ast_sip_get_sorcery(), "acl", "contactdeny", "", acl_handler, NULL, 0, 0); + ast_sorcery_object_field_register_custom(ast_sip_get_sorcery(), "acl", "contactacl", "", acl_handler, NULL, 0, 0); + + /* XXX Is there a more selective way to do this? (i.e. Just reload a specific object type?) */ + ast_sorcery_reload(ast_sip_get_sorcery()); + return 0; +} + +static int load_module(void) +{ + if (load_acls()) { + return AST_MODULE_LOAD_DECLINE; + } + ast_sip_register_service(&acl_module); + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + ast_sip_unregister_service(&acl_module); + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "SIP ACL Resource", + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_APP_DEPEND, + ); diff --git a/res/res_sip_authenticator_digest.c b/res/res_sip_authenticator_digest.c new file mode 100644 index 000000000..499342309 --- /dev/null +++ b/res/res_sip_authenticator_digest.c @@ -0,0 +1,455 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Mark Michelson <mmichelson@digium.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 <pjsip.h> + +#include "asterisk/res_sip.h" +#include "asterisk/logger.h" +#include "asterisk/module.h" +#include "asterisk/strings.h" + +/*** MODULEINFO + <depend>pjproject</depend> + <depend>res_sip</depend> + <support_level>core</support_level> + ***/ + +AO2_GLOBAL_OBJ_STATIC(entity_id); + +/*! + * \brief Determine if authentication is required + * + * Authentication is required if the endpoint has at least one auth + * section specified + */ +static int digest_requires_authentication(struct ast_sip_endpoint *endpoint, pjsip_rx_data *rdata) +{ + return endpoint->num_inbound_auths > 0; +} + +static void auth_store_cleanup(void *data) +{ + struct ast_sip_auth **auth = data; + + ao2_cleanup(*auth); + ast_free(data); +} + +/*! + * \brief Thread-local storage for \ref ast_sip_auth + * + * The PJSIP authentication API is a bit annoying. When you set + * up an authentication server, you specify a lookup callback to + * call into when verifying incoming credentials. The problem + * with this callback is that it only gives you the realm and + * authentication username. In 2.0.5, there is a new version of + * the callback you can use that gives the pjsip_rx_data in + * addition. + * + * Unfortunately, the data we actually \b need is the + * \ref ast_sip_auth we are currently observing. So we have two + * choices: + * 1) Use the current PJSIP API and use thread-local storage + * to temporarily store our SIP authentication information. Then + * in the callback, we can retrieve the authentication info and + * use as needed. Given our threading model, this is safe. + * 2) Use the 2.0.5 API and temporarily store the authentication + * information in the rdata's endpoint_info. Then in the callback, + * we can retrieve the authentication info from the rdata. + * + * I've chosen option 1 since it does not require backporting + * any APIs from future versions of PJSIP, plus I feel the + * thread-local option is a bit cleaner. + */ +AST_THREADSTORAGE_CUSTOM(auth_store, NULL, auth_store_cleanup); + +/*! + * \brief Store authentication information in thread-local storage + */ +static int store_auth(struct ast_sip_auth *auth) +{ + struct ast_sip_auth **pointing; + pointing = ast_threadstorage_get(&auth_store, sizeof(pointing)); + if (!pointing || *pointing) { + return -1; + } + + ao2_ref(auth, +1); + *pointing = auth; + return 0; +} + +/*! + * \brief Remove authentication information from thread-local storage + */ +static int remove_auth(void) +{ + struct ast_sip_auth **pointing; + pointing = ast_threadstorage_get(&auth_store, sizeof(pointing)); + if (!pointing) { + return -1; + } + + ao2_cleanup(*pointing); + *pointing = NULL; + return 0; +} + +/*! + * \brief Retrieve authentication information from thread-local storage + */ +static struct ast_sip_auth *get_auth(void) +{ + struct ast_sip_auth **auth; + auth = ast_threadstorage_get(&auth_store, sizeof(auth)); + if (auth && *auth) { + ao2_ref(*auth, +1); + return *auth; + } + return NULL; +} + +/*! + * \brief Lookup callback for authentication verification + * + * This function is called when we call pjsip_auth_srv_verify(). It + * expects us to verify that the realm and account name from the + * Authorization header is correct. We are then supposed to supply + * a password or MD5 sum of credentials. + * + * \param pool A memory pool we can use for allocations + * \param realm The realm from the Authorization header + * \param acc_name the user from the Authorization header + * \param[out] info The credentials we need to fill in + * \retval PJ_SUCCESS Successful authentication + * \retval other Unsuccessful + */ +static pj_status_t digest_lookup(pj_pool_t *pool, const pj_str_t *realm, + const pj_str_t *acc_name, pjsip_cred_info *info) +{ + RAII_VAR(struct ast_sip_auth *, auth, get_auth(), ao2_cleanup); + if (!auth) { + return PJSIP_SC_FORBIDDEN; + } + + if (pj_strcmp2(realm, auth->realm)) { + return PJSIP_SC_FORBIDDEN; + } + if (pj_strcmp2(acc_name, auth->auth_user)) { + return PJSIP_SC_FORBIDDEN; + } + + pj_strdup2(pool, &info->realm, auth->realm); + pj_strdup2(pool, &info->username, auth->auth_user); + + switch (auth->type) { + case AST_SIP_AUTH_TYPE_USER_PASS: + pj_strdup2(pool, &info->data, auth->auth_pass); + info->data_type = PJSIP_CRED_DATA_PLAIN_PASSWD; + break; + case AST_SIP_AUTH_TYPE_MD5: + pj_strdup2(pool, &info->data, auth->md5_creds); + info->data_type = PJSIP_CRED_DATA_DIGEST; + break; + default: + return PJSIP_SC_FORBIDDEN; + } + return PJ_SUCCESS; +} + +/*! + * \brief Calculate a nonce + * + * We use this in order to create authentication challenges. We also use this in order + * to verify that an incoming request with credentials could be in response to one + * of our challenges. + * + * The nonce is calculated from a timestamp, the source IP address, the source port, a + * unique ID for us, and the realm. This helps to ensure that the incoming request + * is from the same source that the nonce was calculated for. Including the realm + * ensures that multiple challenges to the same request have different nonces. + * + * \param A UNIX timestamp expressed as a string + * \param rdata The incoming request + * \param realm The realm for which authentication should occur + */ +static int build_nonce(struct ast_str **nonce, const char *timestamp, const pjsip_rx_data *rdata, const char *realm) +{ + struct ast_str *str = ast_str_alloca(256); + RAII_VAR(char *, eid, ao2_global_obj_ref(entity_id), ao2_cleanup); + char hash[32]; + + ast_str_append(&str, 0, "%s", timestamp); + ast_str_append(&str, 0, ":%s", rdata->pkt_info.src_name); + ast_str_append(&str, 0, ":%d", rdata->pkt_info.src_port); + ast_str_append(&str, 0, ":%s", eid); + ast_str_append(&str, 0, ":%s", realm); + ast_md5_hash(hash, ast_str_buffer(str)); + + ast_str_append(nonce, 0, "%s/%s", timestamp, hash); + return 0; +} + +/*! + * \brief Ensure that a nonce on an incoming request is sane. + * + * The nonce in an incoming Authorization header needs to pass some scrutiny in order + * for us to consider accepting it. What we do is re-build a nonce based on request + * data and a realm and see if it matches the nonce they sent us. + * \param candidate The nonce on an incoming request + * \param rdata The incoming request + * \param auth The auth credentials we are trying to match against. + * \retval 0 Nonce does not pass validity checks + * \retval 1 Nonce passes validity check + */ +static int check_nonce(const char *candidate, const pjsip_rx_data *rdata, const struct ast_sip_auth *auth) +{ + char *copy = ast_strdupa(candidate); + char *timestamp = strsep(©, "/"); + int timestamp_int; + time_t now = time(NULL); + struct ast_str *calculated = ast_str_alloca(64); + + if (!copy) { + /* Clearly a bad nonce! */ + return 0; + } + + if (sscanf(timestamp, "%30d", ×tamp_int) != 1) { + return 0; + } + + if ((int) now - timestamp_int > auth->nonce_lifetime) { + return 0; + } + + build_nonce(&calculated, timestamp, rdata, auth->realm); + ast_debug(3, "Calculated nonce %s. Actual nonce is %s\n", ast_str_buffer(calculated), candidate); + if (strcmp(ast_str_buffer(calculated), candidate)) { + return 0; + } + return 1; +} + +static int find_challenge(const pjsip_rx_data *rdata, const struct ast_sip_auth *auth) +{ + struct pjsip_authorization_hdr *auth_hdr = (pjsip_authorization_hdr *) &rdata->msg_info.msg->hdr; + int challenge_found = 0; + char nonce[64]; + + while ((auth_hdr = (pjsip_authorization_hdr *) pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_AUTHORIZATION, auth_hdr->next))) { + ast_copy_pj_str(nonce, &auth_hdr->credential.digest.nonce, sizeof(nonce)); + if (check_nonce(nonce, rdata, auth) && !pj_strcmp2(&auth_hdr->credential.digest.realm, auth->realm)) { + challenge_found = 1; + break; + } + } + + return challenge_found; +} + +/*! + * \brief Common code for initializing a pjsip_auth_srv + */ +static void setup_auth_srv(pj_pool_t *pool, pjsip_auth_srv *auth_server, const struct ast_sip_auth *auth) +{ + pj_str_t realm; + pj_cstr(&realm, auth->realm); + + pjsip_auth_srv_init(pool, auth_server, &realm, digest_lookup, 0); +} + +/*! + * \brief Result of digest verification + */ +enum digest_verify_result { + /*! Authentication credentials incorrect */ + AUTH_FAIL, + /*! Authentication credentials correct */ + AUTH_SUCCESS, + /*! Authentication credentials correct but nonce mismatch */ + AUTH_STALE, +}; + +/*! + * \brief astobj2 callback for verifying incoming credentials + * + * \param auth The ast_sip_auth to check against + * \param rdata The incoming request + * \param pool A pool to use for the auth server + * \return CMP_MATCH on successful authentication + * \return 0 on failed authentication + */ +static int verify(struct ast_sip_auth *auth, pjsip_rx_data *rdata, pj_pool_t *pool) +{ + pj_status_t authed; + int response_code; + pjsip_auth_srv auth_server; + int stale = 0; + + if (!find_challenge(rdata, auth)) { + /* Couldn't find a challenge with a sane nonce. + * Nonce mismatch may just be due to staleness. + */ + stale = 1; + } + + setup_auth_srv(pool, &auth_server, auth); + + store_auth(auth); + + authed = pjsip_auth_srv_verify(&auth_server, rdata, &response_code); + + remove_auth(); + + if (authed == PJ_SUCCESS) { + if (stale) { + return AUTH_STALE; + } else { + return AUTH_SUCCESS; + } + } + return AUTH_FAIL; +} + +/*! + * \brief astobj2 callback for adding digest challenges to responses + * + * \param auth The ast_aip_auth to build a challenge from + * \param tdata The response to add the challenge to + * \param rdata The request the challenge is in response to + * \param is_stale Indicates whether nonce on incoming request was stale + */ +static void challenge(const struct ast_sip_auth *auth, pjsip_tx_data *tdata, const pjsip_rx_data *rdata, int is_stale) +{ + pj_str_t qop; + pj_str_t pj_nonce; + pjsip_auth_srv auth_server; + struct ast_str *nonce = ast_str_alloca(256); + char time_buf[32]; + time_t timestamp = time(NULL); + snprintf(time_buf, sizeof(time_buf), "%d", (int) timestamp); + + build_nonce(&nonce, time_buf, rdata, auth->realm); + + setup_auth_srv(tdata->pool, &auth_server, auth); + + pj_cstr(&pj_nonce, ast_str_buffer(nonce)); + pj_cstr(&qop, "auth"); + pjsip_auth_srv_challenge(&auth_server, &qop, &pj_nonce, NULL, is_stale ? PJ_TRUE : PJ_FALSE, tdata); +} + +/*! + * \brief Check authentication using Digest scheme + * + * This function will check an incoming message against configured authentication + * options. If \b any of the incoming Authorization headers result in successful + * authentication, then authentication is considered successful. + * + * \see ast_sip_check_authentication + */ +static enum ast_sip_check_auth_result digest_check_auth(struct ast_sip_endpoint *endpoint, + pjsip_rx_data *rdata, pjsip_tx_data *tdata) +{ + struct ast_sip_auth **auths = ast_alloca(endpoint->num_inbound_auths * sizeof(*auths)); + enum digest_verify_result *verify_res = ast_alloca(endpoint->num_inbound_auths * sizeof(*verify_res)); + enum ast_sip_check_auth_result res; + int i; + + if (!auths) { + return AST_SIP_AUTHENTICATION_ERROR; + } + + if (ast_sip_retrieve_auths(endpoint->sip_inbound_auths, endpoint->num_inbound_auths, auths)) { + res = AST_SIP_AUTHENTICATION_ERROR; + goto cleanup; + } + + for (i = 0; i < endpoint->num_inbound_auths; ++i) { + verify_res[i] = verify(auths[i], rdata, tdata->pool); + if (verify_res[i] == AUTH_SUCCESS) { + res = AST_SIP_AUTHENTICATION_SUCCESS; + goto cleanup; + } + } + + for (i = 0; i < endpoint->num_inbound_auths; ++i) { + challenge(auths[i], tdata, rdata, verify_res[i] == AUTH_STALE); + } + + res = AST_SIP_AUTHENTICATION_CHALLENGE; + +cleanup: + ast_sip_cleanup_auths(auths, endpoint->num_inbound_auths); + return res; +} + +static struct ast_sip_authenticator digest_authenticator = { + .requires_authentication = digest_requires_authentication, + .check_authentication = digest_check_auth, +}; + +static int build_entity_id(void) +{ + RAII_VAR(struct ast_uuid *, uu, ast_uuid_generate(), ast_free_ptr); + RAII_VAR(char *, eid, ao2_alloc(AST_UUID_STR_LEN, NULL), ao2_cleanup); + + if (!uu || !eid) { + return -1; + } + + ast_uuid_to_str(uu, eid, AST_UUID_STR_LEN); + ao2_global_obj_replace_unref(entity_id, eid); + return 0; +} + +static int reload_module(void) +{ + if (build_entity_id()) { + return -1; + } + return 0; +} + +static int load_module(void) +{ + if (build_entity_id()) { + return AST_MODULE_LOAD_DECLINE; + } + if (ast_sip_register_authenticator(&digest_authenticator)) { + ao2_global_obj_release(entity_id); + return AST_MODULE_LOAD_DECLINE; + } + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + ast_sip_unregister_authenticator(&digest_authenticator); + ao2_global_obj_release(entity_id); + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "SIP authentication resource", + .load = load_module, + .unload = unload_module, + .reload = reload_module, + .load_pri = AST_MODPRI_CHANNEL_DEPEND, +); diff --git a/res/res_sip_caller_id.c b/res/res_sip_caller_id.c new file mode 100644 index 000000000..22ece0436 --- /dev/null +++ b/res/res_sip_caller_id.c @@ -0,0 +1,715 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Mark Michelson <mmichelson@digium.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>pjproject</depend> + <depend>res_sip</depend> + <depend>res_sip_session</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include <pjsip.h> +#include <pjsip_ua.h> + +#include "asterisk/res_sip.h" +#include "asterisk/res_sip_session.h" +#include "asterisk/channel.h" +#include "asterisk/module.h" +#include "asterisk/callerid.h" + +/*! + * \internal + * \brief Set an ast_party_id name and number based on an identity header. + * \param hdr From, P-Asserted-Identity, or Remote-Party-ID header on incoming message + * \param[out] id The ID to set data on + */ +static void set_id_from_hdr(pjsip_fromto_hdr *hdr, struct ast_party_id *id) +{ + char cid_name[AST_CHANNEL_NAME]; + char cid_num[AST_CHANNEL_NAME]; + pjsip_sip_uri *uri; + pjsip_name_addr *id_name_addr = (pjsip_name_addr *) hdr->uri; + + uri = pjsip_uri_get_uri(id_name_addr); + ast_copy_pj_str(cid_name, &id_name_addr->display, sizeof(cid_name)); + ast_copy_pj_str(cid_num, &uri->user, sizeof(cid_num)); + + ast_free(id->name.str); + id->name.str = ast_strdup(cid_name); + if (!ast_strlen_zero(cid_name)) { + id->name.valid = 1; + } + ast_free(id->number.str); + id->number.str = ast_strdup(cid_num); + if (!ast_strlen_zero(cid_num)) { + id->number.valid = 1; + } +} + +/*! + * \internal + * \brief Get a P-Asserted-Identity or Remote-Party-ID header from an incoming message + * + * This function will parse the header as if it were a From header. This allows for us + * to easily manipulate the URI, as well as add, modify, or remove parameters from the + * header + * + * \param rdata The incoming message + * \param header_name The name of the ID header to find + * \retval NULL No ID header present or unable to parse ID header + * \retval non-NULL The parsed ID header + */ +static pjsip_fromto_hdr *get_id_header(pjsip_rx_data *rdata, const pj_str_t *header_name) +{ + static const pj_str_t from = { "From", 4 }; + pj_str_t header_content; + pjsip_fromto_hdr *parsed_hdr; + pjsip_generic_string_hdr *ident = pjsip_msg_find_hdr_by_name(rdata->msg_info.msg, + header_name, NULL); + int parsed_len; + + if (!ident) { + return NULL; + } + + pj_strdup_with_null(rdata->tp_info.pool, &header_content, &ident->hvalue); + + parsed_hdr = pjsip_parse_hdr(rdata->tp_info.pool, &from, header_content.ptr, + pj_strlen(&header_content), &parsed_len); + + if (!parsed_hdr) { + return NULL; + } + + return parsed_hdr; +} + +/*! + * \internal + * \brief Set an ast_party_id structure based on data in a P-Asserted-Identity header + * + * This makes use of \ref set_id_from_hdr for setting name and number. It uses + * the contents of a Privacy header in order to set presentation information. + * + * \param rdata The incoming message + * \param[out] id The ID to set + * \retval 0 Successfully set the party ID + * \retval non-zero Could not set the party ID + */ +static int set_id_from_pai(pjsip_rx_data *rdata, struct ast_party_id *id) +{ + static const pj_str_t pai_str = { "P-Asserted-Identity", 19 }; + static const pj_str_t privacy_str = { "Privacy", 7 }; + pjsip_fromto_hdr *pai_hdr = get_id_header(rdata, &pai_str); + pjsip_generic_string_hdr *privacy; + + if (!pai_hdr) { + return -1; + } + + set_id_from_hdr(pai_hdr, id); + + if (!id->number.valid) { + return -1; + } + + privacy = pjsip_msg_find_hdr_by_name(rdata->msg_info.msg, &privacy_str, NULL); + if (!privacy) { + return 0; + } + if (!pj_stricmp2(&privacy->hvalue, "id")) { + id->number.presentation = AST_PRES_PROHIB_USER_NUMBER_NOT_SCREENED; + id->name.presentation = AST_PRES_PROHIB_USER_NUMBER_NOT_SCREENED; + } + + return 0; +} + +/*! + * \internal + * \brief Set an ast_party_id structure based on data in a Remote-Party-ID header + * + * This makes use of \ref set_id_from_hdr for setting name and number. It uses + * the privacy and screen parameters in order to set presentation information. + * + * \param rdata The incoming message + * \param[out] id The ID to set + * \retval 0 Succesfully set the party ID + * \retval non-zero Could not set the party ID + */ +static int set_id_from_rpid(pjsip_rx_data *rdata, struct ast_party_id *id) +{ + static const pj_str_t rpid_str = { "Remote-Party-ID", 15 }; + static const pj_str_t privacy_str = { "privacy", 7 }; + static const pj_str_t screen_str = { "screen", 6 }; + pjsip_fromto_hdr *rpid_hdr = get_id_header(rdata, &rpid_str); + pjsip_param *screen; + pjsip_param *privacy; + + if (!rpid_hdr) { + return -1; + } + + set_id_from_hdr(rpid_hdr, id); + + if (!id->number.valid) { + return -1; + } + + privacy = pjsip_param_find(&rpid_hdr->other_param, &privacy_str); + screen = pjsip_param_find(&rpid_hdr->other_param, &screen_str); + if (privacy && !pj_stricmp2(&privacy->value, "full")) { + id->number.presentation |= AST_PRES_RESTRICTED; + id->name.presentation |= AST_PRES_RESTRICTED; + } + if (screen && !pj_stricmp2(&screen->value, "yes")) { + id->number.presentation |= AST_PRES_USER_NUMBER_PASSED_SCREEN; + id->name.presentation |= AST_PRES_USER_NUMBER_PASSED_SCREEN; + } + + return 0; +} + +/*! + * \internal + * \brief Set an ast_party_id structure based on data in a From + * + * This makes use of \ref set_id_from_hdr for setting name and number. It uses + * no information from the message in order to set privacy. It relies on endpoint + * configuration for privacy information. + * + * \param rdata The incoming message + * \param[out] id The ID to set + * \retval 0 Succesfully set the party ID + * \retval non-zero Could not set the party ID + */ +static int set_id_from_from(struct pjsip_rx_data *rdata, struct ast_party_id *id) +{ + pjsip_fromto_hdr *from = pjsip_msg_find_hdr(rdata->msg_info.msg, + PJSIP_H_FROM, rdata->msg_info.msg->hdr.next); + + if (!from) { + /* This had better not happen */ + return -1; + } + + set_id_from_hdr(from, id); + + if (!id->number.valid) { + return -1; + } + + return 0; +} + +/*! + * \internal + * \brief Determine if a connected line update should be queued + * + * This uses information about the session and the ID that would be queued + * in the connected line update in order to determine if we should queue + * a connected line update. + * + * \param session The session whose channel we wish to queue the connected line update on + * \param id The identification information that would be queued on the connected line update + * \retval 0 We should not queue a connected line update + * \retval non-zero We should queue a connected line update + */ +static int should_queue_connected_line_update(const struct ast_sip_session *session, const struct ast_party_id *id) +{ + /* Invalid number means no update */ + if (!id->number.valid) { + return 0; + } + + /* If the session has never communicated an update or if the + * new ID has a different number than the session, then we + * should queue an update + */ + if (ast_strlen_zero(session->id.number.str) || + strcmp(session->id.number.str, id->number.str)) { + return 1; + } + + /* By making it to this point, it means the number is not enough + * to determine if an update should be sent. Now we look at + * the name + */ + + /* If the number couldn't warrant an update and the name is + * invalid, then no update + */ + if (!id->name.valid) { + return 0; + } + + /* If the name has changed or we don't have a name set for the + * session, then we should send an update + */ + if (ast_strlen_zero(session->id.name.str) || + strcmp(session->id.name.str, id->name.str)) { + return 1; + } + + /* Neither the name nor the number have changed. No update */ + return 0; +} + +/*! + * \internal + * \brief Queue a connected line update on a session's channel. + * \param session The session whose channel should have the connected line update queued upon. + * \param id The identification information to place in the connected line update + */ +static void queue_connected_line_update(struct ast_sip_session *session, const struct ast_party_id *id) +{ + struct ast_party_connected_line connected; + struct ast_set_party_connected_line update_connected; + + ast_party_connected_line_init(&connected); + ast_party_id_copy(&connected.id, id); + + memset(&update_connected, 0, sizeof(update_connected)); + update_connected.id.number = 1; + update_connected.id.name = 1; + + ast_set_party_id_all(&update_connected.priv); + connected.source = AST_CONNECTED_LINE_UPDATE_SOURCE_ANSWER; + ast_party_id_copy(&session->id, &connected.id); + ast_channel_queue_connected_line_update(session->channel, &connected, &update_connected); + + ast_party_connected_line_free(&connected); +} + +/*! + * \internal + * \brief Make updates to connected line information based on an incoming request. + * + * This will get identity information from an incoming request. Once the identification is + * retrieved, we will check if the new information warrants a connected line update and queue + * a connected line update if so. + * + * \param session The session on which we received an incoming request + * \param rdata The incoming request + */ +static void update_incoming_connected_line(struct ast_sip_session *session, pjsip_rx_data *rdata) +{ + struct ast_party_id id; + + if (!session->endpoint->trust_id_inbound) { + return; + } + + ast_party_id_init(&id); + if (set_id_from_pai(rdata, &id) && set_id_from_rpid(rdata, &id)) { + return; + } + if (should_queue_connected_line_update(session, &id)) { + queue_connected_line_update(session, &id); + } + + ast_party_id_free(&id); +} + +/*! + * \internal + * \brief Session supplement callback on an incoming INVITE request + * + * If we are receiving an initial INVITE, then we will set the session's identity + * based on the INVITE or configured endpoint values. If we are receiving a reinvite, + * then we will potentially queue a connected line update via the \ref update_incoming_connected_line + * function + * + * \param session The session that has received an INVITE + * \param rdata The incoming INVITE + */ +static int caller_id_incoming_request(struct ast_sip_session *session, pjsip_rx_data *rdata) +{ + if (session->inv_session->state < PJSIP_INV_STATE_CONFIRMED) { + /* Initial inbound INVITE. Set the session ID directly */ + if (session->endpoint->trust_id_inbound && + (!set_id_from_pai(rdata, &session->id) || !set_id_from_rpid(rdata, &session->id))) { + return 0; + } + ast_party_id_copy(&session->id, &session->endpoint->id); + if (!session->endpoint->id.number.valid) { + set_id_from_from(rdata, &session->id); + } + } else { + /* Reinvite. Check for changes to the ID and queue a connected line + * update if necessary + */ + update_incoming_connected_line(session, rdata); + } + return 0; +} + +/*! + * \internal + * \brief Session supplement callback on INVITE response + * + * INVITE responses could result in queuing connected line updates. + * + * \param session The session on which communication is happening + * \param rdata The incoming INVITE response + */ +static void caller_id_incoming_response(struct ast_sip_session *session, pjsip_rx_data *rdata) +{ + if (!session->channel) { + return; + } + + update_incoming_connected_line(session, rdata); +} + +/*! + * \internal + * \brief Set name and number information on an identity header. + * \param pool Memory pool to use for string duplication + * \param id_hdr A From, P-Asserted-Identity, or Remote-Party-ID header to modify + * \param id The identity information to apply to the header + */ +static void modify_id_header(pj_pool_t *pool, pjsip_fromto_hdr *id_hdr, const struct ast_party_id *id) +{ + pjsip_name_addr *id_name_addr; + pjsip_sip_uri *id_uri; + + id_name_addr = (pjsip_name_addr *) id_hdr->uri; + id_uri = pjsip_uri_get_uri(id_name_addr->uri); + + if (id->name.valid) { + pj_strdup2(pool, &id_name_addr->display, id->name.str); + } + + pj_strdup2(pool, &id_uri->user, id->number.str); +} + +/*! + * \internal + * \brief Create an identity header for an outgoing message + * \param hdr_name The name of the header to create + * \param tdata The message to place the header on + * \param id The identification information for the new header + * \return newly-created header + */ +static pjsip_fromto_hdr *create_new_id_hdr(const pj_str_t *hdr_name, pjsip_tx_data *tdata, const struct ast_party_id *id) +{ + pjsip_fromto_hdr *id_hdr; + pjsip_fromto_hdr *base; + pjsip_name_addr *id_name_addr; + pjsip_sip_uri *id_uri; + + base = tdata->msg->type == PJSIP_REQUEST_MSG ? PJSIP_MSG_FROM_HDR(tdata->msg) : + PJSIP_MSG_TO_HDR(tdata->msg); + id_hdr = pjsip_from_hdr_create(tdata->pool); + id_hdr->type = PJSIP_H_OTHER; + pj_strdup(tdata->pool, &id_hdr->name, hdr_name); + id_hdr->sname.slen = 0; + + id_name_addr = pjsip_uri_clone(tdata->pool, base->uri); + id_uri = pjsip_uri_get_uri(id_name_addr->uri); + + if (id->name.valid) { + pj_strdup2(tdata->pool, &id_name_addr->display, id->name.str); + } + + pj_strdup2(tdata->pool, &id_uri->user, id->number.str); + + id_hdr->uri = (pjsip_uri *) id_name_addr; + return id_hdr; +} + +/*! + * \internal + * \brief Add a Privacy header to an outbound message + * + * When sending a P-Asserted-Identity header, if privacy is requested, then we + * will need to indicate such by adding a Privacy header. Similarly, if no + * privacy is requested, and a Privacy header already exists on the message, + * then the old Privacy header should be removed. + * + * \param tdata The outbound message to add the Privacy header to + * \param id The id information used to determine privacy + */ +static void add_privacy_header(pjsip_tx_data *tdata, const struct ast_party_id *id) +{ + static const pj_str_t pj_privacy_name = { "Privacy", 7 }; + static const pj_str_t pj_privacy_value = { "id", 2 }; + pjsip_hdr *old_privacy; + + old_privacy = pjsip_msg_find_hdr_by_name(tdata->msg, &pj_privacy_name, NULL); + + if ((id->name.presentation & AST_PRES_RESTRICTION) == AST_PRES_RESTRICTED || + (id->name.presentation & AST_PRES_RESTRICTION) == AST_PRES_RESTRICTED) { + if (!old_privacy) { + pjsip_generic_string_hdr *privacy_hdr = pjsip_generic_string_hdr_create( + tdata->pool, &pj_privacy_name, &pj_privacy_value); + pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr *)privacy_hdr); + } + } else { + if (old_privacy) { + pj_list_erase(old_privacy); + } + } +} + +/*! + * \internal + * \brief Add a P-Asserted-Identity header to an outbound message + * \param tdata The message to add the header to + * \param id The identification information used to populate the header + */ +static void add_pai_header(pjsip_tx_data *tdata, const struct ast_party_id *id) +{ + static const pj_str_t pj_pai_name = { "P-Asserted-Identity", 19 }; + pjsip_fromto_hdr *pai_hdr; + pjsip_fromto_hdr *old_pai; + + if (!id->number.valid) { + return; + } + + /* Since inv_session reuses responses, we have to make sure there's not already + * a P-Asserted-Identity present. If there is, we just modify the old one. + */ + old_pai = pjsip_msg_find_hdr_by_name(tdata->msg, &pj_pai_name, NULL); + if (old_pai) { + modify_id_header(tdata->pool, old_pai, id); + add_privacy_header(tdata, id); + return; + } + + pai_hdr = create_new_id_hdr(&pj_pai_name, tdata, id); + if (!pai_hdr) { + return; + } + add_privacy_header(tdata, id); + + pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr *)pai_hdr); +} + +/*! + * \internal + * \brief Add privacy and screen parameters to a Remote-Party-ID header. + * + * If privacy is requested, then the privacy and screen parameters need to + * reflect this. Similarly, if no privacy or screening is to be communicated, + * we need to make sure that any previously set values are updated. + * + * \param tdata The message where the Remote-Party-ID header is + * \param hdr The header on which the parameters are being added + * \param id The identification information used to determine privacy + */ +static void add_privacy_params(pjsip_tx_data *tdata, pjsip_fromto_hdr *hdr, const struct ast_party_id *id) +{ + static const pj_str_t privacy_str = { "privacy", 7 }; + static const pj_str_t screen_str = { "screen", 6 }; + static const pj_str_t privacy_full_str = { "full", 4 }; + static const pj_str_t privacy_off_str = { "off", 3 }; + static const pj_str_t screen_yes_str = { "yes", 3 }; + static const pj_str_t screen_no_str = { "no", 2 }; + pjsip_param *old_privacy; + pjsip_param *old_screen; + pjsip_param *privacy; + pjsip_param *screen; + + old_privacy = pjsip_param_find(&hdr->other_param, &privacy_str); + old_screen = pjsip_param_find(&hdr->other_param, &screen_str); + + if (!old_privacy) { + privacy = PJ_POOL_ALLOC_T(tdata->pool, pjsip_param); + privacy->name = privacy_str; + pj_list_insert_before(&hdr->other_param, privacy); + } else { + privacy = old_privacy; + } + + if (!old_screen) { + screen = PJ_POOL_ALLOC_T(tdata->pool, pjsip_param); + screen->name = screen_str; + pj_list_insert_before(&hdr->other_param, screen); + } else { + screen = old_screen; + } + + if ((id->name.presentation & AST_PRES_RESTRICTION) == AST_PRES_ALLOWED && + (id->name.presentation & AST_PRES_RESTRICTION) == AST_PRES_ALLOWED) { + privacy->value = privacy_off_str; + } else { + privacy->value = privacy_full_str; + } + + if ((id->name.presentation & AST_PRES_NUMBER_TYPE) == AST_PRES_USER_NUMBER_PASSED_SCREEN && + (id->number.presentation & AST_PRES_NUMBER_TYPE) == AST_PRES_USER_NUMBER_PASSED_SCREEN) { + screen->value = screen_yes_str; + } else { + screen->value = screen_no_str; + } +} + +/*! + * \internal + * \brief Add a Remote-Party-ID header to an outbound message + * \param tdata The message to add the header to + * \param id The identification information used to populate the header + */ +static void add_rpid_header(pjsip_tx_data *tdata, const struct ast_party_id *id) +{ + static const pj_str_t pj_rpid_name = { "Remote-Party-ID", 15 }; + pjsip_fromto_hdr *rpid_hdr; + pjsip_fromto_hdr *old_rpid; + + if (!id->number.valid) { + return; + } + + /* Since inv_session reuses responses, we have to make sure there's not already + * a P-Asserted-Identity present. If there is, we just modify the old one. + */ + old_rpid = pjsip_msg_find_hdr_by_name(tdata->msg, &pj_rpid_name, NULL); + if (old_rpid) { + modify_id_header(tdata->pool, old_rpid, id); + add_privacy_params(tdata, old_rpid, id); + return; + } + + rpid_hdr = create_new_id_hdr(&pj_rpid_name, tdata, id); + if (!rpid_hdr) { + return; + } + add_privacy_params(tdata, rpid_hdr, id); + pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr *)rpid_hdr); +} + +/*! + * \internal + * \brief Add any appropriate identification headers to an outbound SIP message + * + * This will determine if an outbound message should have identification headers and + * will add the appropriately configured headers + * + * \param session The session on which we will be sending the message + * \param tdata The outbound message + * \param The identity information to place on the message + */ +static void add_id_headers(const struct ast_sip_session *session, pjsip_tx_data *tdata, const struct ast_party_id *id) +{ + if (((id->name.presentation & AST_PRES_RESTRICTION) == AST_PRES_RESTRICTED || + (id->number.presentation & AST_PRES_RESTRICTION) == AST_PRES_RESTRICTED) && + !session->endpoint->trust_id_outbound) { + return; + } + if (session->endpoint->send_pai) { + add_pai_header(tdata, id); + } + if (session->endpoint->send_rpid) { + add_rpid_header(tdata, id); + } +} + +/*! + * \internal + * \brief Session supplement callback for outgoing INVITE requests + * + * For an initial INVITE request, we may change the From header to appropriately + * reflect the identity information. On all INVITEs (initial and reinvite) we may + * add other identity headers such as P-Asserted-Identity and Remote-Party-ID based + * on configuration and privacy settings + * + * \param session The session on which the INVITE will be sent + * \param tdata The outbound INVITE request + */ +static void caller_id_outgoing_request(struct ast_sip_session *session, pjsip_tx_data *tdata) +{ + struct ast_party_id connected_id; + + if (!session->channel) { + return; + } + + connected_id = ast_channel_connected_effective_id(session->channel); + if (session->inv_session->state < PJSIP_INV_STATE_CONFIRMED && + session->endpoint->trust_id_outbound && + (connected_id.name.presentation & AST_PRES_RESTRICTION) == AST_PRES_ALLOWED && + (connected_id.name.presentation & AST_PRES_RESTRICTION) == AST_PRES_ALLOWED) { + /* Only change the From header on the initial outbound INVITE. Switching it + * mid-call might confuse some UAs. + */ + pjsip_fromto_hdr *from; + pjsip_dialog *dlg; + + from = pjsip_msg_find_hdr(tdata->msg, PJSIP_H_FROM, tdata->msg->hdr.next); + dlg = session->inv_session->dlg; + + modify_id_header(tdata->pool, from, &connected_id); + modify_id_header(dlg->pool, dlg->local.info, &connected_id); + if (should_queue_connected_line_update(session, &session->endpoint->id)) { + queue_connected_line_update(session, &session->endpoint->id); + } + } + add_id_headers(session, tdata, &connected_id); +} + +/*! + * \internal + * \brief Session supplement for outgoing INVITE response + * + * This will add P-Asserted-Identity and Remote-Party-ID headers if necessary + * + * \param session The session on which the INVITE response is to be sent + * \param tdata The outbound INVITE response + */ +static void caller_id_outgoing_response(struct ast_sip_session *session, pjsip_tx_data *tdata) +{ + struct ast_party_id connected_id; + + if (!session->channel) { + return; + } + connected_id = ast_channel_connected_effective_id(session->channel); + add_id_headers(session, tdata, &connected_id); +} + +static struct ast_sip_session_supplement caller_id_supplement = { + .method = "INVITE", + .priority = AST_SIP_SESSION_SUPPLEMENT_PRIORITY_CHANNEL - 1000, + .incoming_request = caller_id_incoming_request, + .incoming_response = caller_id_incoming_response, + .outgoing_request = caller_id_outgoing_request, + .outgoing_response = caller_id_outgoing_response, +}; + +static int load_module(void) +{ + ast_sip_session_register_supplement(&caller_id_supplement); + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + ast_sip_session_unregister_supplement(&caller_id_supplement); + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "SIP Caller ID Support", + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_APP_DEPEND, + ); diff --git a/res/res_sip_dtmf_info.c b/res/res_sip_dtmf_info.c new file mode 100644 index 000000000..453e57d06 --- /dev/null +++ b/res/res_sip_dtmf_info.c @@ -0,0 +1,128 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Jason Parker <jparker@digium.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>pjproject</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include <pjsip.h> +#include <pjsip_ua.h> + +#include "asterisk/res_sip.h" +#include "asterisk/res_sip_session.h" +#include "asterisk/module.h" + +static int dtmf_info_incoming_request(struct ast_sip_session *session, struct pjsip_rx_data *rdata) +{ + int res = 0; + pjsip_msg_body *body = rdata->msg_info.msg->body; + + pjsip_tx_data *tdata; + + char buf[body->len]; + char *cur = buf; + char *line; + + char event = '\0'; + unsigned int duration = 0; + + if (pj_strcmp2(&body->content_type.type, "application") || + pj_strcmp2(&body->content_type.subtype, "dtmf-relay")) { + return 0; + } + + body->print_body(body, buf, body->len); + + while ((line = strsep(&cur, "\r\n"))) { + char *c; + + if (!(c = strchr(line, '='))) { + continue; + } + *c++ = '\0'; + + c = ast_skip_blanks(c); + + if (!strcasecmp(line, "signal")) { + if (c[0] == '!' || c[0] == '*' || c[0] == '#' || + ('0' <= c[0] && c[0] <= '9') || + ('A' <= c[0] && c[0] <= 'D') || + ('a' <= c[0] && c[0] <= 'd')) { + event = c[0]; + } else { + ast_log(LOG_ERROR, "Invalid DTMF event signal in INFO message.\n"); + res = -1; + break; + } + } else if (!strcasecmp(line, "duration")) { + sscanf(c, "%30u", &duration); + } + } + + if (!duration) { + duration = 100; + } + + if (event == '!') { + struct ast_frame f = { AST_FRAME_CONTROL, { AST_CONTROL_FLASH, } }; + + ast_queue_frame(session->channel, &f); + } else if (event != '\0') { + struct ast_frame f = { AST_FRAME_DTMF, }; + f.len = duration; + f.subclass.integer = event; + + ast_queue_frame(session->channel, &f); + } else { + res = -1; + } + + if (pjsip_dlg_create_response(session->inv_session->dlg, rdata, !res ? 200 : 500, NULL, &tdata) == PJ_SUCCESS) { + struct pjsip_transaction *tsx = pjsip_rdata_get_tsx(rdata); + + pjsip_dlg_send_response(session->inv_session->dlg, tsx, tdata); + } + + return res; +} + +static struct ast_sip_session_supplement dtmf_info_supplement = { + .method = "INFO", + .incoming_request = dtmf_info_incoming_request, +}; + +static int load_module(void) +{ + ast_sip_session_register_supplement(&dtmf_info_supplement); + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + ast_sip_session_unregister_supplement(&dtmf_info_supplement); + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "SIP DTMF INFO Support", + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_APP_DEPEND, + ); diff --git a/res/res_sip_endpoint_identifier_constant.c b/res/res_sip_endpoint_identifier_constant.c new file mode 100644 index 000000000..e519a9ee8 --- /dev/null +++ b/res/res_sip_endpoint_identifier_constant.c @@ -0,0 +1,67 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Mark Michelson <mmichelson@digium.com> + * + * Includes code and algorithms from the Zapata library. + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*** MODULEINFO + <depend>pjproject</depend> + <defaultenabled>no</defaultenabled> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include <pjsip.h> + +#include "asterisk/res_sip.h" +#include "asterisk/module.h" + +static struct ast_sip_endpoint *constant_identify(pjsip_rx_data *rdata) +{ + /* This endpoint identifier always returns the same endpoint. It's used + * simply for testing. It allocates an endpoint from sorcery so default values + * do get applied. + */ + struct ast_sip_endpoint *endpoint = ast_sorcery_alloc(ast_sip_get_sorcery(), "endpoint", NULL); + if (!endpoint) { + return NULL; + } + ast_parse_allow_disallow(&endpoint->prefs, endpoint->codecs, "ulaw", 1); + return endpoint; +} + +static struct ast_sip_endpoint_identifier constant_identifier = { + .identify_endpoint = constant_identify, +}; + +static int load_module(void) +{ + ast_sip_register_endpoint_identifier(&constant_identifier); + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "SIP Constant Endpoint Identifier", + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_APP_DEPEND, + ); diff --git a/res/res_sip_endpoint_identifier_ip.c b/res/res_sip_endpoint_identifier_ip.c new file mode 100644 index 000000000..49c70b59d --- /dev/null +++ b/res/res_sip_endpoint_identifier_ip.c @@ -0,0 +1,151 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Mark Michelson <mmichelson@digium.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>pjproject</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include <pjsip.h> + +#include "asterisk/res_sip.h" +#include "asterisk/module.h" +#include "asterisk/acl.h" + +/*! \brief Structure for an IP identification matching object */ +struct ip_identify_match { + /*! \brief Sorcery object details */ + SORCERY_OBJECT(details); + /*! \brief Stringfields */ + AST_DECLARE_STRING_FIELDS( + /*! The name of the endpoint */ + AST_STRING_FIELD(endpoint_name); + ); + /*! \brief Networks or addresses that should match this */ + struct ast_ha *matches; +}; + +/*! \brief Destructor function for a matching object */ +static void ip_identify_destroy(void *obj) +{ + struct ip_identify_match *identify = obj; + + ast_string_field_free_memory(identify); + ast_free_ha(identify->matches); +} + +/*! \brief Allocator function for a matching object */ +static void *ip_identify_alloc(const char *name) +{ + struct ip_identify_match *identify = ao2_alloc(sizeof(*identify), ip_identify_destroy); + + if (!identify || ast_string_field_init(identify, 256)) { + ao2_cleanup(identify); + return NULL; + } + + return identify; +} + +/*! \brief Comparator function for a matching object */ +static int ip_identify_match_check(void *obj, void *arg, int flags) +{ + struct ip_identify_match *identify = obj; + struct ast_sockaddr *addr = arg; + + return (ast_apply_ha(identify->matches, addr) != AST_SENSE_ALLOW) ? CMP_MATCH | CMP_STOP : 0; +} + +static struct ast_sip_endpoint *ip_identify(pjsip_rx_data *rdata) +{ + struct ast_sockaddr addr = { { 0, } }; + RAII_VAR(struct ao2_container *, candidates, NULL, ao2_cleanup); + RAII_VAR(struct ip_identify_match *, match, NULL, ao2_cleanup); + + /* If no possibilities exist return early to save some time */ + if (!(candidates = ast_sorcery_retrieve_by_fields(ast_sip_get_sorcery(), "identify", AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL)) || + !ao2_container_count(candidates)) { + return NULL; + } + + ast_sockaddr_parse(&addr, rdata->pkt_info.src_name, PARSE_PORT_FORBID); + ast_sockaddr_set_port(&addr, rdata->pkt_info.src_port); + + if (!(match = ao2_callback(candidates, 0, ip_identify_match_check, &addr))) { + return NULL; + } + + return ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", match->endpoint_name); +} + +static struct ast_sip_endpoint_identifier ip_identifier = { + .identify_endpoint = ip_identify, +}; + +/*! \brief Custom handler for match field */ +static int ip_identify_match_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct ip_identify_match *identify = obj; + int error = 0; + + /* We deny what we actually want to match because there is an implicit permit all rule for ACLs */ + if (!(identify->matches = ast_append_ha("d", var->value, identify->matches, &error))) { + return -1; + } + + return error; +} + +static int load_module(void) +{ + ast_sorcery_apply_default(ast_sip_get_sorcery(), "identify", "config", "res_sip.conf,criteria=type=identify"); + + if (ast_sorcery_object_register(ast_sip_get_sorcery(), "identify", ip_identify_alloc, NULL, NULL)) { + return AST_MODULE_LOAD_DECLINE; + } + + ast_sorcery_object_field_register(ast_sip_get_sorcery(), "identify", "type", "", OPT_NOOP_T, 0, 0); + ast_sorcery_object_field_register(ast_sip_get_sorcery(), "identify", "endpoint", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ip_identify_match, endpoint_name)); + ast_sorcery_object_field_register_custom(ast_sip_get_sorcery(), "identify", "match", "", ip_identify_match_handler, NULL, 0, 0); + ast_sorcery_reload_object(ast_sip_get_sorcery(), "identify"); + + ast_sip_register_endpoint_identifier(&ip_identifier); + + return AST_MODULE_LOAD_SUCCESS; +} + +static int reload_module(void) +{ + ast_sorcery_reload_object(ast_sip_get_sorcery(), "identify"); + return 0; +} + +static int unload_module(void) +{ + ast_sip_unregister_endpoint_identifier(&ip_identifier); + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "SIP IP endpoint identifier", + .load = load_module, + .reload = reload_module, + .unload = unload_module, + .load_pri = AST_MODPRI_APP_DEPEND, + ); diff --git a/res/res_sip_endpoint_identifier_user.c b/res/res_sip_endpoint_identifier_user.c new file mode 100644 index 000000000..cd1f76bb1 --- /dev/null +++ b/res/res_sip_endpoint_identifier_user.c @@ -0,0 +1,128 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Mark Michelson <mmichelson@digium.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>pjproject</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include <pjsip.h> + +#include "asterisk/res_sip.h" +#include "asterisk/module.h" + +static int get_endpoint_details(pjsip_rx_data *rdata, char *endpoint, size_t endpoint_size, char *domain, size_t domain_size) +{ + pjsip_uri *from = rdata->msg_info.from->uri; + pjsip_sip_uri *sip_from; + if (!PJSIP_URI_SCHEME_IS_SIP(from) && !PJSIP_URI_SCHEME_IS_SIPS(from)) { + return -1; + } + sip_from = (pjsip_sip_uri *) pjsip_uri_get_uri(from); + ast_copy_pj_str(endpoint, &sip_from->user, endpoint_size); + ast_copy_pj_str(domain, &sip_from->host, domain_size); + return 0; +} + +static int find_transport_in_use(void *obj, void *arg, int flags) +{ + struct ast_sip_transport *transport = obj; + pjsip_rx_data *rdata = arg; + + if ((transport->state->transport == rdata->tp_info.transport) || + (transport->state->factory && !pj_strcmp(&transport->state->factory->addr_name.host, &rdata->tp_info.transport->local_name.host) && + transport->state->factory->addr_name.port == rdata->tp_info.transport->local_name.port)) { + return CMP_MATCH | CMP_STOP; + } + + return 0; +} + +static struct ast_sip_endpoint *username_identify(pjsip_rx_data *rdata) +{ + char endpoint_name[64], domain_name[64], id[AST_UUID_STR_LEN]; + struct ast_sip_endpoint *endpoint; + RAII_VAR(struct ast_sip_domain_alias *, alias, NULL, ao2_cleanup); + RAII_VAR(struct ao2_container *, transports, NULL, ao2_cleanup); + RAII_VAR(struct ast_sip_transport *, transport, NULL, ao2_cleanup); + + if (get_endpoint_details(rdata, endpoint_name, sizeof(endpoint_name), domain_name, sizeof(domain_name))) { + return NULL; + } + + /* Attempt to find the endpoint given the name and domain provided */ + snprintf(id, sizeof(id), "%s@%s", endpoint_name, domain_name); + if ((endpoint = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", id))) { + goto done; + } + + /* See if an alias exists for the domain provided */ + if ((alias = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "domain_alias", domain_name))) { + snprintf(id, sizeof(id), "%s@%s", endpoint_name, alias->domain); + if ((endpoint = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", id))) { + goto done; + } + } + + /* See if the transport this came in on has a provided domain */ + if ((transports = ast_sorcery_retrieve_by_fields(ast_sip_get_sorcery(), "transport", AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL)) && + (transport = ao2_callback(transports, 0, find_transport_in_use, rdata)) && + !ast_strlen_zero(transport->domain)) { + snprintf(id, sizeof(id), "%s@%s", endpoint_name, transport->domain); + if ((endpoint = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", id))) { + goto done; + } + } + + /* Fall back to no domain */ + endpoint = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", endpoint_name); + +done: + if (endpoint) { + if (!(endpoint->ident_method & AST_SIP_ENDPOINT_IDENTIFY_BY_USERNAME)) { + ao2_ref(endpoint, -1); + return NULL; + } + ast_debug(3, "Retrieved endpoint %s\n", ast_sorcery_object_get_id(endpoint)); + } + return endpoint; +} + +static struct ast_sip_endpoint_identifier username_identifier = { + .identify_endpoint = username_identify, +}; + +static int load_module(void) +{ + ast_sip_register_endpoint_identifier(&username_identifier); + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + ast_sip_unregister_endpoint_identifier(&username_identifier); + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "SIP username endpoint identifier", + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_APP_DEPEND, + ); diff --git a/res/res_sip_logger.c b/res/res_sip_logger.c new file mode 100644 index 000000000..da1719810 --- /dev/null +++ b/res/res_sip_logger.c @@ -0,0 +1,81 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Mark Michelson <mmichelson@digium.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>pjproject</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include <pjsip.h> + +#include "asterisk/res_sip.h" +#include "asterisk/module.h" +#include "asterisk/logger.h" + +static pj_status_t logging_on_tx_msg(pjsip_tx_data *tdata) +{ + ast_verbose("<--- Transmitting SIP %s (%d bytes) to %s:%s:%d --->\n%.*s\n", + tdata->msg->type == PJSIP_REQUEST_MSG ? "request" : "response", + (int) (tdata->buf.cur - tdata->buf.start), + tdata->tp_info.transport->type_name, + tdata->tp_info.dst_name, + tdata->tp_info.dst_port, + (int) (tdata->buf.end - tdata->buf.start), tdata->buf.start); + return PJ_SUCCESS; +} + +static pj_bool_t logging_on_rx_msg(pjsip_rx_data *rdata) +{ + ast_verbose("<--- Received SIP %s (%d bytes) from %s:%s:%d --->\n%s\n", + rdata->msg_info.msg->type == PJSIP_REQUEST_MSG ? "request" : "response", + rdata->msg_info.len, + rdata->tp_info.transport->type_name, + rdata->pkt_info.src_name, + rdata->pkt_info.src_port, + rdata->pkt_info.packet); + return PJ_FALSE; +} + +static pjsip_module logging_module = { + .name = { "Logging Module", 14 }, + .priority = 0, + .on_rx_request = logging_on_rx_msg, + .on_rx_response = logging_on_rx_msg, + .on_tx_request = logging_on_tx_msg, + .on_tx_response = logging_on_tx_msg, +}; + +static int load_module(void) +{ + ast_sip_register_service(&logging_module); + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + ast_sip_unregister_service(&logging_module); + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "SIP Packet Logger", + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_APP_DEPEND, + ); diff --git a/res/res_sip_mwi.c b/res/res_sip_mwi.c new file mode 100644 index 000000000..7d62816d0 --- /dev/null +++ b/res/res_sip_mwi.c @@ -0,0 +1,709 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Mark Michelson <mmichelson@digium.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 <pjsip.h> +#include <pjsip_simple.h> +#include <pjlib.h> + +#include "asterisk/res_sip.h" +#include "asterisk/res_sip_pubsub.h" +#include "asterisk/module.h" +#include "asterisk/logger.h" +#include "asterisk/astobj2.h" +#include "asterisk/sorcery.h" +#include "asterisk/stasis.h" +#include "asterisk/app.h" + +/*** MODULEINFO + <depend>pjproject</depend> + <depend>res_sip</depend> + <depend>res_sip_pubsub</depend> + <support_level>core</support_level> + ***/ + +struct mwi_subscription; +AO2_GLOBAL_OBJ_STATIC(unsolicited_mwi); + +#define STASIS_BUCKETS 13 +#define MWI_BUCKETS 53 +static void mwi_subscription_shutdown(struct ast_sip_subscription *sub); +static struct ast_sip_subscription *mwi_new_subscribe(struct ast_sip_endpoint *endpoint, + pjsip_rx_data *rdata); +static void mwi_resubscribe(struct ast_sip_subscription *sub, pjsip_rx_data *rdata, + struct ast_sip_subscription_response_data *response_data); +static void mwi_subscription_timeout(struct ast_sip_subscription *sub); +static void mwi_subscription_terminated(struct ast_sip_subscription *sub, pjsip_rx_data *rdata); +static void mwi_notify_response(struct ast_sip_subscription *sub, pjsip_rx_data *rdata); +static void mwi_notify_request(struct ast_sip_subscription *sub, pjsip_rx_data *rdata, + struct ast_sip_subscription_response_data *response_data); +static int mwi_refresh_subscription(struct ast_sip_subscription *sub); + +static struct ast_sip_subscription_handler mwi_handler = { + .event_name = "message-summary", + .accept = { "application/simple-message-summary", }, + .subscription_shutdown = mwi_subscription_shutdown, + .new_subscribe = mwi_new_subscribe, + .resubscribe = mwi_resubscribe, + .subscription_timeout = mwi_subscription_timeout, + .subscription_terminated = mwi_subscription_terminated, + .notify_response = mwi_notify_response, + .notify_request = mwi_notify_request, + .refresh_subscription = mwi_refresh_subscription, +}; + +/*! + * \brief Wrapper for stasis subscription + * + * An MWI subscription has a container of these. This + * represents a stasis subscription for MWI state. + */ +struct mwi_stasis_subscription { + /*! The MWI stasis subscription */ + struct stasis_subscription *stasis_sub; + /*! The mailbox corresponding with the MWI subscription. Used as a hash key */ + char mailbox[1]; +}; + +/*! + * \brief A subscription for MWI + * + * This subscription is the basis for MWI for an endpoint. Each + * endpoint that uses MWI will have a corresponding mwi_subscription. + * + * This structure acts as the owner for the underlying SIP subscription. + * When the mwi_subscription is destroyed, the SIP subscription dies, too. + * The mwi_subscription's lifetime is governed by its underlying stasis + * subscriptions. When all stasis subscriptions are destroyed, the + * mwi_subscription is destroyed as well. + */ +struct mwi_subscription { + /*! Container of \ref mwi_stasis_subscription structures. + * A single MWI subscription may be fore multiple mailboxes, thus + * requiring multiple stasis subscriptions + */ + struct ao2_container *stasis_subs; + /*! The SIP subscription. Unsolicited MWI does not use this */ + struct ast_sip_subscription *sip_sub; + /*! Is the MWI solicited (i.e. Initiated with an external SUBSCRIBE) ? */ + unsigned int is_solicited; + /*! Identifier for the subscription. + * The identifier is the same as the corresponding endpoint's stasis ID. + * Used as a hash key + */ + char id[1]; +}; + +static void mwi_stasis_cb(void *userdata, struct stasis_subscription *sub, + struct stasis_topic *topic, struct stasis_message *msg); + +static struct mwi_stasis_subscription *mwi_stasis_subscription_alloc(const char *mailbox, struct mwi_subscription *mwi_sub) +{ + struct mwi_stasis_subscription *mwi_stasis_sub; + struct stasis_topic *topic; + + if (!mwi_sub) { + return NULL; + } + + mwi_stasis_sub = ao2_alloc(sizeof(*mwi_stasis_sub) + strlen(mailbox), NULL); + if (!mwi_stasis_sub) { + return NULL; + } + + topic = stasis_mwi_topic(mailbox); + + /* Safe strcpy */ + strcpy(mwi_stasis_sub->mailbox, mailbox); + ao2_ref(mwi_sub, +1); + ast_debug(3, "Creating stasis MWI subscription to mailbox %s for endpoint %s\n", mailbox, mwi_sub->id); + mwi_stasis_sub->stasis_sub = stasis_subscribe(topic, mwi_stasis_cb, mwi_sub); + return mwi_stasis_sub; +} + +static int stasis_sub_hash(const void *obj, int flags) +{ + const struct mwi_stasis_subscription *mwi_stasis = obj; + + return ast_str_hash(mwi_stasis->mailbox); +} + +static int stasis_sub_cmp(void *obj, void *arg, int flags) +{ + struct mwi_stasis_subscription *mwi_stasis1 = obj; + struct mwi_stasis_subscription *mwi_stasis2 = arg; + + return strcmp(mwi_stasis1->mailbox, mwi_stasis2->mailbox) ? 0 : CMP_MATCH; +} + +static void mwi_subscription_destructor(void *obj) +{ + struct mwi_subscription *sub = obj; + + ast_debug(3, "Destroying MWI subscription for endpoint %s\n", sub->id); + ao2_cleanup(sub->sip_sub); + ao2_cleanup(sub->stasis_subs); +} + +static struct mwi_subscription *mwi_subscription_alloc(struct ast_sip_endpoint *endpoint, + enum ast_sip_subscription_role role, unsigned int is_solicited, pjsip_rx_data *rdata) +{ + struct mwi_subscription *sub; + const char *endpoint_id = ast_sorcery_object_get_id(endpoint); + + sub = ao2_alloc(sizeof(*sub) + strlen(endpoint_id), + mwi_subscription_destructor); + + if (!sub) { + return NULL; + } + + /* Safe strcpy */ + strcpy(sub->id, endpoint_id); + /* Unsolicited MWI doesn't actually result in a SIP subscription being + * created. This is because a SIP subscription associates with a dialog. + * Most devices expect unsolicited MWI NOTIFYs to appear out of dialog. If + * they receive an in-dialog MWI NOTIFY (i.e. with a to-tag), then they + * will reject the NOTIFY with a 481, thus resulting in message-waiting + * state not being updated on the device + */ + if (is_solicited) { + sub->sip_sub = ast_sip_create_subscription(&mwi_handler, + role, endpoint, rdata); + if (!sub->sip_sub) { + ast_log(LOG_WARNING, "Unable to create MWI SIP subscription for endpoint %s\n", sub->id); + ao2_cleanup(sub); + return NULL; + } + } + + sub->stasis_subs = ao2_container_alloc(STASIS_BUCKETS, stasis_sub_hash, stasis_sub_cmp); + if (!sub->stasis_subs) { + ao2_cleanup(sub); + return NULL; + } + sub->is_solicited = is_solicited; + + ast_debug(3, "Created %s MWI subscription for endpoint %s\n", is_solicited ? "solicited" : "unsolicited", sub->id); + + return sub; +} + +static int mwi_sub_hash(const void *obj, int flags) +{ + const struct mwi_subscription *mwi_sub = obj; + + return ast_str_hash(mwi_sub->id); +} + +static int mwi_sub_cmp(void *obj, void *arg, int flags) +{ + struct mwi_subscription *mwi_sub1 = obj; + struct mwi_subscription *mwi_sub2 = arg; + + return strcmp(mwi_sub1->id, mwi_sub2->id) ? 0 : CMP_MATCH; +} + +struct message_accumulator { + int old_msgs; + int new_msgs; + const char *reason; +}; + +static int get_message_count(void *obj, void *arg, int flags) +{ + RAII_VAR(struct stasis_message *, msg, NULL, ao2_cleanup); + struct mwi_stasis_subscription *mwi_stasis = obj; + struct message_accumulator *counter = arg; + struct stasis_mwi_state *mwi_state; + + msg = stasis_cache_get(stasis_mwi_topic_cached(), stasis_mwi_state_type(), mwi_stasis->mailbox); + if (!msg) { + return 0; + } + + mwi_state = stasis_message_data(msg); + counter->old_msgs += mwi_state->old_msgs; + counter->new_msgs += mwi_state->new_msgs; + return 0; +} + +struct unsolicited_mwi_data { + struct mwi_subscription *sub; + struct ast_sip_endpoint *endpoint; + pjsip_evsub_state state; + const char *reason; + const pjsip_media_type *mwi_type; + const pj_str_t *body_text; +}; + +static int send_unsolicited_mwi_notify_to_contact(void *obj, void *arg, int flags) +{ + struct unsolicited_mwi_data *mwi_data = arg; + struct mwi_subscription *sub = mwi_data->sub; + struct ast_sip_endpoint *endpoint = mwi_data->endpoint; + pjsip_evsub_state state = mwi_data->state; + const char *reason = mwi_data->reason; + const pjsip_media_type *mwi_type = mwi_data->mwi_type; + const pj_str_t *body_text = mwi_data->body_text; + struct ast_sip_contact *contact = obj; + const char *state_name; + pjsip_tx_data *tdata; + pjsip_msg_body *msg_body; + pjsip_sub_state_hdr *sub_state; + pjsip_event_hdr *event; + const pjsip_hdr *allow_events = pjsip_evsub_get_allow_events_hdr(NULL); + + if (ast_sip_create_request("NOTIFY", NULL, endpoint, contact->uri, &tdata)) { + ast_log(LOG_WARNING, "Unable to create unsolicited NOTIFY request to endpoint %s URI %s\n", sub->id, contact->uri); + return 0; + } + + switch (state) { + case PJSIP_EVSUB_STATE_ACTIVE: + state_name = "active"; + break; + case PJSIP_EVSUB_STATE_TERMINATED: + default: + state_name = "terminated"; + break; + } + + sub_state = pjsip_sub_state_hdr_create(tdata->pool); + pj_cstr(&sub_state->sub_state, state_name); + if (reason) { + pj_cstr(&sub_state->reason_param, reason); + } + pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr *) sub_state); + + event = pjsip_event_hdr_create(tdata->pool); + pj_cstr(&event->event_type, "message-summary"); + pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr *) event); + + pjsip_msg_add_hdr(tdata->msg, pjsip_hdr_shallow_clone(tdata->pool, allow_events)); + msg_body = pjsip_msg_body_create(tdata->pool, &mwi_type->type, &mwi_type->subtype, body_text); + tdata->msg->body = msg_body; + ast_sip_send_request(tdata, NULL, endpoint); + + return 0; +} + +static void send_unsolicited_mwi_notify(struct mwi_subscription *sub, pjsip_evsub_state state, const char *reason, + const pjsip_media_type *mwi_type, const pj_str_t *body_text) +{ + RAII_VAR(struct ast_sip_endpoint *, endpoint, ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), + "endpoint", sub->id), ao2_cleanup); + char *endpoint_aors; + char *aor_name; + + if (!endpoint) { + ast_log(LOG_WARNING, "Unable to send unsolicited MWI to %s because endpoint does not exist\n", + sub->id); + return; + } + if (ast_strlen_zero(endpoint->aors)) { + ast_log(LOG_WARNING, "Unable to send unsolicited MWI to %s because the endpoint has no" + " configured AORs\n", sub->id); + return; + } + + endpoint_aors = ast_strdupa(endpoint->aors); + + while ((aor_name = strsep(&endpoint_aors, ","))) { + RAII_VAR(struct ast_sip_aor *, aor, ast_sip_location_retrieve_aor(aor_name), ao2_cleanup); + RAII_VAR(struct ao2_container *, contacts, NULL, ao2_cleanup); + struct unsolicited_mwi_data mwi_data = { + .sub = sub, + .endpoint = endpoint, + .state = state, + .reason = reason, + .mwi_type = mwi_type, + .body_text = body_text, + }; + + if (!aor) { + ast_log(LOG_WARNING, "Unable to locate AOR %s for unsolicited MWI\n", aor_name); + continue; + } + + contacts = ast_sip_location_retrieve_aor_contacts(aor); + if (!contacts || (ao2_container_count(contacts) == 0)) { + ast_log(LOG_WARNING, "No contacts bound to AOR %s. Cannot send unsolicited MWI.\n", aor_name); + continue; + } + + ao2_callback(contacts, OBJ_NODATA, send_unsolicited_mwi_notify_to_contact, &mwi_data); + } +} + +static void send_mwi_notify(struct mwi_subscription *sub, pjsip_evsub_state state, const char *reason) +{ + const pj_str_t *reason_str_ptr = NULL; + static pjsip_media_type mwi_type = { + .type = { "application", 11 }, + .subtype = { "simple-message-summary", 22 }, + }; + struct message_accumulator counter = { + .old_msgs = 0, + .new_msgs = 0, + }; + RAII_VAR(struct ast_str *, body, ast_str_create(64), ast_free_ptr); + pjsip_tx_data *tdata; + pj_str_t reason_str; + pj_str_t pj_body; + + ao2_callback(sub->stasis_subs, OBJ_NODATA, get_message_count, &counter); + + if (reason) { + pj_cstr(&reason_str, reason); + reason_str_ptr = &reason_str; + } + ast_str_append(&body, 0, "Messages-Waiting: %s\r\n", counter.new_msgs ? "yes" : "no"); + ast_str_append(&body, 0, "Voice-Message: %d/%d (0/0)\r\n", counter.new_msgs, counter.old_msgs); + pj_cstr(&pj_body, ast_str_buffer(body)); + + ast_debug(5, "Sending %s MWI NOTIFY to endpoint %s, new messages: %d, old messages: %d\n", + sub->is_solicited ? "solicited" : "unsolicited", sub->id, counter.new_msgs, + counter.old_msgs); + + if (sub->is_solicited) { + if (pjsip_mwi_notify(ast_sip_subscription_get_evsub(sub->sip_sub), + state, + NULL, + reason_str_ptr, + &mwi_type, + &pj_body, + &tdata) != PJ_SUCCESS) { + ast_log(LOG_WARNING, "Unable to create MWI NOTIFY request to %s.\n", sub->id); + return; + } + if (ast_sip_subscription_send_request(sub->sip_sub, tdata) != PJ_SUCCESS) { + ast_log(LOG_WARNING, "Unable to send MWI NOTIFY request to %s\n", sub->id); + return; + } + } else { + send_unsolicited_mwi_notify(sub, state, reason, &mwi_type, &pj_body); + } +} + +static int unsubscribe_stasis(void *obj, void *arg, int flags) +{ + struct mwi_stasis_subscription *mwi_stasis = obj; + if (mwi_stasis->stasis_sub) { + ast_debug(3, "Removing stasis subscription to mailbox %s\n", mwi_stasis->mailbox); + mwi_stasis->stasis_sub = stasis_unsubscribe(mwi_stasis->stasis_sub); + } + return CMP_MATCH; +} + +static void mwi_subscription_shutdown(struct ast_sip_subscription *sub) +{ + struct mwi_subscription *mwi_sub; + RAII_VAR(struct ast_datastore *, mwi_datastore, + ast_sip_subscription_get_datastore(sub, "MWI datastore"), ao2_cleanup); + + if (!mwi_datastore) { + return; + } + + mwi_sub = mwi_datastore->data; + ao2_callback(mwi_sub->stasis_subs, OBJ_UNLINK | OBJ_NODATA | OBJ_MULTIPLE, unsubscribe_stasis, NULL); +} + +static struct ast_datastore_info mwi_ds_info = { }; + +static int add_mwi_datastore(struct mwi_subscription *sub) +{ + RAII_VAR(struct ast_datastore *, mwi_datastore, NULL, ao2_cleanup); + + mwi_datastore = ast_sip_subscription_alloc_datastore(&mwi_ds_info, "MWI datastore"); + if (!mwi_datastore) { + return -1; + } + mwi_datastore->data = sub; + + ast_sip_subscription_add_datastore(sub->sip_sub, mwi_datastore); + return 0; +} + +static struct ast_sip_subscription *mwi_new_subscribe(struct ast_sip_endpoint *endpoint, + pjsip_rx_data *rdata) +{ + RAII_VAR(struct ast_sip_aor *, aor, NULL, ao2_cleanup); + /* It's not obvious here, but the reference(s) to this subscription, + * once this function exits, is held by the stasis subscription(s) + * created in mwi_stasis_subscription_alloc() + */ + RAII_VAR(struct mwi_subscription *, sub, NULL, ao2_cleanup); + pjsip_uri *ruri = rdata->msg_info.msg->line.req.uri; + pjsip_sip_uri *sip_ruri; + pjsip_evsub *evsub; + char aor_name[80]; + char *mailboxes; + char *mailbox; + + if (!PJSIP_URI_SCHEME_IS_SIP(ruri) && !PJSIP_URI_SCHEME_IS_SIPS(ruri)) { + ast_log(LOG_WARNING, "Attempt to SUBSCRIBE to a non-SIP URI\n"); + return NULL; + } + sip_ruri = pjsip_uri_get_uri(ruri); + ast_copy_pj_str(aor_name, &sip_ruri->user, sizeof(aor_name)); + + aor = ast_sip_location_retrieve_aor(aor_name); + if (!aor) { + ast_log(LOG_WARNING, "Unable to locate aor %s. MWI subscription failed.\n", aor_name); + return NULL; + } + + if (ast_strlen_zero(aor->mailboxes)) { + ast_log(LOG_WARNING, "AOR %s has no configured mailboxes. MWI subscription failed\n", aor_name); + return NULL; + } + + sub = mwi_subscription_alloc(endpoint, AST_SIP_NOTIFIER, 1, rdata); + if (!sub) { + return NULL; + } + + if (add_mwi_datastore(sub)) { + ast_log(LOG_WARNING, "Unable to allocate datastore on MWI subscription from %s\n", sub->id); + return NULL; + } + + mailboxes = ast_strdupa(aor->mailboxes); + while ((mailbox = strsep(&mailboxes, ","))) { + RAII_VAR(struct mwi_stasis_subscription *, mwi_stasis_sub, + mwi_stasis_subscription_alloc(mailbox, sub), ao2_cleanup); + if (mwi_stasis_sub) { + ao2_link(sub->stasis_subs, mwi_stasis_sub); + } + } + + evsub = ast_sip_subscription_get_evsub(sub->sip_sub); + pjsip_evsub_accept(evsub, rdata, 200, NULL); + send_mwi_notify(sub, PJSIP_EVSUB_STATE_ACTIVE, NULL); + + return sub->sip_sub; +} + +static void mwi_resubscribe(struct ast_sip_subscription *sub, + pjsip_rx_data *rdata, struct ast_sip_subscription_response_data *response_data) +{ + pjsip_tx_data *tdata; + + pjsip_mwi_current_notify(ast_sip_subscription_get_evsub(sub), &tdata); + ast_sip_subscription_send_request(sub, tdata); +} + +static void mwi_subscription_timeout(struct ast_sip_subscription *sub) +{ + struct mwi_subscription *mwi_sub; + RAII_VAR(struct ast_datastore *, mwi_datastore, + ast_sip_subscription_get_datastore(sub, "MWI datastore"), ao2_cleanup); + + if (!mwi_datastore) { + return; + } + + + mwi_sub = mwi_datastore->data; + + ast_log(LOG_NOTICE, "MWI subscription for %s has timed out.\n", mwi_sub->id); + + send_mwi_notify(mwi_sub, PJSIP_EVSUB_STATE_TERMINATED, "timeout"); +} + +static void mwi_subscription_terminated(struct ast_sip_subscription *sub, pjsip_rx_data *rdata) +{ + struct mwi_subscription *mwi_sub; + RAII_VAR(struct ast_datastore *, mwi_datastore, + ast_sip_subscription_get_datastore(sub, "MWI datastore"), ao2_cleanup); + + if (!mwi_datastore) { + return; + } + + mwi_sub = mwi_datastore->data; + + ast_log(LOG_NOTICE, "MWI subscription for %s has been terminated\n", mwi_sub->id); + + send_mwi_notify(mwi_sub, PJSIP_EVSUB_STATE_TERMINATED, NULL); +} + +static void mwi_notify_response(struct ast_sip_subscription *sub, pjsip_rx_data *rdata) +{ + /* We don't really care about NOTIFY responses for the moment */ +} + +static void mwi_notify_request(struct ast_sip_subscription *sub, pjsip_rx_data *rdata, + struct ast_sip_subscription_response_data *response_data) +{ + ast_log(LOG_WARNING, "Received an MWI NOTIFY request? This should not happen\n"); +} + +static int mwi_refresh_subscription(struct ast_sip_subscription *sub) +{ + ast_log(LOG_WARNING, "Being told to refresh an MWI subscription? This should not happen\n"); + return 0; +} + +static int serialized_notify(void *userdata) +{ + struct mwi_subscription *mwi_sub = userdata; + + send_mwi_notify(mwi_sub, PJSIP_EVSUB_STATE_ACTIVE, NULL); + ao2_ref(mwi_sub, -1); + return 0; +} + +static int serialized_cleanup(void *userdata) +{ + struct mwi_subscription *mwi_sub = userdata; + + /* This is getting rid of the reference that was added + * just before this serialized task was pushed. + */ + ao2_cleanup(mwi_sub); + /* This is getting rid of the reference held by the + * stasis subscription + */ + ao2_cleanup(mwi_sub); + return 0; +} + +static void mwi_stasis_cb(void *userdata, struct stasis_subscription *sub, + struct stasis_topic *topic, struct stasis_message *msg) +{ + struct mwi_subscription *mwi_sub = userdata; + + if (stasis_subscription_final_message(sub, msg)) { + ao2_ref(mwi_sub, +1); + ast_sip_push_task(NULL, serialized_cleanup, mwi_sub); + return; + } + + if (stasis_mwi_state_type() == stasis_message_type(msg)) { + struct ast_taskprocessor *serializer = mwi_sub->is_solicited ? ast_sip_subscription_get_serializer(mwi_sub->sip_sub) : NULL; + ao2_ref(mwi_sub, +1); + ast_sip_push_task(serializer, serialized_notify, mwi_sub); + } +} + +static int create_mwi_subscriptions_for_endpoint(void *obj, void *arg, int flags) +{ + RAII_VAR(struct mwi_subscription *, aggregate_sub, NULL, ao2_cleanup); + struct ast_sip_endpoint *endpoint = obj; + struct ao2_container *mwi_subscriptions = arg; + char *mailboxes; + char *mailbox; + + if (ast_strlen_zero(endpoint->mailboxes)) { + return 0; + } + + if (endpoint->aggregate_mwi) { + aggregate_sub = mwi_subscription_alloc(endpoint, AST_SIP_NOTIFIER, 0, NULL); + if (!aggregate_sub) { + return 0; + } + } + + mailboxes = ast_strdupa(endpoint->mailboxes); + while ((mailbox = strsep(&mailboxes, ","))) { + struct mwi_subscription *sub = aggregate_sub ?: + mwi_subscription_alloc(endpoint, AST_SIP_SUBSCRIBER, 0, NULL); + RAII_VAR(struct mwi_stasis_subscription *, mwi_stasis_sub, + mwi_stasis_subscription_alloc(mailbox, sub), ao2_cleanup); + if (mwi_stasis_sub) { + ao2_link(sub->stasis_subs, mwi_stasis_sub); + } + if (!aggregate_sub) { + ao2_link(mwi_subscriptions, sub); + ao2_cleanup(sub); + } + } + ao2_link(mwi_subscriptions, aggregate_sub); + return 0; +} + +static int unsubscribe(void *obj, void *arg, int flags) +{ + struct mwi_subscription *mwi_sub = obj; + + ao2_callback(mwi_sub->stasis_subs, OBJ_UNLINK | OBJ_NODATA | OBJ_MULTIPLE, unsubscribe_stasis, NULL); + return CMP_MATCH; +} + +static void create_mwi_subscriptions(void) +{ + struct ao2_container *mwi_subscriptions = ao2_container_alloc(MWI_BUCKETS, mwi_sub_hash, mwi_sub_cmp); + RAII_VAR(struct ao2_container *, old_mwi_subscriptions, ao2_global_obj_ref(unsolicited_mwi), ao2_cleanup); + RAII_VAR(struct ao2_container *, endpoints, ast_sorcery_retrieve_by_fields( + ast_sip_get_sorcery(), "endpoint", AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL), + ao2_cleanup); + + if (!mwi_subscriptions) { + return; + } + + /* We remove all the old stasis subscriptions first before applying the new configuration. This + * prevents a situation where there might be multiple overlapping stasis subscriptions for an + * endpoint for mailboxes. Though there may be mailbox changes during the gap between unsubscribing + * and resubscribing, up-to-date mailbox state will be sent out to the endpoint when the + * new stasis subscription is established + */ + if (old_mwi_subscriptions) { + ao2_callback(old_mwi_subscriptions, OBJ_UNLINK | OBJ_NODATA | OBJ_MULTIPLE, unsubscribe, NULL); + } + ao2_callback(endpoints, OBJ_NODATA, create_mwi_subscriptions_for_endpoint, mwi_subscriptions); + ao2_global_obj_replace_unref(unsolicited_mwi, mwi_subscriptions); +} + +static int reload(void) +{ + create_mwi_subscriptions(); + return 0; +} + +static int load_module(void) +{ + if (ast_sip_register_subscription_handler(&mwi_handler)) { + return AST_MODULE_LOAD_DECLINE; + } + create_mwi_subscriptions(); + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + RAII_VAR(struct ao2_container *, mwi_subscriptions, ao2_global_obj_ref(unsolicited_mwi), ao2_cleanup); + if (mwi_subscriptions) { + ao2_callback(mwi_subscriptions, OBJ_UNLINK | OBJ_NODATA | OBJ_MULTIPLE, unsubscribe, NULL); + ao2_global_obj_release(unsolicited_mwi); + } + ast_sip_unregister_subscription_handler(&mwi_handler); + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "SIP MWI resource", + .load = load_module, + .unload = unload_module, + .reload = reload, + .load_pri = AST_MODPRI_CHANNEL_DEPEND, +); diff --git a/res/res_sip_nat.c b/res/res_sip_nat.c new file mode 100644 index 000000000..6c924af68 --- /dev/null +++ b/res/res_sip_nat.c @@ -0,0 +1,235 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Joshua Colp <jcolp@digium.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 + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include <pjsip.h> +#include <pjsip_ua.h> + +#include "asterisk/res_sip.h" +#include "asterisk/module.h" +#include "asterisk/acl.h" + +static pj_bool_t nat_on_rx_request(pjsip_rx_data *rdata) +{ + RAII_VAR(struct ast_sip_endpoint *, endpoint, ast_pjsip_rdata_get_endpoint(rdata), ao2_cleanup); + pjsip_contact_hdr *contact; + + if (!endpoint) { + return PJ_FALSE; + } + + if (endpoint->rewrite_contact && (contact = pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_CONTACT, NULL)) && + (PJSIP_URI_SCHEME_IS_SIP(contact->uri) || PJSIP_URI_SCHEME_IS_SIPS(contact->uri))) { + pjsip_sip_uri *uri = pjsip_uri_get_uri(contact->uri); + + pj_cstr(&uri->host, rdata->pkt_info.src_name); + uri->port = rdata->pkt_info.src_port; + } + + if (endpoint->force_rport) { + rdata->msg_info.via->rport_param = 0; + } + + return PJ_FALSE; +} + +/*! \brief Structure which contains information about a transport */ +struct request_transport_details { + /*! \brief Type of transport */ + enum ast_sip_transport_type type; + /*! \brief Potential pointer to the transport itself, if UDP */ + pjsip_transport *transport; + /*! \brief Potential pointer to the transport factory itself, if TCP/TLS */ + pjsip_tpfactory *factory; + /*! \brief Local address for transport */ + pj_str_t local_address; + /*! \brief Local port for transport */ + int local_port; +}; + +/*! \brief Callback function for finding the transport the request is going out on */ +static int find_transport_in_use(void *obj, void *arg, int flags) +{ + struct ast_sip_transport *transport = obj; + struct request_transport_details *details = arg; + + /* If an explicit transport or factory matches then this is what is in use, if we are unavailable + * to compare based on that we make sure that the type is the same and the source IP address/port are the same + */ + if ((details->transport && details->transport == transport->state->transport) || + (details->factory && details->factory == transport->state->factory) || + ((details->type == transport->type) && (transport->state->factory) && + !pj_strcmp(&transport->state->factory->addr_name.host, &details->local_address) && + transport->state->factory->addr_name.port == details->local_port)) { + return CMP_MATCH | CMP_STOP; + } + + return 0; +} + +/*! \brief Helper function which returns the SIP URI of a Contact header */ +static pjsip_sip_uri *nat_get_contact_sip_uri(pjsip_tx_data *tdata) +{ + pjsip_contact_hdr *contact = pjsip_msg_find_hdr(tdata->msg, PJSIP_H_CONTACT, NULL); + + if (!contact || (!PJSIP_URI_SCHEME_IS_SIP(contact->uri) && !PJSIP_URI_SCHEME_IS_SIPS(contact->uri))) { + return NULL; + } + + return pjsip_uri_get_uri(contact->uri); +} + +/*! \brief Structure which contains hook details */ +struct nat_hook_details { + /*! \brief Outgoing message itself */ + pjsip_tx_data *tdata; + /*! \brief Chosen transport */ + struct ast_sip_transport *transport; +}; + +/*! \brief Callback function for invoking hooks */ +static int nat_invoke_hook(void *obj, void *arg, int flags) +{ + struct ast_sip_nat_hook *hook = obj; + struct nat_hook_details *details = arg; + + if (hook->outgoing_external_message) { + hook->outgoing_external_message(details->tdata, details->transport); + } + + return 0; +} + +static pj_status_t nat_on_tx_message(pjsip_tx_data *tdata) +{ + RAII_VAR(struct ao2_container *, transports, NULL, ao2_cleanup); + RAII_VAR(struct ast_sip_transport *, transport, NULL, ao2_cleanup); + struct request_transport_details details = { 0, }; + pjsip_via_hdr *via = NULL; + struct ast_sockaddr addr = { { 0, } }; + pjsip_sip_uri *uri = NULL; + RAII_VAR(struct ao2_container *, hooks, NULL, ao2_cleanup); + + /* If a transport selector is in use we know the transport or factory, so explicitly find it */ + if (tdata->tp_sel.type == PJSIP_TPSELECTOR_TRANSPORT) { + details.transport = tdata->tp_sel.u.transport; + } else if (tdata->tp_sel.type == PJSIP_TPSELECTOR_LISTENER) { + details.factory = tdata->tp_sel.u.listener; + } else if (tdata->tp_info.transport->key.type == PJSIP_TRANSPORT_UDP || tdata->tp_info.transport->key.type == PJSIP_TRANSPORT_UDP6) { + /* Connectionless uses the same transport for all requests */ + details.type = AST_SIP_TRANSPORT_UDP; + details.transport = tdata->tp_info.transport; + } else { + if (tdata->tp_info.transport->key.type == PJSIP_TRANSPORT_TCP) { + details.type = AST_SIP_TRANSPORT_TCP; + } else if (tdata->tp_info.transport->key.type == PJSIP_TRANSPORT_TLS) { + details.type = AST_SIP_TRANSPORT_TLS; + } else { + /* Unknown transport type, we can't map and thus can't apply NAT changes */ + return PJ_SUCCESS; + } + + if ((uri = nat_get_contact_sip_uri(tdata))) { + details.local_address = uri->host; + details.local_port = uri->port; + } else if ((tdata->msg->type == PJSIP_REQUEST_MSG) && + (via = pjsip_msg_find_hdr(tdata->msg, PJSIP_H_VIA, NULL))) { + details.local_address = via->sent_by.host; + details.local_port = via->sent_by.port; + } else { + return PJ_SUCCESS; + } + + if (!details.local_port) { + details.local_port = (details.type == AST_SIP_TRANSPORT_TLS) ? 5061 : 5060; + } + } + + if (!(transports = ast_sorcery_retrieve_by_fields(ast_sip_get_sorcery(), "transport", AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL)) || + !(transport = ao2_callback(transports, 0, find_transport_in_use, &details)) || !transport->localnet || + ast_sockaddr_isnull(&transport->external_address)) { + return PJ_SUCCESS; + } + + ast_sockaddr_parse(&addr, tdata->tp_info.dst_name, PARSE_PORT_FORBID); + ast_sockaddr_set_port(&addr, tdata->tp_info.dst_port); + + /* See if where we are sending this request is local or not, and if not that we can get a Contact URI to modify */ + if (ast_apply_ha(transport->localnet, &addr) != AST_SENSE_ALLOW) { + return PJ_SUCCESS; + } + + /* Update the contact header with the external address */ + if (uri || (uri = nat_get_contact_sip_uri(tdata))) { + pj_strdup2(tdata->pool, &uri->host, ast_sockaddr_stringify_host(&transport->external_address)); + if (transport->external_signaling_port) { + uri->port = transport->external_signaling_port; + } + } + + /* Update the via header if relevant */ + if ((tdata->msg->type == PJSIP_REQUEST_MSG) && (via || (via = pjsip_msg_find_hdr(tdata->msg, PJSIP_H_VIA, NULL)))) { + pj_strdup2(tdata->pool, &via->sent_by.host, ast_sockaddr_stringify_host(&transport->external_address)); + if (transport->external_signaling_port) { + via->sent_by.port = transport->external_signaling_port; + } + } + + /* Invoke any additional hooks that may be registered */ + if ((hooks = ast_sorcery_retrieve_by_fields(ast_sip_get_sorcery(), "nat_hook", AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL))) { + struct nat_hook_details hook_details = { + .tdata = tdata, + .transport = transport, + }; + ao2_callback(hooks, 0, nat_invoke_hook, &hook_details); + } + + return PJ_SUCCESS; +} + +static pjsip_module nat_module = { + .name = { "NAT", 3 }, + .id = -1, + .priority = PJSIP_MOD_PRIORITY_TSX_LAYER - 2, + .on_rx_request = nat_on_rx_request, + .on_tx_request = nat_on_tx_message, + .on_tx_response = nat_on_tx_message, +}; + +static int load_module(void) +{ + ast_sip_register_service(&nat_module); + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + ast_sip_unregister_service(&nat_module); + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "SIP NAT Support", + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_APP_DEPEND, + ); diff --git a/res/res_sip_outbound_authenticator_digest.c b/res/res_sip_outbound_authenticator_digest.c new file mode 100644 index 000000000..180c05e27 --- /dev/null +++ b/res/res_sip_outbound_authenticator_digest.c @@ -0,0 +1,110 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Mark Michelson <mmichelson@digium.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 <pjsip.h> + +#include "asterisk/res_sip.h" +#include "asterisk/logger.h" +#include "asterisk/module.h" +#include "asterisk/strings.h" + +static int set_outbound_authentication_credentials(pjsip_auth_clt_sess *auth_sess, const char **auth_strs, size_t num_auths) +{ + struct ast_sip_auth **auths = ast_alloca(num_auths * sizeof(*auths)); + pjsip_cred_info *auth_creds = ast_alloca(num_auths * sizeof(*auth_creds)); + int res = 0; + int i; + + if (ast_sip_retrieve_auths(auth_strs, num_auths, auths)) { + res = -1; + goto cleanup; + } + + for (i = 0; i < num_auths; ++i) { + pj_cstr(&auth_creds[i].realm, auths[i]->realm); + pj_cstr(&auth_creds[i].username, auths[i]->auth_user); + pj_cstr(&auth_creds[i].scheme, "digest"); + switch (auths[i]->type) { + case AST_SIP_AUTH_TYPE_USER_PASS: + pj_cstr(&auth_creds[i].data, auths[i]->auth_pass); + auth_creds[i].data_type = PJSIP_CRED_DATA_PLAIN_PASSWD; + break; + case AST_SIP_AUTH_TYPE_MD5: + pj_cstr(&auth_creds[i].data, auths[i]->md5_creds); + auth_creds[i].data_type = PJSIP_CRED_DATA_DIGEST; + break; + } + } + + pjsip_auth_clt_set_credentials(auth_sess, num_auths, auth_creds); + +cleanup: + ast_sip_cleanup_auths(auths, num_auths); + return res; +} + +static int digest_create_request_with_auth(const char **auths, size_t num_auths, pjsip_rx_data *challenge, + pjsip_transaction *tsx, pjsip_tx_data **new_request) +{ + pjsip_auth_clt_sess auth_sess; + + if (pjsip_auth_clt_init(&auth_sess, ast_sip_get_pjsip_endpoint(), + tsx->pool, 0) != PJ_SUCCESS) { + ast_log(LOG_WARNING, "Failed to initialize client authentication session\n"); + return -1; + } + + if (set_outbound_authentication_credentials(&auth_sess, auths, num_auths)) { + ast_log(LOG_WARNING, "Failed to set authentication credentials\n"); + return -1; + } + + if (pjsip_auth_clt_reinit_req(&auth_sess, challenge, + tsx->last_tx, new_request) != PJ_SUCCESS) { + ast_log(LOG_WARNING, "Failed to create new request with authentication credentials\n"); + return -1; + } + + return 0; +} + +static struct ast_sip_outbound_authenticator digest_authenticator = { + .create_request_with_auth = digest_create_request_with_auth, +}; + +static int load_module(void) +{ + if (ast_sip_register_outbound_authenticator(&digest_authenticator)) { + return AST_MODULE_LOAD_DECLINE; + } + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + ast_sip_unregister_outbound_authenticator(&digest_authenticator); + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "SIP authentication resource", + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_CHANNEL_DEPEND, +); diff --git a/res/res_sip_outbound_registration.c b/res/res_sip_outbound_registration.c new file mode 100644 index 000000000..8f1108df5 --- /dev/null +++ b/res/res_sip_outbound_registration.c @@ -0,0 +1,708 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Joshua Colp <jcolp@digium.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 + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include <pjsip.h> +#include <pjsip_ua.h> + +#include "asterisk/res_sip.h" +#include "asterisk/module.h" +#include "asterisk/taskprocessor.h" + +/*! \brief Amount of buffer time (in seconds) before expiration that we re-register at */ +#define REREGISTER_BUFFER_TIME 10 + +/*! \brief Various states that an outbound registration may be in */ +enum sip_outbound_registration_status { + /*! \brief Currently unregistered */ + SIP_REGISTRATION_UNREGISTERED = 0, + /*! \brief Registered, yay! */ + SIP_REGISTRATION_REGISTERED, + /*! \brief Registration was rejected, but response was temporal */ + SIP_REGISTRATION_REJECTED_TEMPORARY, + /*! \brief Registration was rejected, permanently */ + SIP_REGISTRATION_REJECTED_PERMANENT, + /*! \brief Registration has been stopped */ + SIP_REGISTRATION_STOPPED, +}; + +/*! \brief Outbound registration client state information (persists for lifetime of regc) */ +struct sip_outbound_registration_client_state { + /*! \brief Current status of this registration */ + enum sip_outbound_registration_status status; + /*! \brief Outbound registration client */ + pjsip_regc *client; + /*! \brief Timer entry for retrying on temporal responses */ + pj_timer_entry timer; + /*! \brief Current number of retries */ + unsigned int retries; + /*! \brief Maximum number of retries permitted */ + unsigned int max_retries; + /*! \brief Interval at which retries should occur for temporal responses */ + unsigned int retry_interval; + /*! \brief Treat authentication challenges that we cannot handle as permanent failures */ + unsigned int auth_rejection_permanent; + /*! \brief Serializer for stuff and things */ + struct ast_taskprocessor *serializer; + /*! \brief Configured authentication credentials */ + const char **sip_outbound_auths; + /*! \brief Number of configured auths */ + size_t num_outbound_auths; + /*! \brief Registration should be destroyed after completion of transaction */ + unsigned int destroy:1; +}; + +/*! \brief Outbound registration state information (persists for lifetime that registration should exist) */ +struct sip_outbound_registration_state { + /*! \brief Client state information */ + struct sip_outbound_registration_client_state *client_state; +}; + +/*! \brief Outbound registration information */ +struct sip_outbound_registration { + /*! \brief Sorcery object details */ + SORCERY_OBJECT(details); + /*! \brief Stringfields */ + AST_DECLARE_STRING_FIELDS( + /*! \brief URI for the registrar */ + AST_STRING_FIELD(server_uri); + /*! \brief URI for the AOR */ + AST_STRING_FIELD(client_uri); + /*! \brief Optional user for contact header */ + AST_STRING_FIELD(contact_user); + /*! \brief Explicit transport to use for registration */ + AST_STRING_FIELD(transport); + /*! \brief Outbound proxy to use */ + AST_STRING_FIELD(outbound_proxy); + ); + /*! \brief Requested expiration time */ + unsigned int expiration; + /*! \brief Interval at which retries should occur for temporal responses */ + unsigned int retry_interval; + /*! \brief Treat authentication challenges that we cannot handle as permanent failures */ + unsigned int auth_rejection_permanent; + /*! \brief Maximum number of retries permitted */ + unsigned int max_retries; + /*! \brief Outbound registration state */ + struct sip_outbound_registration_state *state; + /*! \brief Configured authentication credentials */ + const char **sip_outbound_auths; + /*! \brief Number of configured auths */ + size_t num_outbound_auths; +}; + +static void destroy_auths(const char **auths, size_t num_auths) +{ + int i; + for (i = 0; i < num_auths; ++i) { + ast_free((char *) auths[i]); + } + ast_free(auths); +} + +/*! \brief Helper function which cancels the timer on a client */ +static void cancel_registration(struct sip_outbound_registration_client_state *client_state) +{ + if (pj_timer_heap_cancel(pjsip_endpt_get_timer_heap(ast_sip_get_pjsip_endpoint()), &client_state->timer)) { + /* The timer was successfully cancelled, drop the refcount of client_state */ + ao2_ref(client_state, -1); + } +} + +/*! \brief Callback function for registering */ +static int handle_client_registration(void *data) +{ + RAII_VAR(struct sip_outbound_registration_client_state *, client_state, data, ao2_cleanup); + pjsip_tx_data *tdata; + + cancel_registration(client_state); + + if ((client_state->status == SIP_REGISTRATION_STOPPED) || + (pjsip_regc_register(client_state->client, PJ_FALSE, &tdata) != PJ_SUCCESS)) { + return 0; + } + + /* Due to the registration the callback may now get called, so bump the ref count */ + ao2_ref(client_state, +1); + if (pjsip_regc_send(client_state->client, tdata) != PJ_SUCCESS) { + ao2_ref(client_state, -1); + pjsip_tx_data_dec_ref(tdata); + } + + return 0; +} + +/*! \brief Timer callback function, used just for registrations */ +static void sip_outbound_registration_timer_cb(pj_timer_heap_t *timer_heap, struct pj_timer_entry *entry) +{ + RAII_VAR(struct sip_outbound_registration_client_state *, client_state, entry->user_data, ao2_cleanup); + + ao2_ref(client_state, +1); + if (ast_sip_push_task(client_state->serializer, handle_client_registration, client_state)) { + ast_log(LOG_WARNING, "Failed to pass outbound registration to threadpool\n"); + ao2_ref(client_state, -1); + } + + entry->id = 0; +} + +/*! \brief Helper function which sets up the timer to re-register in a specific amount of time */ +static void schedule_registration(struct sip_outbound_registration_client_state *client_state, unsigned int seconds) +{ + pj_time_val delay = { .sec = seconds, }; + + cancel_registration(client_state); + + ao2_ref(client_state, +1); + if (pjsip_endpt_schedule_timer(ast_sip_get_pjsip_endpoint(), &client_state->timer, &delay) != PJ_SUCCESS) { + ast_log(LOG_WARNING, "Failed to pass timed registration to scheduler\n"); + ao2_ref(client_state, -1); + } +} + +/*! \brief Callback function for unregistering (potentially) and destroying state */ +static int handle_client_state_destruction(void *data) +{ + RAII_VAR(struct sip_outbound_registration_client_state *, client_state, data, ao2_cleanup); + pjsip_regc_info info; + + cancel_registration(client_state); + + pjsip_regc_get_info(client_state->client, &info); + + if (info.is_busy == PJ_TRUE) { + /* If a client transaction is in progress we defer until it is complete */ + client_state->destroy = 1; + return 0; + } + + if (client_state->status != SIP_REGISTRATION_UNREGISTERED && client_state->status != SIP_REGISTRATION_REJECTED_PERMANENT) { + pjsip_tx_data *tdata; + + if (pjsip_regc_unregister(client_state->client, &tdata) == PJ_SUCCESS) { + pjsip_regc_send(client_state->client, tdata); + } + } + + pjsip_regc_destroy(client_state->client); + + client_state->status = SIP_REGISTRATION_STOPPED; + destroy_auths(client_state->sip_outbound_auths, client_state->num_outbound_auths); + + return 0; +} + +/*! \brief Structure for registration response */ +struct registration_response { + /*! \brief Response code for the registration attempt */ + int code; + /*! \brief Expiration time for registration */ + int expiration; + /*! \brief Retry-After value */ + int retry_after; + /*! \brief Outbound registration client state */ + struct sip_outbound_registration_client_state *client_state; + /*! \brief The response message */ + pjsip_rx_data *rdata; + /*! \brief The response transaction */ + pjsip_transaction *tsx; +}; + +/*! \brief Registration response structure destructor */ +static void registration_response_destroy(void *obj) +{ + struct registration_response *response = obj; + + pjsip_rx_data_free_cloned(response->rdata); + ao2_cleanup(response->client_state); +} + +/* \brief Helper funtion which determines if a response code is temporal or not */ +static int sip_outbound_registration_is_temporal(unsigned int code, + struct sip_outbound_registration_client_state *client_state) +{ + /* Shamelessly taken from pjsua */ + if (code == PJSIP_SC_REQUEST_TIMEOUT || + code == PJSIP_SC_INTERNAL_SERVER_ERROR || + code == PJSIP_SC_BAD_GATEWAY || + code == PJSIP_SC_SERVICE_UNAVAILABLE || + code == PJSIP_SC_SERVER_TIMEOUT || + ((code == PJSIP_SC_UNAUTHORIZED || + code == PJSIP_SC_PROXY_AUTHENTICATION_REQUIRED) && + !client_state->auth_rejection_permanent) || + PJSIP_IS_STATUS_IN_CLASS(code, 600)) { + return 1; + } else { + return 0; + } +} + +/*! \brief Callback function for handling a response to a registration attempt */ +static int handle_registration_response(void *data) +{ + RAII_VAR(struct registration_response *, response, data, ao2_cleanup); + pjsip_regc_info info; + char server_uri[PJSIP_MAX_URL_SIZE], client_uri[PJSIP_MAX_URL_SIZE]; + + if (response->client_state->status == SIP_REGISTRATION_STOPPED) { + return 0; + } + + pjsip_regc_get_info(response->client_state->client, &info); + ast_copy_pj_str(server_uri, &info.server_uri, sizeof(server_uri)); + ast_copy_pj_str(client_uri, &info.client_uri, sizeof(client_uri)); + + if (response->code == 401 || response->code == 407) { + pjsip_tx_data *tdata; + if (!ast_sip_create_request_with_auth(response->client_state->sip_outbound_auths, response->client_state->num_outbound_auths, + response->rdata, response->tsx, &tdata)) { + pjsip_regc_send(response->client_state->client, tdata); + return 0; + } + /* Otherwise, fall through so the failure is processed appropriately */ + } + + if (PJSIP_IS_STATUS_IN_CLASS(response->code, 200)) { + /* If the registration went fine simply reschedule registration for the future */ + response->client_state->status = SIP_REGISTRATION_REGISTERED; + response->client_state->retries = 0; + schedule_registration(response->client_state, response->expiration - REREGISTER_BUFFER_TIME); + } else if (response->retry_after) { + /* If we have been instructed to retry after a period of time, schedule it as such */ + response->client_state->status = SIP_REGISTRATION_REJECTED_TEMPORARY; + schedule_registration(response->client_state, response->retry_after); + ast_log(LOG_WARNING, "Temporal response '%d' received from '%s' on registration attempt to '%s', instructed to retry in '%d'\n", + response->code, server_uri, client_uri, response->retry_after); + } else if (response->client_state->retry_interval && sip_outbound_registration_is_temporal(response->code, response->client_state)) { + if (response->client_state->retries == response->client_state->max_retries) { + /* If we received enough temporal responses to exceed our maximum give up permanently */ + response->client_state->status = SIP_REGISTRATION_REJECTED_PERMANENT; + ast_log(LOG_WARNING, "Maximum retries reached when attempting outbound registration to '%s' with client '%s', stopping registration attempt\n", + server_uri, client_uri); + } else { + /* On the other hand if we can still try some more do so */ + response->client_state->status = SIP_REGISTRATION_REJECTED_TEMPORARY; + response->client_state->retries++; + schedule_registration(response->client_state, response->client_state->retry_interval); + ast_log(LOG_WARNING, "Temporal response '%d' received from '%s' on registration attempt to '%s', retrying in '%d' seconds\n", + response->code, server_uri, client_uri, response->client_state->retry_interval); + } + } else { + /* Finally if there's no hope of registering give up */ + response->client_state->status = SIP_REGISTRATION_REJECTED_PERMANENT; + ast_log(LOG_WARNING, "Fatal response '%d' received from '%s' on registration attempt to '%s', stopping outbound registration\n", + response->code, server_uri, client_uri); + } + + /* If deferred destruction is in use see if we need to destroy now */ + if (response->client_state->destroy) { + handle_client_state_destruction(response->client_state); + } + + return 0; +} + +/*! \brief Callback function for outbound registration client */ +static void sip_outbound_registration_response_cb(struct pjsip_regc_cbparam *param) +{ + RAII_VAR(struct sip_outbound_registration_client_state *, client_state, param->token, ao2_cleanup); + struct registration_response *response = ao2_alloc(sizeof(*response), registration_response_destroy); + struct pjsip_retry_after_hdr *retry_after = pjsip_msg_find_hdr(param->rdata->msg_info.msg, PJSIP_H_RETRY_AFTER, NULL); + + response->code = param->code; + response->expiration = param->expiration; + response->retry_after = retry_after ? retry_after->ivalue : 0; + response->client_state = client_state; + response->tsx = pjsip_rdata_get_tsx(param->rdata); + pjsip_rx_data_clone(param->rdata, 0, &response->rdata); + ao2_ref(response->client_state, +1); + + if (ast_sip_push_task(client_state->serializer, handle_registration_response, response)) { + ast_log(LOG_WARNING, "Failed to pass incoming registration response to threadpool\n"); + ao2_cleanup(response); + } +} + +/*! \brief Destructor function for registration state */ +static void sip_outbound_registration_state_destroy(void *obj) +{ + struct sip_outbound_registration_state *state = obj; + + if (!state->client_state) { + return; + } + + if (state->client_state->serializer && ast_sip_push_task(state->client_state->serializer, handle_client_state_destruction, state->client_state)) { + ast_log(LOG_WARNING, "Failed to pass outbound registration client destruction to threadpool\n"); + ao2_ref(state->client_state, -1); + } +} + +/*! \brief Destructor function for client registration state */ +static void sip_outbound_registration_client_state_destroy(void *obj) +{ + struct sip_outbound_registration_client_state *client_state = obj; + + ast_taskprocessor_unreference(client_state->serializer); +} + +/*! \brief Allocator function for registration state */ +static struct sip_outbound_registration_state *sip_outbound_registration_state_alloc(void) +{ + struct sip_outbound_registration_state *state = ao2_alloc(sizeof(*state), sip_outbound_registration_state_destroy); + + if (!state || !(state->client_state = ao2_alloc(sizeof(*state->client_state), sip_outbound_registration_client_state_destroy))) { + ao2_cleanup(state); + return NULL; + } + + if ((pjsip_regc_create(ast_sip_get_pjsip_endpoint(), state->client_state, sip_outbound_registration_response_cb, &state->client_state->client) != PJ_SUCCESS) || + !(state->client_state->serializer = ast_sip_create_serializer())) { + /* This is on purpose, normal operation will have it be deallocated within the serializer */ + pjsip_regc_destroy(state->client_state->client); + ao2_cleanup(state->client_state); + ao2_cleanup(state); + return NULL; + } + + state->client_state->status = SIP_REGISTRATION_UNREGISTERED; + state->client_state->timer.user_data = state->client_state; + state->client_state->timer.cb = sip_outbound_registration_timer_cb; + + return state; +} + +/*! \brief Destructor function for registration information */ +static void sip_outbound_registration_destroy(void *obj) +{ + struct sip_outbound_registration *registration = obj; + + ao2_cleanup(registration->state); + destroy_auths(registration->sip_outbound_auths, registration->num_outbound_auths); + + ast_string_field_free_memory(registration); +} + +/*! \brief Allocator function for registration information */ +static void *sip_outbound_registration_alloc(const char *name) +{ + struct sip_outbound_registration *registration = ao2_alloc(sizeof(*registration), sip_outbound_registration_destroy); + + if (!registration || ast_string_field_init(registration, 256)) { + ao2_cleanup(registration); + return NULL; + } + + return registration; +} + +/*! \brief Helper function which populates a pj_str_t with a contact header */ +static int sip_dialog_create_contact(pj_pool_t *pool, pj_str_t *contact, const char *user, const pj_str_t *target, pjsip_tpselector *selector) +{ + pj_str_t tmp, local_addr; + pjsip_uri *uri; + pjsip_sip_uri *sip_uri; + pjsip_transport_type_e type = PJSIP_TRANSPORT_UNSPECIFIED; + int local_port; + + pj_strdup_with_null(pool, &tmp, target); + + if (!(uri = pjsip_parse_uri(pool, tmp.ptr, tmp.slen, 0)) || + (!PJSIP_URI_SCHEME_IS_SIP(uri) && !PJSIP_URI_SCHEME_IS_SIPS(uri))) { + return -1; + } + + sip_uri = pjsip_uri_get_uri(uri); + + if (PJSIP_URI_SCHEME_IS_SIPS(sip_uri)) { + type = PJSIP_TRANSPORT_TLS; + } else if (!sip_uri->transport_param.slen) { + type = PJSIP_TRANSPORT_UDP; + } else { + type = pjsip_transport_get_type_from_name(&sip_uri->transport_param); + } + + if (type == PJSIP_TRANSPORT_UNSPECIFIED) { + return -1; + } + + if (pj_strchr(&sip_uri->host, ':')) { + type = (pjsip_transport_type_e)(((int)type) + PJSIP_TRANSPORT_IPV6); + } + + if (pjsip_tpmgr_find_local_addr(pjsip_endpt_get_tpmgr(ast_sip_get_pjsip_endpoint()), pool, type, selector, + &local_addr, &local_port) != PJ_SUCCESS) { + return -1; + } + + if (!pj_strchr(&sip_uri->host, ':') && pj_strchr(&local_addr, ':')) { + type = (pjsip_transport_type_e)(((int)type) + PJSIP_TRANSPORT_IPV6); + } + + contact->ptr = pj_pool_alloc(pool, PJSIP_MAX_URL_SIZE); + contact->slen = pj_ansi_snprintf(contact->ptr, PJSIP_MAX_URL_SIZE, + "<%s:%s@%s%.*s%s:%d%s%s>", + (pjsip_transport_get_flag_from_type(type) & PJSIP_TRANSPORT_SECURE) ? "sips" : "sip", + user, + (type & PJSIP_TRANSPORT_IPV6) ? "[" : "", + (int)local_addr.slen, + local_addr.ptr, + (type & PJSIP_TRANSPORT_IPV6) ? "]" : "", + local_port, + (type != PJSIP_TRANSPORT_UDP && type != PJSIP_TRANSPORT_UDP6) ? ";transport=" : "", + (type != PJSIP_TRANSPORT_UDP && type != PJSIP_TRANSPORT_UDP6) ? pjsip_transport_get_type_name(type) : ""); + + return 0; +} + +/*! + * \internal + * \brief Check if a registration can be reused + * + * This checks if the existing outbound registration's configuration differs from a newly-applied + * outbound registration to see if the applied one. + * + * \param existing The pre-existing outbound registration + * \param applied The newly-created registration + */ +static int can_reuse_registration(struct sip_outbound_registration *existing, struct sip_outbound_registration *applied) +{ + int i; + + if (strcmp(existing->server_uri, applied->server_uri) || strcmp(existing->client_uri, applied->client_uri) || + strcmp(existing->transport, applied->transport) || strcmp(existing->contact_user, applied->contact_user) || + strcmp(existing->outbound_proxy, applied->outbound_proxy) || existing->num_outbound_auths != applied->num_outbound_auths || + existing->auth_rejection_permanent != applied->auth_rejection_permanent) { + return 0; + } + + for (i = 0; i < existing->num_outbound_auths; ++i) { + if (strcmp(existing->sip_outbound_auths[i], applied->sip_outbound_auths[i])) { + return 0; + } + } + + return 1; +} + +/*! \brief Apply function which finds or allocates a state structure */ +static int sip_outbound_registration_apply(const struct ast_sorcery *sorcery, void *obj) +{ + RAII_VAR(struct sip_outbound_registration *, existing, ast_sorcery_retrieve_by_id(sorcery, "registration", ast_sorcery_object_get_id(obj)), ao2_cleanup); + struct sip_outbound_registration *applied = obj; + pj_str_t server_uri, client_uri, contact_uri; + pjsip_tpselector selector = { .type = PJSIP_TPSELECTOR_NONE, }; + + if (!existing) { + /* If no existing registration exists we can just start fresh easily */ + applied->state = sip_outbound_registration_state_alloc(); + } else { + /* If there is an existing registration things are more complicated, we can immediately reuse this state if most stuff remains unchanged */ + if (can_reuse_registration(existing, applied)) { + applied->state = existing->state; + ao2_ref(applied->state, +1); + return 0; + } + applied->state = sip_outbound_registration_state_alloc(); + } + + if (!applied->state) { + return -1; + } + + if (!ast_strlen_zero(applied->transport)) { + RAII_VAR(struct ast_sip_transport *, transport, ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "transport", applied->transport), ao2_cleanup); + + if (!transport || !transport->state) { + return -1; + } + + if (transport->type == AST_SIP_TRANSPORT_UDP) { + selector.type = PJSIP_TPSELECTOR_TRANSPORT; + selector.u.transport = transport->state->transport; + } else if (transport->type == AST_SIP_TRANSPORT_TCP || transport->type == AST_SIP_TRANSPORT_TLS) { + selector.type = PJSIP_TPSELECTOR_LISTENER; + selector.u.listener = transport->state->factory; + } else { + return -1; + } + } + + pjsip_regc_set_transport(applied->state->client_state->client, &selector); + + if (!ast_strlen_zero(applied->outbound_proxy)) { + pjsip_route_hdr route_set, *route; + static const pj_str_t ROUTE_HNAME = { "Route", 5 }; + pj_str_t tmp; + + pj_list_init(&route_set); + + pj_strdup2_with_null(pjsip_regc_get_pool(applied->state->client_state->client), &tmp, applied->outbound_proxy); + if (!(route = pjsip_parse_hdr(pjsip_regc_get_pool(applied->state->client_state->client), &ROUTE_HNAME, tmp.ptr, tmp.slen, NULL))) { + return -1; + } + pj_list_push_back(&route_set, route); + + pjsip_regc_set_route_set(applied->state->client_state->client, &route_set); + } + + pj_cstr(&server_uri, applied->server_uri); + + if (sip_dialog_create_contact(pjsip_regc_get_pool(applied->state->client_state->client), &contact_uri, S_OR(applied->contact_user, "s"), &server_uri, &selector)) { + return -1; + } + + pj_cstr(&client_uri, applied->client_uri); + + if (pjsip_regc_init(applied->state->client_state->client, &server_uri, &client_uri, &client_uri, 1, &contact_uri, applied->expiration) != PJ_SUCCESS) { + return -1; + } + + return 0; +} + +/*! \brief Helper function which performs a single registration */ +static int sip_outbound_registration_perform(void *obj, void *arg, int flags) +{ + struct sip_outbound_registration *registration = obj; + size_t i; + + /* Just in case the client state is being reused for this registration, free the auth information */ + destroy_auths(registration->state->client_state->sip_outbound_auths, + registration->state->client_state->num_outbound_auths); + registration->state->client_state->num_outbound_auths = 0; + + registration->state->client_state->sip_outbound_auths = ast_calloc(registration->num_outbound_auths, sizeof(char *)); + for (i = 0; i < registration->num_outbound_auths; ++i) { + registration->state->client_state->sip_outbound_auths[i] = ast_strdup(registration->sip_outbound_auths[i]); + } + registration->state->client_state->num_outbound_auths = registration->num_outbound_auths; + registration->state->client_state->retry_interval = registration->retry_interval; + registration->state->client_state->max_retries = registration->max_retries; + registration->state->client_state->retries = 0; + + pjsip_regc_update_expires(registration->state->client_state->client, registration->expiration); + + schedule_registration(registration->state->client_state, (ast_random() % 10) + 1); + + return 0; +} + +/*! \brief Helper function which performs all registrations */ +static void sip_outbound_registration_perform_all(void) +{ + RAII_VAR(struct ao2_container *, registrations, ast_sorcery_retrieve_by_fields(ast_sip_get_sorcery(), "registration", AST_RETRIEVE_FLAG_MULTIPLE | AST_RETRIEVE_FLAG_ALL, NULL), ao2_cleanup); + + if (!registrations) { + return; + } + + ao2_callback(registrations, OBJ_NODATA, sip_outbound_registration_perform, NULL); +} + +#define AUTH_INCREMENT 4 + +static const char **auth_alloc(const char *value, size_t *num_auths) +{ + char *auths = ast_strdupa(value); + char *val; + int num_alloced = 0; + const char **alloced_auths = NULL; + + while ((val = strsep(&auths, ","))) { + if (*num_auths >= num_alloced) { + size_t size; + num_alloced += AUTH_INCREMENT; + size = num_alloced * sizeof(char *); + alloced_auths = ast_realloc(alloced_auths, size); + if (!alloced_auths) { + goto failure; + } + } + alloced_auths[*num_auths] = ast_strdup(val); + if (!alloced_auths[*num_auths]) { + goto failure; + } + ++(*num_auths); + } + return alloced_auths; + +failure: + destroy_auths(alloced_auths, *num_auths); + return NULL; +} + +static int outbound_auth_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct sip_outbound_registration *registration = obj; + + registration->sip_outbound_auths = auth_alloc(var->value, ®istration->num_outbound_auths); + if (!registration->sip_outbound_auths) { + return -1; + } + return 0; +} + +static int load_module(void) +{ + ast_sorcery_apply_default(ast_sip_get_sorcery(), "registration", "config", "res_sip.conf,criteria=type=registration"); + + if (ast_sorcery_object_register(ast_sip_get_sorcery(), "registration", sip_outbound_registration_alloc, NULL, sip_outbound_registration_apply)) { + return AST_MODULE_LOAD_DECLINE; + } + + ast_sorcery_object_field_register(ast_sip_get_sorcery(), "registration", "type", "", OPT_NOOP_T, 0, 0); + ast_sorcery_object_field_register(ast_sip_get_sorcery(), "registration", "server_uri", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct sip_outbound_registration, server_uri)); + ast_sorcery_object_field_register(ast_sip_get_sorcery(), "registration", "client_uri", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct sip_outbound_registration, client_uri)); + ast_sorcery_object_field_register(ast_sip_get_sorcery(), "registration", "contact_user", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct sip_outbound_registration, contact_user)); + ast_sorcery_object_field_register(ast_sip_get_sorcery(), "registration", "transport", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct sip_outbound_registration, transport)); + ast_sorcery_object_field_register(ast_sip_get_sorcery(), "registration", "outbound_proxy", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct sip_outbound_registration, outbound_proxy)); + ast_sorcery_object_field_register(ast_sip_get_sorcery(), "registration", "expiration", "3600", OPT_UINT_T, 0, FLDSET(struct sip_outbound_registration, expiration)); + ast_sorcery_object_field_register(ast_sip_get_sorcery(), "registration", "retry_interval", "60", OPT_UINT_T, 0, FLDSET(struct sip_outbound_registration, retry_interval)); + ast_sorcery_object_field_register(ast_sip_get_sorcery(), "registration", "max_retries", "10", OPT_UINT_T, 0, FLDSET(struct sip_outbound_registration, max_retries)); + ast_sorcery_object_field_register(ast_sip_get_sorcery(), "registration", "auth_rejection_permanent", "yes", OPT_BOOL_T, 1, FLDSET(struct sip_outbound_registration, auth_rejection_permanent)); + ast_sorcery_object_field_register_custom(ast_sip_get_sorcery(), "registration", "outbound_auth", "", outbound_auth_handler, NULL, 0, 0); + ast_sorcery_reload_object(ast_sip_get_sorcery(), "registration"); + sip_outbound_registration_perform_all(); + + return AST_MODULE_LOAD_SUCCESS; +} + +static int reload_module(void) +{ + ast_sorcery_reload_object(ast_sip_get_sorcery(), "registration"); + sip_outbound_registration_perform_all(); + return 0; +} + +static int unload_module(void) +{ + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "SIP Outbound Registration Support", + .load = load_module, + .reload = reload_module, + .unload = unload_module, + .load_pri = AST_MODPRI_APP_DEPEND, + ); diff --git a/res/res_sip_pubsub.c b/res/res_sip_pubsub.c new file mode 100644 index 000000000..2983d563e --- /dev/null +++ b/res/res_sip_pubsub.c @@ -0,0 +1,662 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Mark Michelson <mmichelson@digium.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. + */ +/*! + * \brief Opaque structure representing an RFC 3265 SIP subscription + */ + +#include "asterisk.h" + +#include <pjsip.h> +#include <pjsip_simple.h> +#include <pjlib.h> + +#include "asterisk/res_sip_pubsub.h" +#include "asterisk/module.h" +#include "asterisk/linkedlists.h" +#include "asterisk/astobj2.h" +#include "asterisk/datastore.h" +#include "asterisk/uuid.h" +#include "asterisk/taskprocessor.h" +#include "asterisk/res_sip.h" + +static pj_bool_t sub_on_rx_request(pjsip_rx_data *rdata); + +static struct pjsip_module sub_module = { + .name = { "PubSub Module", 13 }, + .priority = PJSIP_MOD_PRIORITY_APPLICATION, + .on_rx_request = sub_on_rx_request, +}; + +/*! + * \brief Structure representing a SIP subscription + */ +struct ast_sip_subscription { + /*! Subscription datastores set up by handlers */ + struct ao2_container *datastores; + /*! The endpoint with which the subscription is communicating */ + struct ast_sip_endpoint *endpoint; + /*! Serializer on which to place operations for this subscription */ + struct ast_taskprocessor *serializer; + /*! The handler for this subscription */ + const struct ast_sip_subscription_handler *handler; + /*! The role for this subscription */ + enum ast_sip_subscription_role role; + /*! The underlying PJSIP event subscription structure */ + pjsip_evsub *evsub; + /*! The underlying PJSIP dialog */ + pjsip_dialog *dlg; +}; + +#define DATASTORE_BUCKETS 53 + +#define DEFAULT_EXPIRES 3600 + +static int datastore_hash(const void *obj, int flags) +{ + const struct ast_datastore *datastore = obj; + const char *uid = flags & OBJ_KEY ? obj : datastore->uid; + + ast_assert(uid != NULL); + + return ast_str_hash(uid); +} + +static int datastore_cmp(void *obj, void *arg, int flags) +{ + const struct ast_datastore *datastore1 = obj; + const struct ast_datastore *datastore2 = arg; + const char *uid2 = flags & OBJ_KEY ? arg : datastore2->uid; + + ast_assert(datastore1->uid != NULL); + ast_assert(uid2 != NULL); + + return strcmp(datastore1->uid, uid2) ? 0 : CMP_MATCH | CMP_STOP; +} + +static void subscription_destructor(void *obj) +{ + struct ast_sip_subscription *sub = obj; + + ast_debug(3, "Destroying SIP subscription\n"); + + ao2_cleanup(sub->datastores); + ao2_cleanup(sub->endpoint); + + if (sub->dlg) { + /* This is why we keep the dialog on the subscription. When the subscription + * is destroyed, there is no guarantee that the underlying dialog is ready + * to be destroyed. Furthermore, there's no guarantee in the opposite direction + * either. The dialog could be destroyed before our subscription is. We fix + * this problem by keeping a reference to the dialog until it is time to + * destroy the subscription. We need to have the dialog available when the + * subscription is destroyed so that we can guarantee that our attempt to + * remove the serializer will be successful. + */ + ast_sip_dialog_set_serializer(sub->dlg, NULL); + pjsip_dlg_dec_session(sub->dlg, &sub_module); + } + ast_taskprocessor_unreference(sub->serializer); +} + +static void pubsub_on_evsub_state(pjsip_evsub *sub, pjsip_event *event); +static void pubsub_on_tsx_state(pjsip_evsub *sub, pjsip_transaction *tsx, pjsip_event *event); +static void pubsub_on_rx_refresh(pjsip_evsub *sub, pjsip_rx_data *rdata, + int *p_st_code, pj_str_t **p_st_text, pjsip_hdr *res_hdr, pjsip_msg_body **p_body); +static void pubsub_on_rx_notify(pjsip_evsub *sub, pjsip_rx_data *rdata, int *p_st_code, + pj_str_t **p_st_text, pjsip_hdr *res_hdr, pjsip_msg_body **p_body); +static void pubsub_on_client_refresh(pjsip_evsub *sub); +static void pubsub_on_server_timeout(pjsip_evsub *sub); + + +static pjsip_evsub_user pubsub_cb = { + .on_evsub_state = pubsub_on_evsub_state, + .on_tsx_state = pubsub_on_tsx_state, + .on_rx_refresh = pubsub_on_rx_refresh, + .on_rx_notify = pubsub_on_rx_notify, + .on_client_refresh = pubsub_on_client_refresh, + .on_server_timeout = pubsub_on_server_timeout, +}; + +static pjsip_evsub *allocate_evsub(const char *event, enum ast_sip_subscription_role role, + struct ast_sip_endpoint *endpoint, pjsip_rx_data *rdata, pjsip_dialog *dlg) +{ + pjsip_evsub *evsub; + /* PJSIP is kind enough to have some built-in support for certain + * events. We need to use the correct initialization function for the + * built-in events + */ + if (role == AST_SIP_NOTIFIER) { + if (!strcmp(event, "message-summary")) { + pjsip_mwi_create_uas(dlg, &pubsub_cb, rdata, &evsub); + } else if (!strcmp(event, "presence")) { + pjsip_pres_create_uas(dlg, &pubsub_cb, rdata, &evsub); + } else { + pjsip_evsub_create_uas(dlg, &pubsub_cb, rdata, 0, &evsub); + } + } else { + if (!strcmp(event, "message-summary")) { + pjsip_mwi_create_uac(dlg, &pubsub_cb, 0, &evsub); + } else if (!strcmp(event, "presence")) { + pjsip_pres_create_uac(dlg, &pubsub_cb, 0, &evsub); + } else { + pj_str_t pj_event; + pj_cstr(&pj_event, event); + pjsip_evsub_create_uac(dlg, &pubsub_cb, &pj_event, 0, &evsub); + } + } + return evsub; +} + +struct ast_sip_subscription *ast_sip_create_subscription(const struct ast_sip_subscription_handler *handler, + enum ast_sip_subscription_role role, struct ast_sip_endpoint *endpoint, pjsip_rx_data *rdata) +{ + struct ast_sip_subscription *sub = ao2_alloc(sizeof(*sub), subscription_destructor); + pjsip_dialog *dlg; + + if (!sub) { + return NULL; + } + sub->datastores = ao2_container_alloc(DATASTORE_BUCKETS, datastore_hash, datastore_cmp); + if (!sub->datastores) { + ao2_ref(sub, -1); + return NULL; + } + sub->serializer = ast_sip_create_serializer(); + if (!sub->serializer) { + ao2_ref(sub, -1); + return NULL; + } + sub->role = role; + if (role == AST_SIP_NOTIFIER) { + pjsip_dlg_create_uas(pjsip_ua_instance(), rdata, NULL, &dlg); + } else { + RAII_VAR(struct ast_sip_contact *, contact, NULL, ao2_cleanup); + + contact = ast_sip_location_retrieve_contact_from_aor_list(endpoint->aors); + if (!contact || ast_strlen_zero(contact->uri)) { + ast_log(LOG_WARNING, "No contacts configured for endpoint %s. Unable to create SIP subsription\n", + ast_sorcery_object_get_id(endpoint)); + ao2_ref(sub, -1); + return NULL; + } + dlg = ast_sip_create_dialog(endpoint, contact->uri, NULL); + } + if (!dlg) { + ast_log(LOG_WARNING, "Unable to create dialog for SIP subscription\n"); + ao2_ref(sub, -1); + return NULL; + } + sub->evsub = allocate_evsub(handler->event_name, role, endpoint, rdata, dlg); + /* We keep a reference to the dialog until our subscription is destroyed. See + * the subscription_destructor for more details + */ + pjsip_dlg_inc_session(dlg, &sub_module); + sub->dlg = dlg; + ast_sip_dialog_set_serializer(dlg, sub->serializer); + pjsip_evsub_set_mod_data(sub->evsub, sub_module.id, sub); + ao2_ref(endpoint, +1); + sub->endpoint = endpoint; + sub->handler = handler; + return sub; +} + +struct ast_sip_endpoint *ast_sip_subscription_get_endpoint(struct ast_sip_subscription *sub) +{ + ast_assert(sub->endpoint != NULL); + ao2_ref(sub->endpoint, +1); + return sub->endpoint; +} + +struct ast_taskprocessor *ast_sip_subscription_get_serializer(struct ast_sip_subscription *sub) +{ + ast_assert(sub->serializer != NULL); + return sub->serializer; +} + +pjsip_evsub *ast_sip_subscription_get_evsub(struct ast_sip_subscription *sub) +{ + return sub->evsub; +} + +int ast_sip_subscription_send_request(struct ast_sip_subscription *sub, pjsip_tx_data *tdata) +{ + return pjsip_evsub_send_request(ast_sip_subscription_get_evsub(sub), + tdata) == PJ_SUCCESS ? 0 : -1; +} + +static void subscription_datastore_destroy(void *obj) +{ + struct ast_datastore *datastore = obj; + + /* Using the destroy function (if present) destroy the data */ + if (datastore->info->destroy != NULL && datastore->data != NULL) { + datastore->info->destroy(datastore->data); + datastore->data = NULL; + } + + ast_free((void *) datastore->uid); + datastore->uid = NULL; +} + +struct ast_datastore *ast_sip_subscription_alloc_datastore(const struct ast_datastore_info *info, const char *uid) +{ + RAII_VAR(struct ast_datastore *, datastore, NULL, ao2_cleanup); + const char *uid_ptr = uid; + + if (!info) { + return NULL; + } + + datastore = ao2_alloc(sizeof(*datastore), subscription_datastore_destroy); + if (!datastore) { + return NULL; + } + + datastore->info = info; + if (ast_strlen_zero(uid)) { + /* They didn't provide an ID so we'll provide one ourself */ + struct ast_uuid *uuid = ast_uuid_generate(); + char uuid_buf[AST_UUID_STR_LEN]; + if (!uuid) { + return NULL; + } + uid_ptr = ast_uuid_to_str(uuid, uuid_buf, sizeof(uuid_buf)); + ast_free(uuid); + } + + datastore->uid = ast_strdup(uid_ptr); + if (!datastore->uid) { + return NULL; + } + + ao2_ref(datastore, +1); + return datastore; +} + +int ast_sip_subscription_add_datastore(struct ast_sip_subscription *subscription, struct ast_datastore *datastore) +{ + ast_assert(datastore != NULL); + ast_assert(datastore->info != NULL); + ast_assert(ast_strlen_zero(datastore->uid) == 0); + + if (!ao2_link(subscription->datastores, datastore)) { + return -1; + } + return 0; +} + +struct ast_datastore *ast_sip_subscription_get_datastore(struct ast_sip_subscription *subscription, const char *name) +{ + return ao2_find(subscription->datastores, name, OBJ_KEY); +} + +void ast_sip_subscription_remove_datastore(struct ast_sip_subscription *subscription, const char *name) +{ + ao2_callback(subscription->datastores, OBJ_KEY | OBJ_UNLINK | OBJ_NODATA, NULL, (void *) name); +} + +AST_RWLIST_HEAD_STATIC(subscription_handlers, ast_sip_subscription_handler); + +static void add_handler(struct ast_sip_subscription_handler *handler) +{ + SCOPED_LOCK(lock, &subscription_handlers, AST_RWLIST_WRLOCK, AST_RWLIST_UNLOCK); + AST_RWLIST_INSERT_TAIL(&subscription_handlers, handler, next); + ast_module_ref(ast_module_info->self); +} + +static int handler_exists_for_event_name(const char *event_name) +{ + struct ast_sip_subscription_handler *iter; + SCOPED_LOCK(lock, &subscription_handlers, AST_RWLIST_RDLOCK, AST_RWLIST_UNLOCK); + + AST_RWLIST_TRAVERSE(&subscription_handlers, iter, next) { + if (!strcmp(iter->event_name, event_name)) { + return 1; + } + } + return 0; +} + +int ast_sip_register_subscription_handler(struct ast_sip_subscription_handler *handler) +{ + pj_str_t event; + pj_str_t accept[AST_SIP_MAX_ACCEPT]; + int i; + + if (ast_strlen_zero(handler->event_name)) { + ast_log(LOG_ERROR, "No event package specifief for subscription handler. Cannot register\n"); + return -1; + } + + if (ast_strlen_zero(handler->accept[0])) { + ast_log(LOG_ERROR, "Subscription handler must supply at least one 'Accept' format\n"); + return -1; + } + + if (handler_exists_for_event_name(handler->event_name)) { + ast_log(LOG_ERROR, "A subscription handler for event %s already exists. Not registering " + "new subscription handler\n", handler->event_name); + return -1; + } + + pj_cstr(&event, handler->event_name); + for (i = 0; i < AST_SIP_MAX_ACCEPT && !ast_strlen_zero(handler->accept[i]); ++i) { + pj_cstr(&accept[i], handler->accept[i]); + } + + if (!strcmp(handler->event_name, "message-summary")) { + pjsip_mwi_init_module(ast_sip_get_pjsip_endpoint(), pjsip_evsub_instance()); + } else if (!strcmp(handler->event_name, "presence")) { + pjsip_pres_init_module(ast_sip_get_pjsip_endpoint(), pjsip_evsub_instance()); + } else { + pjsip_evsub_register_pkg(&sub_module, &event, DEFAULT_EXPIRES, i, accept); + } + + add_handler(handler); + return 0; +} + +void ast_sip_unregister_subscription_handler(struct ast_sip_subscription_handler *handler) +{ + struct ast_sip_subscription_handler *iter; + SCOPED_LOCK(lock, &subscription_handlers, AST_RWLIST_WRLOCK, AST_RWLIST_UNLOCK); + AST_RWLIST_TRAVERSE_SAFE_BEGIN(&subscription_handlers, iter, next) { + if (handler == iter) { + AST_RWLIST_REMOVE_CURRENT(next); + ast_module_unref(ast_module_info->self); + break; + } + } + AST_RWLIST_TRAVERSE_SAFE_END; +} + +static struct ast_sip_subscription_handler *find_handler(const char *event, char accept[AST_SIP_MAX_ACCEPT][64], size_t num_accept) +{ + struct ast_sip_subscription_handler *iter; + int match = 0; + SCOPED_LOCK(lock, &subscription_handlers, AST_RWLIST_RDLOCK, AST_RWLIST_UNLOCK); + AST_RWLIST_TRAVERSE(&subscription_handlers, iter, next) { + int i; + int j; + if (strcmp(event, iter->event_name)) { + ast_debug(3, "Event %s does not match %s\n", event, iter->event_name); + continue; + } + ast_debug(3, "Event name match: %s = %s\n", event, iter->event_name); + for (i = 0; i < num_accept; ++i) { + for (j = 0; j < num_accept; ++j) { + if (ast_strlen_zero(iter->accept[i])) { + ast_debug(3, "Breaking because subscription handler has run out of 'accept' types\n"); + break; + } + if (!strcmp(accept[j], iter->accept[i])) { + ast_debug(3, "Accept headers match: %s = %s\n", accept[j], iter->accept[i]); + match = 1; + break; + } + ast_debug(3, "Accept %s does not match %s\n", accept[j], iter->accept[i]); + } + if (match) { + break; + } + } + if (match) { + break; + } + } + + return iter; +} + +static pj_bool_t sub_on_rx_request(pjsip_rx_data *rdata) +{ + static const pj_str_t event_name = { "Event", 5 }; + char event[32]; + char accept[AST_SIP_MAX_ACCEPT][64]; + pjsip_accept_hdr *accept_header; + pjsip_event_hdr *event_header; + struct ast_sip_subscription_handler *handler; + RAII_VAR(struct ast_sip_endpoint *, endpoint, NULL, ao2_cleanup); + struct ast_sip_subscription *sub; + int i; + + if (pjsip_method_cmp(&rdata->msg_info.msg->line.req.method, pjsip_get_subscribe_method())) { + return PJ_FALSE; + } + + endpoint = ast_pjsip_rdata_get_endpoint(rdata); + ast_assert(endpoint != NULL); + + event_header = pjsip_msg_find_hdr_by_name(rdata->msg_info.msg, &event_name, rdata->msg_info.msg->hdr.next); + if (!event_header) { + ast_log(LOG_WARNING, "Incoming SUBSCRIBE request with no Event header\n"); + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 489, NULL, NULL, NULL); + return PJ_TRUE; + } + + accept_header = pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_ACCEPT, rdata->msg_info.msg->hdr.next); + if (!accept_header) { + ast_log(LOG_WARNING, "Incoming SUBSCRIBE request with no Accept header\n"); + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 400, NULL, NULL, NULL); + return PJ_TRUE; + } + + ast_copy_pj_str(event, &event_header->event_type, sizeof(event)); + for (i = 0; i < accept_header->count; ++i) { + ast_copy_pj_str(accept[i], &accept_header->values[i], sizeof(accept[i])); + } + + handler = find_handler(event, accept, accept_header->count); + if (!handler) { + ast_log(LOG_WARNING, "No registered handler for event %s\n", event); + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 489, NULL, NULL, NULL); + return PJ_TRUE; + } + sub = handler->new_subscribe(endpoint, rdata); + if (!sub) { + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 500, NULL, NULL, NULL); + } + return PJ_TRUE; +} + +static void pubsub_on_evsub_state(pjsip_evsub *evsub, pjsip_event *event) +{ + struct ast_sip_subscription *sub; + if (pjsip_evsub_get_state(evsub) != PJSIP_EVSUB_STATE_TERMINATED) { + return; + } + + sub = pjsip_evsub_get_mod_data(evsub, sub_module.id); + if (!sub) { + return; + } + + if (event->type == PJSIP_EVENT_RX_MSG) { + sub->handler->subscription_terminated(sub, event->body.rx_msg.rdata); + } + + if (event->type == PJSIP_EVENT_TSX_STATE && + event->body.tsx_state.type == PJSIP_EVENT_RX_MSG) { + sub->handler->subscription_terminated(sub, event->body.tsx_state.src.rdata); + } + + if (sub->handler->subscription_shutdown) { + sub->handler->subscription_shutdown(sub); + } + pjsip_evsub_set_mod_data(evsub, sub_module.id, NULL); +} + +static void pubsub_on_tsx_state(pjsip_evsub *evsub, pjsip_transaction *tsx, pjsip_event *event) +{ + struct ast_sip_subscription *sub = pjsip_evsub_get_mod_data(evsub, sub_module.id); + + if (!sub) { + return; + } + + if (tsx->role == PJSIP_ROLE_UAC && event->body.tsx_state.type == PJSIP_EVENT_RX_MSG) { + sub->handler->notify_response(sub, event->body.tsx_state.src.rdata); + } +} + +static void set_parameters_from_response_data(pj_pool_t *pool, int *p_st_code, + pj_str_t **p_st_text, pjsip_hdr *res_hdr, pjsip_msg_body **p_body, + struct ast_sip_subscription_response_data *response_data) +{ + ast_assert(response_data->status_code >= 200 && response_data->status_code <= 699); + *p_st_code = response_data->status_code; + + if (!ast_strlen_zero(response_data->status_text)) { + pj_strdup2(pool, *p_st_text, response_data->status_text); + } + + if (response_data->headers) { + struct ast_variable *iter; + for (iter = response_data->headers; iter; iter = iter->next) { + pj_str_t header_name; + pj_str_t header_value; + pjsip_generic_string_hdr *hdr; + + pj_cstr(&header_name, iter->name); + pj_cstr(&header_value, iter->value); + hdr = pjsip_generic_string_hdr_create(pool, &header_name, &header_value); + pj_list_insert_before(res_hdr, hdr); + } + } + + if (response_data->body) { + pj_str_t type; + pj_str_t subtype; + pj_str_t body_text; + + pj_cstr(&type, response_data->body->type); + pj_cstr(&subtype, response_data->body->subtype); + pj_cstr(&body_text, response_data->body->body_text); + + *p_body = pjsip_msg_body_create(pool, &type, &subtype, &body_text); + } +} + +static int response_data_changed(struct ast_sip_subscription_response_data *response_data) +{ + if (response_data->status_code != 200 || + !ast_strlen_zero(response_data->status_text) || + response_data->headers || + response_data->body) { + return 1; + } + return 0; +} + +static void pubsub_on_rx_refresh(pjsip_evsub *evsub, pjsip_rx_data *rdata, + int *p_st_code, pj_str_t **p_st_text, pjsip_hdr *res_hdr, pjsip_msg_body **p_body) +{ + struct ast_sip_subscription *sub = pjsip_evsub_get_mod_data(evsub, sub_module.id); + struct ast_sip_subscription_response_data response_data = { + .status_code = 200, + }; + + if (!sub) { + return; + } + + sub->handler->resubscribe(sub, rdata, &response_data); + + if (!response_data_changed(&response_data)) { + return; + } + + set_parameters_from_response_data(rdata->tp_info.pool, p_st_code, p_st_text, + res_hdr, p_body, &response_data); +} + +static void pubsub_on_rx_notify(pjsip_evsub *evsub, pjsip_rx_data *rdata, int *p_st_code, + pj_str_t **p_st_text, pjsip_hdr *res_hdr, pjsip_msg_body **p_body) +{ + struct ast_sip_subscription *sub = pjsip_evsub_get_mod_data(evsub, sub_module.id); + struct ast_sip_subscription_response_data response_data = { + .status_code = 200, + }; + + if (!sub|| !sub->handler->notify_request) { + return; + } + + sub->handler->notify_request(sub, rdata, &response_data); + + if (!response_data_changed(&response_data)) { + return; + } + + set_parameters_from_response_data(rdata->tp_info.pool, p_st_code, p_st_text, + res_hdr, p_body, &response_data); +} + +static int serialized_pubsub_on_client_refresh(void *userdata) +{ + struct ast_sip_subscription *sub = userdata; + + sub->handler->refresh_subscription(sub); + ao2_cleanup(sub); + return 0; +} + +static void pubsub_on_client_refresh(pjsip_evsub *evsub) +{ + struct ast_sip_subscription *sub = pjsip_evsub_get_mod_data(evsub, sub_module.id); + + ao2_ref(sub, +1); + ast_sip_push_task(sub->serializer, serialized_pubsub_on_client_refresh, sub); +} + +static int serialized_pubsub_on_server_timeout(void *userdata) +{ + struct ast_sip_subscription *sub = userdata; + + sub->handler->subscription_timeout(sub); + ao2_cleanup(sub); + return 0; +} + +static void pubsub_on_server_timeout(pjsip_evsub *evsub) +{ + struct ast_sip_subscription *sub = pjsip_evsub_get_mod_data(evsub, sub_module.id); + + ao2_ref(sub, +1); + ast_sip_push_task(sub->serializer, serialized_pubsub_on_server_timeout, sub); +} + +static int load_module(void) +{ + pjsip_evsub_init_module(ast_sip_get_pjsip_endpoint()); + if (ast_sip_register_service(&sub_module)) { + return AST_MODULE_LOAD_DECLINE; + } + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "SIP event resource", + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_CHANNEL_DEPEND, +); diff --git a/res/res_sip_pubsub.exports.in b/res/res_sip_pubsub.exports.in new file mode 100644 index 000000000..55308746a --- /dev/null +++ b/res/res_sip_pubsub.exports.in @@ -0,0 +1,16 @@ +{ + global: + LINKER_SYMBOL_PREFIXast_sip_create_subscription; + LINKER_SYMBOL_PREFIXast_sip_subsription_get_endpoint; + LINKER_SYMBOL_PREFIXast_sip_subscription_get_serializer; + LINKER_SYMBOL_PREFIXast_sip_subscription_get_evsub; + LINKER_SYMBOL_PREFIXast_sip_subscription_send_request; + LINKER_SYMBOL_PREFIXast_sip_subscription_alloc_datastore; + LINKER_SYMBOL_PREFIXast_sip_subscription_add_datastore; + LINKER_SYMBOL_PREFIXast_sip_subscription_get_datastore; + LINKER_SYMBOL_PREFIXast_sip_subscription_remove_datastore; + LINKER_SYMBOL_PREFIXast_sip_register_subscription_handler; + LINKER_SYMBOL_PREFIXast_sip_unregister_subscription_handler; + local: + *; +}; diff --git a/res/res_sip_registrar.c b/res/res_sip_registrar.c new file mode 100644 index 000000000..e5a2e888b --- /dev/null +++ b/res/res_sip_registrar.c @@ -0,0 +1,382 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Joshua Colp <jcolp@digium.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 + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include <pjsip.h> +#include <pjsip_ua.h> + +#include "asterisk/res_sip.h" +#include "asterisk/module.h" + +/*! \brief Internal function which returns the expiration time for a contact */ +static int registrar_get_expiration(const struct ast_sip_aor *aor, const pjsip_contact_hdr *contact, const pjsip_rx_data *rdata) +{ + pjsip_expires_hdr *expires; + int expiration = aor->default_expiration; + + if (contact->expires != -1) { + /* Expiration was provided with the contact itself */ + expiration = contact->expires; + } else if ((expires = pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_EXPIRES, NULL))) { + /* Expiration was provided using the Expires header */ + expiration = expires->ivalue; + } + + /* If the value has explicitly been set to 0, do not enforce */ + if (!expiration) { + return expiration; + } + + /* Enforce the range that we will allow for expiration */ + if (expiration < aor->minimum_expiration) { + expiration = aor->minimum_expiration; + } else if (expiration > aor->maximum_expiration) { + expiration = aor->maximum_expiration; + } + + return expiration; +} + +/*! \brief Structure used for finding contact */ +struct registrar_contact_details { + /*! \brief Pool used for parsing URI */ + pj_pool_t *pool; + /*! \brief URI being looked for */ + pjsip_uri *uri; +}; + +/*! \brief Callback function for finding a contact */ +static int registrar_find_contact(void *obj, void *arg, int flags) +{ + struct ast_sip_contact *contact = obj; + const struct registrar_contact_details *details = arg; + pjsip_uri *contact_uri = pjsip_parse_uri(details->pool, (char*)contact->uri, strlen(contact->uri), 0); + + return (pjsip_uri_cmp(PJSIP_URI_IN_CONTACT_HDR, details->uri, contact_uri) == PJ_SUCCESS) ? CMP_MATCH | CMP_STOP : 0; +} + +/*! \brief Internal function which validates provided Contact headers to confirm that they are acceptable, and returns number of contacts */ +static int registrar_validate_contacts(const pjsip_rx_data *rdata, struct ao2_container *contacts, struct ast_sip_aor *aor, int *added, int *updated, int *deleted) +{ + pjsip_contact_hdr *previous = NULL, *contact = (pjsip_contact_hdr *)&rdata->msg_info.msg->hdr; + struct registrar_contact_details details = { + .pool = pjsip_endpt_create_pool(ast_sip_get_pjsip_endpoint(), "Contact Comparison", 256, 256), + }; + + if (!details.pool) { + return -1; + } + + while ((contact = (pjsip_contact_hdr *) pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_CONTACT, contact->next))) { + int expiration; + RAII_VAR(struct ast_sip_contact *, existing, NULL, ao2_cleanup); + + if (contact->star) { + /* The expiration MUST be 0 when a '*' contact is used and there must be no other contact */ + if ((contact->expires != 0) || previous) { + pjsip_endpt_release_pool(ast_sip_get_pjsip_endpoint(), details.pool); + return -1; + } + } else if (previous && previous->star) { + /* If there is a previous contact and it is a '*' this is a deal breaker */ + pjsip_endpt_release_pool(ast_sip_get_pjsip_endpoint(), details.pool); + return -1; + } + previous = contact; + + if (!PJSIP_URI_SCHEME_IS_SIP(contact->uri) && !PJSIP_URI_SCHEME_IS_SIPS(contact->uri)) { + continue; + } + + details.uri = pjsip_uri_get_uri(contact->uri); + expiration = registrar_get_expiration(aor, contact, rdata); + + /* Determine if this is an add, update, or delete for policy enforcement purposes */ + if (!(existing = ao2_callback(contacts, 0, registrar_find_contact, &details))) { + if (expiration) { + (*added)++; + } + } else if (expiration) { + (*updated)++; + } else { + (*deleted)++; + } + } + + /* The provided contacts are acceptable, huzzah! */ + pjsip_endpt_release_pool(ast_sip_get_pjsip_endpoint(), details.pool); + return 0; +} + +/*! \brief Callback function which prunes static contacts */ +static int registrar_prune_static(void *obj, void *arg, int flags) +{ + struct ast_sip_contact *contact = obj; + + return ast_tvzero(contact->expiration_time) ? CMP_MATCH : 0; +} + +/*! \brief Internal function used to delete all contacts from an AOR */ +static int registrar_delete_contact(void *obj, void *arg, int flags) +{ + struct ast_sip_contact *contact = obj; + + ast_sip_location_delete_contact(contact); + + return 0; +} + +/*! \brief Internal function which adds a contact to a response */ +static int registrar_add_contact(void *obj, void *arg, int flags) +{ + struct ast_sip_contact *contact = obj; + pjsip_tx_data *tdata = arg; + pjsip_contact_hdr *hdr = pjsip_contact_hdr_create(tdata->pool); + pj_str_t uri; + + pj_strdup2_with_null(tdata->pool, &uri, contact->uri); + hdr->uri = pjsip_parse_uri(tdata->pool, uri.ptr, uri.slen, PJSIP_PARSE_URI_AS_NAMEADDR); + hdr->expires = ast_tvdiff_ms(contact->expiration_time, ast_tvnow()) / 1000; + + pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr*)hdr); + + return 0; +} + +/*! \brief Helper function which adds a Date header to a response */ +static void registrar_add_date_header(pjsip_tx_data *tdata) +{ + char date[256]; + struct tm tm; + time_t t = time(NULL); + + gmtime_r(&t, &tm); + strftime(date, sizeof(date), "%a, %d %b %Y %T GMT", &tm); + + ast_sip_add_header(tdata, "Date", date); +} + +static pj_bool_t registrar_on_rx_request(struct pjsip_rx_data *rdata) +{ + struct ast_sip_endpoint *endpoint = ast_pjsip_rdata_get_endpoint(rdata); + pjsip_sip_uri *uri; + char user_name[64], domain_name[64]; + char *configured_aors, *aor_name; + RAII_VAR(struct ast_sip_aor *, aor, NULL, ao2_cleanup); + RAII_VAR(struct ao2_container *, contacts, NULL, ao2_cleanup); + int added = 0, updated = 0, deleted = 0; + pjsip_contact_hdr *contact_hdr = NULL; + struct registrar_contact_details details = { 0, }; + pjsip_tx_data *tdata; + pjsip_response_addr addr; + + if (pjsip_method_cmp(&rdata->msg_info.msg->line.req.method, &pjsip_register_method) || !endpoint) { + return PJ_FALSE; + } + + if (ast_strlen_zero(endpoint->aors)) { + /* Short circuit early if the endpoint has no AORs configured on it, which means no registration possible */ + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 403, NULL, NULL, NULL); + return PJ_TRUE; + } + + if (!PJSIP_URI_SCHEME_IS_SIP(rdata->msg_info.to->uri) && !PJSIP_URI_SCHEME_IS_SIPS(rdata->msg_info.to->uri)) { + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 416, NULL, NULL, NULL); + return PJ_TRUE; + } + + uri = pjsip_uri_get_uri(rdata->msg_info.to->uri); + ast_copy_pj_str(user_name, &uri->user, sizeof(user_name)); + ast_copy_pj_str(domain_name, &uri->host, sizeof(domain_name)); + + configured_aors = ast_strdupa(endpoint->aors); + + /* Iterate the configured AORs to see if the user or the user+domain match */ + while ((aor_name = strsep(&configured_aors, ","))) { + char id[AST_UUID_STR_LEN]; + RAII_VAR(struct ast_sip_domain_alias *, alias, NULL, ao2_cleanup); + + snprintf(id, sizeof(id), "%s@%s", user_name, domain_name); + if (!strcmp(aor_name, id)) { + break; + } + + if ((alias = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "domain_alias", domain_name))) { + snprintf(id, sizeof(id), "%s@%s", user_name, alias->domain); + if (!strcmp(aor_name, id)) { + break; + } + } + + if (!strcmp(aor_name, user_name)) { + break; + } + } + + if (ast_strlen_zero(aor_name) || !(aor = ast_sip_location_retrieve_aor(aor_name))) { + /* The provided AOR name was not found (be it within the configuration or sorcery itself) */ + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 404, NULL, NULL, NULL); + return PJ_TRUE; + } + + if (!aor->max_contacts) { + /* Registration is not permitted for this AOR */ + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 403, NULL, NULL, NULL); + return PJ_TRUE; + } + + /* Retrieve the current contacts, we'll need to know whether to update or not */ + contacts = ast_sip_location_retrieve_aor_contacts(aor); + + /* So we don't count static contacts against max_contacts we prune them out from the container */ + ao2_callback(contacts, OBJ_NODATA | OBJ_UNLINK | OBJ_MULTIPLE, registrar_prune_static, NULL); + + if (registrar_validate_contacts(rdata, contacts, aor, &added, &updated, &deleted)) { + /* The provided Contact headers do not conform to the specification */ + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 400, NULL, NULL, NULL); + return PJ_TRUE; + } + + if (((added - deleted) + (!aor->remove_existing ? ao2_container_count(contacts) : 0)) > aor->max_contacts) { + /* Enforce the maximum number of contacts */ + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 403, NULL, NULL, NULL); + return PJ_TRUE; + } + + if (!(details.pool = pjsip_endpt_create_pool(ast_sip_get_pjsip_endpoint(), "Contact Comparison", 256, 256))) { + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 500, NULL, NULL, NULL); + return PJ_TRUE; + } + + /* Iterate each provided Contact header and add, update, or delete */ + while ((contact_hdr = pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_CONTACT, contact_hdr ? contact_hdr->next : NULL))) { + int expiration; + char contact_uri[PJSIP_MAX_URL_SIZE]; + RAII_VAR(struct ast_sip_contact *, contact, NULL, ao2_cleanup); + + if (contact_hdr->star) { + /* A star means to unregister everything, so do so for the possible contacts */ + ao2_callback(contacts, OBJ_NODATA | OBJ_MULTIPLE, registrar_delete_contact, NULL); + break; + } + + if (!PJSIP_URI_SCHEME_IS_SIP(contact_hdr->uri) && !PJSIP_URI_SCHEME_IS_SIPS(contact_hdr->uri)) { + /* This registrar only currently supports sip: and sips: URI schemes */ + continue; + } + + expiration = registrar_get_expiration(aor, contact_hdr, rdata); + details.uri = pjsip_uri_get_uri(contact_hdr->uri); + pjsip_uri_print(PJSIP_URI_IN_CONTACT_HDR, details.uri, contact_uri, sizeof(contact_uri)); + + if (!(contact = ao2_callback(contacts, OBJ_UNLINK, registrar_find_contact, &details))) { + /* If they are actually trying to delete a contact that does not exist... be forgiving */ + if (!expiration) { + ast_verb(3, "Attempted to remove non-existent contact '%s' from AOR '%s' by request\n", + contact_uri, aor_name); + continue; + } + + ast_sip_location_add_contact(aor, contact_uri, ast_tvadd(ast_tvnow(), ast_samp2tv(expiration, 1))); + ast_verb(3, "Added contact '%s' to AOR '%s' with expiration of %d seconds\n", + contact_uri, aor_name, expiration); + } else if (expiration) { + RAII_VAR(struct ast_sip_contact *, updated, ast_sorcery_copy(ast_sip_get_sorcery(), contact), ao2_cleanup); + + updated->expiration_time = ast_tvadd(ast_tvnow(), ast_samp2tv(expiration, 1)); + + ast_sip_location_update_contact(updated); + ast_debug(3, "Refreshed contact '%s' on AOR '%s' with new expiration of %d seconds\n", + contact_uri, aor_name, expiration); + } else { + ast_sip_location_delete_contact(contact); + ast_verb(3, "Removed contact '%s' from AOR '%s' due to request\n", contact_uri, aor_name); + } + } + + pjsip_endpt_release_pool(ast_sip_get_pjsip_endpoint(), details.pool); + + /* If the AOR is configured to remove any existing contacts that have not been updated/added as a result of this REGISTER + * do so + */ + if (aor->remove_existing) { + ao2_callback(contacts, OBJ_NODATA | OBJ_MULTIPLE, registrar_delete_contact, NULL); + } + + /* Update the contacts as things will probably have changed */ + ao2_cleanup(contacts); + contacts = ast_sip_location_retrieve_aor_contacts(aor); + + /* Send a response containing all of the contacts (including static) that are present on this AOR */ + if (pjsip_endpt_create_response(ast_sip_get_pjsip_endpoint(), rdata, 200, NULL, &tdata) != PJ_SUCCESS) { + return PJ_TRUE; + } + + /* Add the date header to the response, some UAs use this to set their date and time */ + registrar_add_date_header(tdata); + + ao2_callback(contacts, 0, registrar_add_contact, tdata); + + if (pjsip_get_response_addr(tdata->pool, rdata, &addr) == PJ_SUCCESS) { + pjsip_endpt_send_response(ast_sip_get_pjsip_endpoint(), &addr, tdata, NULL, NULL); + } else { + pjsip_tx_data_dec_ref(tdata); + } + + return PJ_TRUE; +} + +static pjsip_module registrar_module = { + .name = { "Registrar", 9 }, + .id = -1, + .priority = PJSIP_MOD_PRIORITY_APPLICATION, + .on_rx_request = registrar_on_rx_request, +}; + +static int load_module(void) +{ + const pj_str_t STR_REGISTER = { "REGISTER", 8 }; + + if (ast_sip_register_service(®istrar_module)) { + return AST_MODULE_LOAD_DECLINE; + } + + if (pjsip_endpt_add_capability(ast_sip_get_pjsip_endpoint(), NULL, PJSIP_H_ALLOW, NULL, 1, &STR_REGISTER) != PJ_SUCCESS) { + ast_sip_unregister_service(®istrar_module); + return AST_MODULE_LOAD_DECLINE; + } + + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + ast_sip_unregister_service(®istrar_module); + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "SIP Registrar Support", + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_APP_DEPEND, + ); diff --git a/res/res_sip_rfc3326.c b/res/res_sip_rfc3326.c new file mode 100644 index 000000000..1c9ec6154 --- /dev/null +++ b/res/res_sip_rfc3326.c @@ -0,0 +1,145 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Joshua Colp <jcolp@digium.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>pjproject</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include <pjsip.h> +#include <pjsip_ua.h> + +#include "asterisk/res_sip.h" +#include "asterisk/res_sip_session.h" +#include "asterisk/module.h" +#include "asterisk/causes.h" + +static void rfc3326_use_reason_header(struct ast_sip_session *session, struct pjsip_rx_data *rdata) +{ + const pj_str_t str_reason = { "Reason", 6 }; + pjsip_generic_string_hdr *header = pjsip_msg_find_hdr_by_name(rdata->msg_info.msg, &str_reason, NULL); + char buf[20], *cause, *text; + int code; + + if (!header) { + return; + } + + ast_copy_pj_str(buf, &header->hvalue, sizeof(buf)); + cause = ast_skip_blanks(buf); + + if (strncasecmp(cause, "Q.850", 5) || !(cause = strstr(cause, "cause="))) { + return; + } + + /* If text is present get rid of it */ + if ((text = strstr(cause, ";"))) { + *text = '\0'; + } + + if (sscanf(cause, "cause=%30d", &code) != 1) { + return; + } + + ast_channel_hangupcause_set(session->channel, code & 0x7f); +} + +static int rfc3326_incoming_request(struct ast_sip_session *session, struct pjsip_rx_data *rdata) +{ + if ((pjsip_method_cmp(&rdata->msg_info.msg->line.req.method, &pjsip_bye_method) && + pjsip_method_cmp(&rdata->msg_info.msg->line.req.method, &pjsip_cancel_method)) || + !session->channel) { + return 0; + } + + rfc3326_use_reason_header(session, rdata); + + return 0; +} + +static void rfc3326_incoming_response(struct ast_sip_session *session, struct pjsip_rx_data *rdata) +{ + struct pjsip_status_line status = rdata->msg_info.msg->line.status; + + if ((status.code < 300) || !session->channel) { + return; + } + + rfc3326_use_reason_header(session, rdata); +} + +static void rfc3326_add_reason_header(struct ast_sip_session *session, struct pjsip_tx_data *tdata) +{ + char buf[20]; + + snprintf(buf, sizeof(buf), "Q.850;cause=%i", ast_channel_hangupcause(session->channel) & 0x7f); + ast_sip_add_header(tdata, "Reason", buf); + + if (ast_channel_hangupcause(session->channel) == AST_CAUSE_ANSWERED_ELSEWHERE) { + ast_sip_add_header(tdata, "Reason", "SIP;cause=200;text=\"Call completed elsewhere\""); + } +} + +static void rfc3326_outgoing_request(struct ast_sip_session *session, struct pjsip_tx_data *tdata) +{ + if ((pjsip_method_cmp(&tdata->msg->line.req.method, &pjsip_bye_method) && + pjsip_method_cmp(&tdata->msg->line.req.method, &pjsip_cancel_method)) || + !session->channel) { + return; + } + + rfc3326_add_reason_header(session, tdata); +} + +static void rfc3326_outgoing_response(struct ast_sip_session *session, struct pjsip_tx_data *tdata) +{ + struct pjsip_status_line status = tdata->msg->line.status; + + if ((status.code < 300) || !session->channel) { + return; + } + + rfc3326_add_reason_header(session, tdata); +} + +static struct ast_sip_session_supplement rfc3326_supplement = { + .incoming_request = rfc3326_incoming_request, + .incoming_response = rfc3326_incoming_response, + .outgoing_request = rfc3326_outgoing_request, + .outgoing_response = rfc3326_outgoing_response, +}; + +static int load_module(void) +{ + ast_sip_session_register_supplement(&rfc3326_supplement); + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + ast_sip_session_unregister_supplement(&rfc3326_supplement); + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "SIP RFC3326 Support", + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_APP_DEPEND, + ); diff --git a/res/res_sip_sdp_rtp.c b/res/res_sip_sdp_rtp.c new file mode 100644 index 000000000..13e6aa1aa --- /dev/null +++ b/res/res_sip_sdp_rtp.c @@ -0,0 +1,848 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013, Digium, Inc. + * + * Joshua Colp <jcolp@digium.com> + * Kevin Harwell <kharwell@digium.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. + */ + +/*! \file + * + * \author Joshua Colp <jcolp@digium.com> + * + * \brief SIP SDP media stream handling + */ + +/*** MODULEINFO + <depend>pjproject</depend> + <depend>res_sip</depend> + <depend>res_sip_session</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include <pjsip.h> +#include <pjsip_ua.h> +#include <pjmedia.h> +#include <pjlib.h> + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include "asterisk/module.h" +#include "asterisk/rtp_engine.h" +#include "asterisk/netsock2.h" +#include "asterisk/channel.h" +#include "asterisk/causes.h" +#include "asterisk/sched.h" +#include "asterisk/acl.h" + +#include "asterisk/res_sip.h" +#include "asterisk/res_sip_session.h" + +/*! \brief Scheduler for RTCP purposes */ +static struct ast_sched_context *sched; + +/*! \brief Address for IPv4 RTP */ +static struct ast_sockaddr address_ipv4; + +/*! \brief Address for IPv6 RTP */ +static struct ast_sockaddr address_ipv6; + +static const char STR_AUDIO[] = "audio"; +static const int FD_AUDIO = 0; + +static const char STR_VIDEO[] = "video"; +static const int FD_VIDEO = 2; + +/*! \brief Retrieves an ast_format_type based on the given stream_type */ +static enum ast_format_type stream_to_media_type(const char *stream_type) +{ + if (!strcasecmp(stream_type, STR_AUDIO)) { + return AST_FORMAT_TYPE_AUDIO; + } else if (!strcasecmp(stream_type, STR_VIDEO)) { + return AST_FORMAT_TYPE_VIDEO; + } + + return 0; +} + +/*! \brief Get the starting descriptor for a media type */ +static int media_type_to_fdno(enum ast_format_type media_type) +{ + switch (media_type) { + case AST_FORMAT_TYPE_AUDIO: return FD_AUDIO; + case AST_FORMAT_TYPE_VIDEO: return FD_VIDEO; + case AST_FORMAT_TYPE_TEXT: + case AST_FORMAT_TYPE_IMAGE: break; + } + return -1; +} + +/*! \brief Remove all other cap types but the one given */ +static void format_cap_only_type(struct ast_format_cap *caps, enum ast_format_type media_type) +{ + int i = AST_FORMAT_INC; + while (i <= AST_FORMAT_TYPE_TEXT) { + if (i != media_type) { + ast_format_cap_remove_bytype(caps, i); + } + i += AST_FORMAT_INC; + } +} + +/*! \brief Internal function which creates an RTP instance */ +static int create_rtp(struct ast_sip_session *session, struct ast_sip_session_media *session_media, unsigned int ipv6) +{ + struct ast_rtp_engine_ice *ice; + + if (!(session_media->rtp = ast_rtp_instance_new("asterisk", sched, ipv6 ? &address_ipv6 : &address_ipv4, NULL))) { + return -1; + } + + ast_rtp_instance_set_prop(session_media->rtp, AST_RTP_PROPERTY_RTCP, 1); + ast_rtp_instance_set_prop(session_media->rtp, AST_RTP_PROPERTY_NAT, session->endpoint->rtp_symmetric); + + ast_rtp_codecs_packetization_set(ast_rtp_instance_get_codecs(session_media->rtp), + session_media->rtp, &session->endpoint->prefs); + + if (!session->endpoint->ice_support && (ice = ast_rtp_instance_get_ice(session_media->rtp))) { + ice->stop(session_media->rtp); + } + + return 0; +} + +static void get_codecs(struct ast_sip_session *session, const struct pjmedia_sdp_media *stream, struct ast_rtp_codecs *codecs) +{ + pjmedia_sdp_attr *attr; + pjmedia_sdp_rtpmap *rtpmap; + pjmedia_sdp_fmtp fmtp; + struct ast_format *format; + int i, num = 0; + char name[256]; + char media[20]; + char fmt_param[256]; + + ast_rtp_codecs_payloads_initialize(codecs); + + /* Iterate through provided formats */ + for (i = 0; i < stream->desc.fmt_count; ++i) { + /* The payload is kept as a string for things like t38 but for video it is always numerical */ + ast_rtp_codecs_payloads_set_m_type(codecs, NULL, pj_strtoul(&stream->desc.fmt[i])); + /* Look for the optional rtpmap attribute */ + if (!(attr = pjmedia_sdp_media_find_attr2(stream, "rtpmap", &stream->desc.fmt[i]))) { + continue; + } + + /* Interpret the attribute as an rtpmap */ + if ((pjmedia_sdp_attr_to_rtpmap(session->inv_session->pool_prov, attr, &rtpmap)) != PJ_SUCCESS) { + continue; + } + + ast_copy_pj_str(name, &rtpmap->enc_name, sizeof(name)); + ast_copy_pj_str(media, (pj_str_t*)&stream->desc.media, sizeof(media)); + ast_rtp_codecs_payloads_set_rtpmap_type_rate(codecs, NULL, pj_strtoul(&stream->desc.fmt[i]), + media, name, 0, rtpmap->clock_rate); + /* Look for an optional associated fmtp attribute */ + if (!(attr = pjmedia_sdp_media_find_attr2(stream, "fmtp", &rtpmap->pt))) { + continue; + } + + if ((pjmedia_sdp_attr_get_fmtp(attr, &fmtp)) == PJ_SUCCESS) { + sscanf(pj_strbuf(&fmtp.fmt), "%d", &num); + if ((format = ast_rtp_codecs_get_payload_format(codecs, num))) { + ast_copy_pj_str(fmt_param, &fmtp.fmt_param, sizeof(fmt_param)); + ast_format_sdp_parse(format, fmt_param); + } + } + } +} + +static int set_caps(struct ast_sip_session *session, struct ast_sip_session_media *session_media, + const struct pjmedia_sdp_media *stream) +{ + RAII_VAR(struct ast_format_cap *, caps, NULL, ast_format_cap_destroy); + RAII_VAR(struct ast_format_cap *, peer, NULL, ast_format_cap_destroy); + RAII_VAR(struct ast_format_cap *, joint, NULL, ast_format_cap_destroy); + enum ast_format_type media_type = stream_to_media_type(session_media->stream_type); + struct ast_rtp_codecs codecs; + struct ast_format fmt; + int fmts = 0; + int direct_media_enabled = !ast_sockaddr_isnull(&session_media->direct_media_addr) && + !ast_format_cap_is_empty(session->direct_media_cap); + + if (!(caps = ast_format_cap_alloc_nolock()) || + !(peer = ast_format_cap_alloc_nolock())) { + ast_log(LOG_ERROR, "Failed to allocate %s capabilities\n", session_media->stream_type); + return -1; + } + + /* get the endpoint capabilities */ + if (direct_media_enabled) { + ast_format_cap_joint_copy(session->endpoint->codecs, session->direct_media_cap, caps); + } else { + ast_format_cap_copy(caps, session->endpoint->codecs); + } + format_cap_only_type(caps, media_type); + + /* get the capabilities on the peer */ + get_codecs(session, stream, &codecs); + ast_rtp_codecs_payload_formats(&codecs, peer, &fmts); + + /* get the joint capabilities between peer and endpoint */ + if (!(joint = ast_format_cap_joint(caps, peer))) { + char usbuf[64], thembuf[64]; + + ast_rtp_codecs_payloads_destroy(&codecs); + + ast_getformatname_multiple(usbuf, sizeof(usbuf), caps); + ast_getformatname_multiple(thembuf, sizeof(thembuf), peer); + ast_log(LOG_WARNING, "No joint capabilities between our configuration(%s) and incoming SDP(%s)\n", usbuf, thembuf); + return -1; + } + + ast_rtp_codecs_payloads_copy(&codecs, ast_rtp_instance_get_codecs(session_media->rtp), + session_media->rtp); + + ast_format_cap_copy(caps, session->req_caps); + ast_format_cap_remove_bytype(caps, media_type); + ast_format_cap_append(caps, joint); + ast_format_cap_append(session->req_caps, caps); + + if (session->channel) { + ast_format_cap_copy(caps, ast_channel_nativeformats(session->channel)); + ast_format_cap_remove_bytype(caps, media_type); + ast_format_cap_append(caps, joint); + + /* Apply the new formats to the channel, potentially changing read/write formats while doing so */ + ast_format_cap_append(ast_channel_nativeformats(session->channel), caps); + ast_codec_choose(&session->endpoint->prefs, caps, 0, &fmt); + ast_format_copy(ast_channel_rawwriteformat(session->channel), &fmt); + ast_format_copy(ast_channel_rawreadformat(session->channel), &fmt); + ast_set_read_format(session->channel, ast_channel_readformat(session->channel)); + ast_set_write_format(session->channel, ast_channel_writeformat(session->channel)); + } + + ast_rtp_codecs_payloads_destroy(&codecs); + return 1; +} + +static pjmedia_sdp_attr* generate_rtpmap_attr(pjmedia_sdp_media *media, pj_pool_t *pool, int rtp_code, + int asterisk_format, struct ast_format *format, int code) +{ + pjmedia_sdp_rtpmap rtpmap; + pjmedia_sdp_attr *attr = NULL; + char tmp[64]; + + snprintf(tmp, sizeof(tmp), "%d", rtp_code); + pj_strdup2(pool, &media->desc.fmt[media->desc.fmt_count++], tmp); + rtpmap.pt = media->desc.fmt[media->desc.fmt_count - 1]; + rtpmap.clock_rate = ast_rtp_lookup_sample_rate2(asterisk_format, format, code); + pj_strdup2(pool, &rtpmap.enc_name, ast_rtp_lookup_mime_subtype2(asterisk_format, format, code, 0)); + rtpmap.param.slen = 0; + + pjmedia_sdp_rtpmap_to_attr(pool, &rtpmap, &attr); + + return attr; +} + +static pjmedia_sdp_attr* generate_fmtp_attr(pj_pool_t *pool, struct ast_format *format, int rtp_code) +{ + struct ast_str *fmtp0 = ast_str_alloca(256); + pj_str_t fmtp1; + pjmedia_sdp_attr *attr = NULL; + char *tmp; + + ast_format_sdp_generate(format, rtp_code, &fmtp0); + if (ast_str_strlen(fmtp0)) { + tmp = ast_str_buffer(fmtp0) + ast_str_strlen(fmtp0) - 1; + /* remove any carriage return line feeds */ + while (*tmp == '\r' || *tmp == '\n') --tmp; + *++tmp = '\0'; + /* ast...generate gives us everything, just need value */ + tmp = strchr(ast_str_buffer(fmtp0), ':'); + if (tmp && tmp + 1) { + fmtp1 = pj_str(tmp + 1); + } else { + fmtp1 = pj_str(ast_str_buffer(fmtp0)); + } + attr = pjmedia_sdp_attr_create(pool, "fmtp", &fmtp1); + } + return attr; +} + +/*! \brief Function which adds ICE attributes to a media stream */ +static void add_ice_to_stream(struct ast_sip_session *session, struct ast_sip_session_media *session_media, pj_pool_t *pool, pjmedia_sdp_media *media) +{ + struct ast_rtp_engine_ice *ice; + struct ao2_container *candidates; + const char *username, *password; + pj_str_t stmp; + pjmedia_sdp_attr *attr; + struct ao2_iterator it_candidates; + struct ast_rtp_engine_ice_candidate *candidate; + + if (!session->endpoint->ice_support || !(ice = ast_rtp_instance_get_ice(session_media->rtp)) || + !(candidates = ice->get_local_candidates(session_media->rtp))) { + return; + } + + if ((username = ice->get_ufrag(session_media->rtp))) { + attr = pjmedia_sdp_attr_create(pool, "ice-ufrag", pj_cstr(&stmp, username)); + media->attr[media->attr_count++] = attr; + } + + if ((password = ice->get_password(session_media->rtp))) { + attr = pjmedia_sdp_attr_create(pool, "ice-pwd", pj_cstr(&stmp, password)); + media->attr[media->attr_count++] = attr; + } + + it_candidates = ao2_iterator_init(candidates, 0); + for (; (candidate = ao2_iterator_next(&it_candidates)); ao2_ref(candidate, -1)) { + struct ast_str *attr_candidate = ast_str_create(128); + + ast_str_set(&attr_candidate, -1, "%s %d %s %d %s ", candidate->foundation, candidate->id, candidate->transport, + candidate->priority, ast_sockaddr_stringify_host(&candidate->address)); + ast_str_append(&attr_candidate, -1, "%s typ ", ast_sockaddr_stringify_port(&candidate->address)); + + switch (candidate->type) { + case AST_RTP_ICE_CANDIDATE_TYPE_HOST: + ast_str_append(&attr_candidate, -1, "host"); + break; + case AST_RTP_ICE_CANDIDATE_TYPE_SRFLX: + ast_str_append(&attr_candidate, -1, "srflx"); + break; + case AST_RTP_ICE_CANDIDATE_TYPE_RELAYED: + ast_str_append(&attr_candidate, -1, "relay"); + break; + } + + if (!ast_sockaddr_isnull(&candidate->relay_address)) { + ast_str_append(&attr_candidate, -1, " raddr %s rport ", ast_sockaddr_stringify_host(&candidate->relay_address)); + ast_str_append(&attr_candidate, -1, " %s", ast_sockaddr_stringify_port(&candidate->relay_address)); + } + + attr = pjmedia_sdp_attr_create(pool, "candidate", pj_cstr(&stmp, ast_str_buffer(attr_candidate))); + media->attr[media->attr_count++] = attr; + + ast_free(attr_candidate); + } + + ao2_iterator_destroy(&it_candidates); +} + +/*! \brief Function which processes ICE attributes in an audio stream */ +static void process_ice_attributes(struct ast_sip_session *session, struct ast_sip_session_media *session_media, + const struct pjmedia_sdp_session *remote, const struct pjmedia_sdp_media *remote_stream) +{ + struct ast_rtp_engine_ice *ice; + const pjmedia_sdp_attr *attr; + char attr_value[256]; + unsigned int attr_i; + + /* If ICE support is not enabled or available exit early */ + if (!session->endpoint->ice_support || !(ice = ast_rtp_instance_get_ice(session_media->rtp))) { + return; + } + + if ((attr = pjmedia_sdp_media_find_attr2(remote_stream, "ice-ufrag", NULL))) { + ast_copy_pj_str(attr_value, (pj_str_t*)&attr->value, sizeof(attr_value)); + ice->set_authentication(session_media->rtp, attr_value, NULL); + } + + if ((attr = pjmedia_sdp_media_find_attr2(remote_stream, "ice-pwd", NULL))) { + ast_copy_pj_str(attr_value, (pj_str_t*)&attr->value, sizeof(attr_value)); + ice->set_authentication(session_media->rtp, NULL, attr_value); + } + + if (pjmedia_sdp_media_find_attr2(remote_stream, "ice-lite", NULL)) { + ice->ice_lite(session_media->rtp); + } + + /* Find all of the candidates */ + for (attr_i = 0; attr_i < remote_stream->attr_count; ++attr_i) { + char foundation[32], transport[32], address[PJ_INET6_ADDRSTRLEN + 1], cand_type[6], relay_address[PJ_INET6_ADDRSTRLEN + 1] = ""; + int port, relay_port = 0; + struct ast_rtp_engine_ice_candidate candidate = { 0, }; + + attr = remote_stream->attr[attr_i]; + + /* If this is not a candidate line skip it */ + if (pj_strcmp2(&attr->name, "candidate")) { + continue; + } + + ast_copy_pj_str(attr_value, (pj_str_t*)&attr->value, sizeof(attr_value)); + + if (sscanf(attr_value, "%31s %30u %31s %30u %46s %30u typ %5s %*s %23s %*s %30u", foundation, &candidate.id, transport, + &candidate.priority, address, &port, cand_type, relay_address, &relay_port) < 7) { + /* Candidate did not parse properly */ + continue; + } + + candidate.foundation = foundation; + candidate.transport = transport; + + ast_sockaddr_parse(&candidate.address, address, PARSE_PORT_FORBID); + ast_sockaddr_set_port(&candidate.address, port); + + if (!strcasecmp(cand_type, "host")) { + candidate.type = AST_RTP_ICE_CANDIDATE_TYPE_HOST; + } else if (!strcasecmp(cand_type, "srflx")) { + candidate.type = AST_RTP_ICE_CANDIDATE_TYPE_SRFLX; + } else if (!strcasecmp(cand_type, "relay")) { + candidate.type = AST_RTP_ICE_CANDIDATE_TYPE_RELAYED; + } else { + continue; + } + + if (!ast_strlen_zero(relay_address)) { + ast_sockaddr_parse(&candidate.relay_address, relay_address, PARSE_PORT_FORBID); + } + + if (relay_port) { + ast_sockaddr_set_port(&candidate.relay_address, relay_port); + } + + ice->add_remote_candidate(session_media->rtp, &candidate); + } + + ice->start(session_media->rtp); +} + +static void apply_packetization(struct ast_sip_session *session, struct ast_sip_session_media *session_media, + const struct pjmedia_sdp_media *remote_stream) +{ + pjmedia_sdp_attr *attr; + pj_str_t value; + unsigned long framing; + int codec; + struct ast_codec_pref *pref = &ast_rtp_instance_get_codecs(session_media->rtp)->pref; + + /* Apply packetization if available and configured to do so */ + if (!session->endpoint->use_ptime || !(attr = pjmedia_sdp_media_find_attr2(remote_stream, "ptime", NULL))) { + return; + } + + value = attr->value; + framing = pj_strtoul(pj_strltrim(&value)); + + for (codec = 0; codec < AST_RTP_MAX_PT; codec++) { + struct ast_rtp_payload_type format = ast_rtp_codecs_payload_lookup(ast_rtp_instance_get_codecs( + session_media->rtp), codec); + + if (!format.asterisk_format) { + continue; + } + + ast_codec_pref_setsize(pref, &format.format, framing); + } + + ast_rtp_codecs_packetization_set(ast_rtp_instance_get_codecs(session_media->rtp), + session_media->rtp, pref); +} + +/*! \brief Function which negotiates an incoming media stream */ +static int negotiate_incoming_sdp_stream(struct ast_sip_session *session, struct ast_sip_session_media *session_media, + const struct pjmedia_sdp_session *sdp, const struct pjmedia_sdp_media *stream) +{ + char host[NI_MAXHOST]; + RAII_VAR(struct ast_sockaddr *, addrs, NULL, ast_free_ptr); + enum ast_format_type media_type = stream_to_media_type(session_media->stream_type); + + /* If no type formats have been configured reject this stream */ + if (!ast_format_cap_has_type(session->endpoint->codecs, media_type)) { + return 0; + } + + ast_copy_pj_str(host, stream->conn ? &stream->conn->addr : &sdp->conn->addr, sizeof(host)); + + /* Ensure that the address provided is valid */ + if (ast_sockaddr_resolve(&addrs, host, PARSE_PORT_FORBID, AST_AF_UNSPEC) <= 0) { + /* The provided host was actually invalid so we error out this negotiation */ + return -1; + } + + /* Using the connection information create an appropriate RTP instance */ + if (!session_media->rtp && create_rtp(session, session_media, ast_sockaddr_is_ipv6(addrs))) { + return -1; + } + + return set_caps(session, session_media, stream); +} + +/*! \brief Function which creates an outgoing stream */ +static int create_outgoing_sdp_stream(struct ast_sip_session *session, struct ast_sip_session_media *session_media, + struct pjmedia_sdp_session *sdp) +{ + pj_pool_t *pool = session->inv_session->pool_prov; + static const pj_str_t STR_IN = { "IN", 2 }; + static const pj_str_t STR_IP4 = { "IP4", 3}; + static const pj_str_t STR_IP6 = { "IP6", 3}; + static const pj_str_t STR_RTP_AVP = { "RTP/AVP", 7 }; + static const pj_str_t STR_SENDRECV = { "sendrecv", 8 }; + pjmedia_sdp_media *media; + char hostip[PJ_INET6_ADDRSTRLEN+2]; + struct ast_sockaddr addr; + char tmp[512]; + pj_str_t stmp; + pjmedia_sdp_attr *attr; + int index = 0, min_packet_size = 0, noncodec = (session->endpoint->dtmf == AST_SIP_DTMF_RFC_4733) ? AST_RTP_DTMF : 0; + int rtp_code; + struct ast_format format; + struct ast_format compat_format; + RAII_VAR(struct ast_format_cap *, caps, NULL, ast_format_cap_destroy); + enum ast_format_type media_type = stream_to_media_type(session_media->stream_type); + + int direct_media_enabled = !ast_sockaddr_isnull(&session_media->direct_media_addr) && + !ast_format_cap_is_empty(session->direct_media_cap); + + if (!ast_format_cap_has_type(session->endpoint->codecs, media_type)) { + /* If no type formats are configured don't add a stream */ + return 0; + } else if (!session_media->rtp && create_rtp(session, session_media, session->endpoint->rtp_ipv6)) { + return -1; + } + + if (!(media = pj_pool_zalloc(pool, sizeof(struct pjmedia_sdp_media))) || + !(media->conn = pj_pool_zalloc(pool, sizeof(struct pjmedia_sdp_conn)))) { + return -1; + } + + /* TODO: This should eventually support SRTP */ + media->desc.media = pj_str(session_media->stream_type); + media->desc.transport = STR_RTP_AVP; + + /* Add connection level details */ + if (direct_media_enabled) { + ast_copy_string(hostip, ast_sockaddr_stringify_fmt(&session_media->direct_media_addr, AST_SOCKADDR_STR_ADDR), sizeof(hostip)); + } else if (ast_strlen_zero(session->endpoint->external_media_address)) { + pj_sockaddr localaddr; + + if (pj_gethostip(session->endpoint->rtp_ipv6 ? pj_AF_INET6() : pj_AF_INET(), &localaddr)) { + return -1; + } + pj_sockaddr_print(&localaddr, hostip, sizeof(hostip), 2); + } else { + ast_copy_string(hostip, session->endpoint->external_media_address, sizeof(hostip)); + } + + media->conn->net_type = STR_IN; + media->conn->addr_type = session->endpoint->rtp_ipv6 ? STR_IP6 : STR_IP4; + pj_strdup2(pool, &media->conn->addr, hostip); + ast_rtp_instance_get_local_address(session_media->rtp, &addr); + media->desc.port = direct_media_enabled ? ast_sockaddr_port(&session_media->direct_media_addr) : (pj_uint16_t) ast_sockaddr_port(&addr); + media->desc.port_count = 1; + + /* Add ICE attributes and candidates */ + add_ice_to_stream(session, session_media, pool, media); + + if (!(caps = ast_format_cap_alloc_nolock())) { + ast_log(LOG_ERROR, "Failed to allocate %s capabilities\n", session_media->stream_type); + return -1; + } + + if (direct_media_enabled) { + ast_format_cap_joint_copy(session->endpoint->codecs, session->direct_media_cap, caps); + } else if (ast_format_cap_is_empty(session->req_caps)) { + ast_format_cap_copy(caps, session->endpoint->codecs); + } else { + ast_format_cap_joint_copy(session->endpoint->codecs, session->req_caps, caps); + } + + for (index = 0; ast_codec_pref_index(&session->endpoint->prefs, index, &format); ++index) { + struct ast_codec_pref *pref = &ast_rtp_instance_get_codecs(session_media->rtp)->pref; + + if (AST_FORMAT_GET_TYPE(format.id) != media_type) { + continue; + } + + if (!ast_format_cap_get_compatible_format(caps, &format, &compat_format)) { + continue; + } + + if ((rtp_code = ast_rtp_codecs_payload_code(ast_rtp_instance_get_codecs(session_media->rtp), 1, &compat_format, 0)) == -1) { + return -1; + } + + if (!(attr = generate_rtpmap_attr(media, pool, rtp_code, 1, &compat_format, 0))) { + continue; + } + + media->attr[media->attr_count++] = attr; + + if ((attr = generate_fmtp_attr(pool, &compat_format, rtp_code))) { + media->attr[media->attr_count++] = attr; + } + + if (pref && media_type != AST_FORMAT_TYPE_VIDEO) { + struct ast_format_list fmt = ast_codec_pref_getsize(pref, &compat_format); + if (fmt.cur_ms && ((fmt.cur_ms < min_packet_size) || !min_packet_size)) { + min_packet_size = fmt.cur_ms; + } + } + } + + /* Add non-codec formats */ + if (media_type != AST_FORMAT_TYPE_VIDEO) { + for (index = 1LL; index <= AST_RTP_MAX; index <<= 1) { + if (!(noncodec & index) || (rtp_code = ast_rtp_codecs_payload_code(ast_rtp_instance_get_codecs(session_media->rtp), + 0, NULL, index)) == -1) { + continue; + } + + if (!(attr = generate_rtpmap_attr(media, pool, rtp_code, 0, NULL, index))) { + continue; + } + + media->attr[media->attr_count++] = attr; + + if (index == AST_RTP_DTMF) { + snprintf(tmp, sizeof(tmp), "%d 0-16", rtp_code); + attr = pjmedia_sdp_attr_create(pool, "fmtp", pj_cstr(&stmp, tmp)); + media->attr[media->attr_count++] = attr; + } + } + } + + /* If ptime is set add it as an attribute */ + if (min_packet_size) { + snprintf(tmp, sizeof(tmp), "%d", min_packet_size); + attr = pjmedia_sdp_attr_create(pool, "ptime", pj_cstr(&stmp, tmp)); + media->attr[media->attr_count++] = attr; + } + + /* Add the sendrecv attribute - we purposely don't keep track because pjmedia-sdp will automatically change our offer for us */ + attr = PJ_POOL_ZALLOC_T(pool, pjmedia_sdp_attr); + attr->name = STR_SENDRECV; + media->attr[media->attr_count++] = attr; + + /* Add the media stream to the SDP */ + sdp->media[sdp->media_count++] = media; + + return 1; +} + +static int apply_negotiated_sdp_stream(struct ast_sip_session *session, struct ast_sip_session_media *session_media, + const struct pjmedia_sdp_session *local, const struct pjmedia_sdp_media *local_stream, + const struct pjmedia_sdp_session *remote, const struct pjmedia_sdp_media *remote_stream) +{ + RAII_VAR(struct ast_sockaddr *, addrs, NULL, ast_free_ptr); + enum ast_format_type media_type = stream_to_media_type(session_media->stream_type); + char host[NI_MAXHOST]; + int fdno; + + if (!session->channel) { + return 1; + } + + /* Create an RTP instance if need be */ + if (!session_media->rtp && create_rtp(session, session_media, session->endpoint->rtp_ipv6)) { + return -1; + } + + ast_copy_pj_str(host, remote_stream->conn ? &remote_stream->conn->addr : &remote->conn->addr, sizeof(host)); + + /* Ensure that the address provided is valid */ + if (ast_sockaddr_resolve(&addrs, host, PARSE_PORT_FORBID, AST_AF_UNSPEC) <= 0) { + /* The provided host was actually invalid so we error out this negotiation */ + return -1; + } + + /* Apply connection information to the RTP instance */ + ast_sockaddr_set_port(addrs, remote_stream->desc.port); + ast_rtp_instance_set_remote_address(session_media->rtp, addrs); + + if (set_caps(session, session_media, local_stream) < 1) { + return -1; + } + + if (media_type == AST_FORMAT_TYPE_AUDIO) { + apply_packetization(session, session_media, remote_stream); + } + + if ((fdno = media_type_to_fdno(media_type)) < 0) { + return -1; + } + ast_channel_set_fd(session->channel, fdno, ast_rtp_instance_fd(session_media->rtp, 0)); + ast_channel_set_fd(session->channel, fdno + 1, ast_rtp_instance_fd(session_media->rtp, 1)); + + /* If ICE support is enabled find all the needed attributes */ + process_ice_attributes(session, session_media, remote, remote_stream); + + /* audio stream handles music on hold */ + if (media_type != AST_FORMAT_TYPE_AUDIO) { + return 1; + } + + /* Music on hold for audio streams only */ + if (session_media->held && + (!ast_sockaddr_isnull(addrs) || + !pjmedia_sdp_media_find_attr2(remote_stream, "sendonly", NULL))) { + /* The remote side has taken us off hold */ + ast_queue_control(session->channel, AST_CONTROL_UNHOLD); + ast_queue_frame(session->channel, &ast_null_frame); + session_media->held = 0; + } else if (ast_sockaddr_isnull(addrs) || + ast_sockaddr_is_any(addrs) || + pjmedia_sdp_media_find_attr2(remote_stream, "sendonly", NULL)) { + /* The remote side has put us on hold */ + ast_queue_control_data(session->channel, AST_CONTROL_HOLD, S_OR(session->endpoint->mohsuggest, NULL), + !ast_strlen_zero(session->endpoint->mohsuggest) ? strlen(session->endpoint->mohsuggest) + 1 : 0); + ast_rtp_instance_stop(session_media->rtp); + ast_queue_frame(session->channel, &ast_null_frame); + session_media->held = 1; + } else { + /* The remote side has not changed state, but make sure the instance is active */ + ast_rtp_instance_activate(session_media->rtp); + } + + return 1; +} + +/*! \brief Function which updates the media stream with external media address, if applicable */ +static void change_outgoing_sdp_stream_media_address(pjsip_tx_data *tdata, struct pjmedia_sdp_media *stream, struct ast_sip_transport *transport) +{ + char host[NI_MAXHOST]; + struct ast_sockaddr addr = { { 0, } }; + + ast_copy_pj_str(host, &stream->conn->addr, sizeof(host)); + ast_sockaddr_parse(&addr, host, PARSE_PORT_FORBID); + + /* Is the address within the SDP inside the same network? */ + if (ast_apply_ha(transport->localnet, &addr) == AST_SENSE_ALLOW) { + return; + } + + pj_strdup2(tdata->pool, &stream->conn->addr, transport->external_media_address); +} + +/*! \brief Function which destroys the RTP instance when session ends */ +static void stream_destroy(struct ast_sip_session_media *session_media) +{ + if (session_media->rtp) { + ast_rtp_instance_stop(session_media->rtp); + ast_rtp_instance_destroy(session_media->rtp); + } +} + +/*! \brief SDP handler for 'audio' media stream */ +static struct ast_sip_session_sdp_handler audio_sdp_handler = { + .id = STR_AUDIO, + .negotiate_incoming_sdp_stream = negotiate_incoming_sdp_stream, + .create_outgoing_sdp_stream = create_outgoing_sdp_stream, + .apply_negotiated_sdp_stream = apply_negotiated_sdp_stream, + .change_outgoing_sdp_stream_media_address = change_outgoing_sdp_stream_media_address, + .stream_destroy = stream_destroy, +}; + +/*! \brief SDP handler for 'video' media stream */ +static struct ast_sip_session_sdp_handler video_sdp_handler = { + .id = STR_VIDEO, + .negotiate_incoming_sdp_stream = negotiate_incoming_sdp_stream, + .create_outgoing_sdp_stream = create_outgoing_sdp_stream, + .apply_negotiated_sdp_stream = apply_negotiated_sdp_stream, + .change_outgoing_sdp_stream_media_address = change_outgoing_sdp_stream_media_address, + .stream_destroy = stream_destroy, +}; + +static int video_info_incoming_request(struct ast_sip_session *session, struct pjsip_rx_data *rdata) +{ + struct pjsip_transaction *tsx = pjsip_rdata_get_tsx(rdata); + pjsip_tx_data *tdata; + + if (pj_strcmp2(&rdata->msg_info.msg->body->content_type.type, "application") || + pj_strcmp2(&rdata->msg_info.msg->body->content_type.subtype, "media_control+xml")) { + + return 0; + } + + ast_queue_control(session->channel, AST_CONTROL_VIDUPDATE); + + if (pjsip_dlg_create_response(session->inv_session->dlg, rdata, 200, NULL, &tdata) == PJ_SUCCESS) { + pjsip_dlg_send_response(session->inv_session->dlg, tsx, tdata); + } + + return 0; +} + +static struct ast_sip_session_supplement video_info_supplement = { + .method = "INFO", + .incoming_request = video_info_incoming_request, +}; + +/*! \brief Unloads the sdp RTP/AVP module from Asterisk */ +static int unload_module(void) +{ + ast_sip_session_unregister_supplement(&video_info_supplement); + ast_sip_session_unregister_sdp_handler(&video_sdp_handler, STR_VIDEO); + ast_sip_session_unregister_sdp_handler(&audio_sdp_handler, STR_AUDIO); + + if (sched) { + ast_sched_context_destroy(sched); + } + + return 0; +} + +/*! + * \brief Load the module + * + * Module loading including tests for configuration or dependencies. + * This function can return AST_MODULE_LOAD_FAILURE, AST_MODULE_LOAD_DECLINE, + * or AST_MODULE_LOAD_SUCCESS. If a dependency or environment variable fails + * tests return AST_MODULE_LOAD_FAILURE. If the module can not load the + * configuration file or other non-critical problem return + * AST_MODULE_LOAD_DECLINE. On success return AST_MODULE_LOAD_SUCCESS. + */ +static int load_module(void) +{ + ast_sockaddr_parse(&address_ipv4, "0.0.0.0", 0); + ast_sockaddr_parse(&address_ipv6, "::", 0); + + if (!(sched = ast_sched_context_create())) { + ast_log(LOG_ERROR, "Unable to create scheduler context.\n"); + goto end; + } + + if (ast_sched_start_thread(sched)) { + ast_log(LOG_ERROR, "Unable to create scheduler context thread.\n"); + goto end; + } + + if (ast_sip_session_register_sdp_handler(&audio_sdp_handler, STR_AUDIO)) { + ast_log(LOG_ERROR, "Unable to register SDP handler for %s stream type\n", STR_AUDIO); + goto end; + } + + if (ast_sip_session_register_sdp_handler(&video_sdp_handler, STR_VIDEO)) { + ast_log(LOG_ERROR, "Unable to register SDP handler for %s stream type\n", STR_VIDEO); + goto end; + } + + ast_sip_session_register_supplement(&video_info_supplement); + + return AST_MODULE_LOAD_SUCCESS; +end: + unload_module(); + + return AST_MODULE_LOAD_FAILURE; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "SIP SDP RTP/AVP stream handler", + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_CHANNEL_DRIVER, + ); diff --git a/res/res_sip_session.c b/res/res_sip_session.c new file mode 100644 index 000000000..6a31d9448 --- /dev/null +++ b/res/res_sip_session.c @@ -0,0 +1,1799 @@ +/* +* Asterisk -- An open source telephony toolkit. +* +* Copyright (C) 2013, Digium, Inc. +* +* Mark Michelson <mmichelson@digium.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>pjproject</depend> + <depend>res_sip</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include <pjsip.h> +#include <pjsip_ua.h> +#include <pjlib.h> + +#include "asterisk/res_sip.h" +#include "asterisk/res_sip_session.h" +#include "asterisk/datastore.h" +#include "asterisk/module.h" +#include "asterisk/logger.h" +#include "asterisk/res_sip.h" +#include "asterisk/astobj2.h" +#include "asterisk/lock.h" +#include "asterisk/uuid.h" +#include "asterisk/pbx.h" +#include "asterisk/taskprocessor.h" +#include "asterisk/causes.h" + +#define SDP_HANDLER_BUCKETS 11 + +/* Some forward declarations */ +static void handle_incoming_request(struct ast_sip_session *session, pjsip_rx_data *rdata); +static void handle_incoming_response(struct ast_sip_session *session, pjsip_rx_data *rdata); +static int handle_incoming(struct ast_sip_session *session, pjsip_rx_data *rdata); +static void handle_outgoing_request(struct ast_sip_session *session, pjsip_tx_data *tdata); +static void handle_outgoing_response(struct ast_sip_session *session, pjsip_tx_data *tdata); +static void handle_outgoing(struct ast_sip_session *session, pjsip_tx_data *tdata); + +/*! \brief NAT hook for modifying outgoing messages with SDP */ +static struct ast_sip_nat_hook *nat_hook; + +/*! + * \brief Registered SDP stream handlers + * + * This container is keyed on stream types. Each + * object in the container is a linked list of + * handlers for the stream type. + */ +static struct ao2_container *sdp_handlers; + +/*! + * These are the objects in the sdp_handlers container + */ +struct sdp_handler_list { + /* The list of handlers to visit */ + AST_LIST_HEAD_NOLOCK(, ast_sip_session_sdp_handler) list; + /* The handlers in this list handle streams of this type */ + char stream_type[1]; +}; + +static struct pjmedia_sdp_session *create_local_sdp(pjsip_inv_session *inv, struct ast_sip_session *session, const pjmedia_sdp_session *offer); + +static int sdp_handler_list_hash(const void *obj, int flags) +{ + const struct sdp_handler_list *handler_list = obj; + const char *stream_type = flags & OBJ_KEY ? obj : handler_list->stream_type; + + return ast_str_hash(stream_type); +} + +static int sdp_handler_list_cmp(void *obj, void *arg, int flags) +{ + struct sdp_handler_list *handler_list1 = obj; + struct sdp_handler_list *handler_list2 = arg; + const char *stream_type2 = flags & OBJ_KEY ? arg : handler_list2->stream_type; + + return strcmp(handler_list1->stream_type, stream_type2) ? 0 : CMP_MATCH | CMP_STOP; +} + +static int session_media_hash(const void *obj, int flags) +{ + const struct ast_sip_session_media *session_media = obj; + const char *stream_type = flags & OBJ_KEY ? obj : session_media->stream_type; + + return ast_str_hash(stream_type); +} + +static int session_media_cmp(void *obj, void *arg, int flags) +{ + struct ast_sip_session_media *session_media1 = obj; + struct ast_sip_session_media *session_media2 = arg; + const char *stream_type2 = flags & OBJ_KEY ? arg : session_media2->stream_type; + + return strcmp(session_media1->stream_type, stream_type2) ? 0 : CMP_MATCH | CMP_STOP; +} + +int ast_sip_session_register_sdp_handler(struct ast_sip_session_sdp_handler *handler, const char *stream_type) +{ + RAII_VAR(struct sdp_handler_list *, handler_list, + ao2_find(sdp_handlers, stream_type, OBJ_KEY), ao2_cleanup); + SCOPED_AO2LOCK(lock, sdp_handlers); + + if (handler_list) { + struct ast_sip_session_sdp_handler *iter; + /* Check if this handler is already registered for this stream type */ + AST_LIST_TRAVERSE(&handler_list->list, iter, next) { + if (!strcmp(iter->id, handler->id)) { + ast_log(LOG_WARNING, "Handler '%s' already registered for stream type '%s'.\n", handler->id, stream_type); + return -1; + } + } + AST_LIST_INSERT_TAIL(&handler_list->list, handler, next); + ast_debug(1, "Registered SDP stream handler '%s' for stream type '%s'\n", handler->id, stream_type); + ast_module_ref(ast_module_info->self); + return 0; + } + + /* No stream of this type has been registered yet, so we need to create a new list */ + handler_list = ao2_alloc(sizeof(*handler_list) + strlen(stream_type), NULL); + if (!handler_list) { + return -1; + } + /* Safe use of strcpy */ + strcpy(handler_list->stream_type, stream_type); + AST_LIST_HEAD_INIT_NOLOCK(&handler_list->list); + AST_LIST_INSERT_TAIL(&handler_list->list, handler, next); + if (!ao2_link(sdp_handlers, handler_list)) { + return -1; + } + ast_debug(1, "Registered SDP stream handler '%s' for stream type '%s'\n", handler->id, stream_type); + ast_module_ref(ast_module_info->self); + return 0; +} + +static int remove_handler(void *obj, void *arg, void *data, int flags) +{ + struct sdp_handler_list *handler_list = obj; + struct ast_sip_session_sdp_handler *handler = data; + struct ast_sip_session_sdp_handler *iter; + const char *stream_type = arg; + + AST_LIST_TRAVERSE_SAFE_BEGIN(&handler_list->list, iter, next) { + if (!strcmp(iter->id, handler->id)) { + AST_LIST_REMOVE_CURRENT(next); + ast_debug(1, "Unregistered SDP stream handler '%s' for stream type '%s'\n", handler->id, stream_type); + ast_module_unref(ast_module_info->self); + } + } + AST_LIST_TRAVERSE_SAFE_END; + + if (AST_LIST_EMPTY(&handler_list->list)) { + ast_debug(3, "No more handlers exist for stream type '%s'\n", stream_type); + return CMP_MATCH; + } else { + return CMP_STOP; + } +} + +void ast_sip_session_unregister_sdp_handler(struct ast_sip_session_sdp_handler *handler, const char *stream_type) +{ + ao2_callback_data(sdp_handlers, OBJ_KEY | OBJ_UNLINK | OBJ_NODATA, remove_handler, (void *)stream_type, handler); +} + +static int validate_port_hash(const void *obj, int flags) +{ + const int *port = obj; + return *port; +} + +static int validate_port_cmp(void *obj, void *arg, int flags) +{ + int *port1 = obj; + int *port2 = arg; + + return *port1 == *port2 ? CMP_MATCH | CMP_STOP : 0; +} + +struct bundle_assoc { + int port; + char tag[1]; +}; + +static int bundle_assoc_hash(const void *obj, int flags) +{ + const struct bundle_assoc *assoc = obj; + const char *tag = flags & OBJ_KEY ? obj : assoc->tag; + + return ast_str_hash(tag); +} + +static int bundle_assoc_cmp(void *obj, void *arg, int flags) +{ + struct bundle_assoc *assoc1 = obj; + struct bundle_assoc *assoc2 = arg; + const char *tag2 = flags & OBJ_KEY ? arg : assoc2->tag; + + return strcmp(assoc1->tag, tag2) ? 0 : CMP_MATCH | CMP_STOP; +} + +/* return must be ast_freed */ +static pjmedia_sdp_attr *media_get_mid(pjmedia_sdp_media *media) +{ + pjmedia_sdp_attr *attr = pjmedia_sdp_media_find_attr2(media, "mid", NULL); + if (!attr) { + return NULL; + } + + return attr; +} + +static int get_bundle_port(const pjmedia_sdp_session *sdp, const char *mid) +{ + int i; + for (i = 0; i < sdp->media_count; ++i) { + pjmedia_sdp_attr *mid_attr = media_get_mid(sdp->media[i]); + if (mid_attr && !pj_strcmp2(&mid_attr->value, mid)) { + return sdp->media[i]->desc.port; + } + } + + return -1; +} + +static int validate_incoming_sdp(const pjmedia_sdp_session *sdp) +{ + int i; + RAII_VAR(struct ao2_container *, portlist, ao2_container_alloc(5, validate_port_hash, validate_port_cmp), ao2_cleanup); + RAII_VAR(struct ao2_container *, bundle_assoc_list, ao2_container_alloc(5, bundle_assoc_hash, bundle_assoc_cmp), ao2_cleanup); + + /* check for bundles (for websocket RTP multiplexing, there can be more than one) */ + for (i = 0; i < sdp->attr_count; ++i) { + char *bundle_list; + int bundle_port = 0; + if (pj_stricmp2(&sdp->attr[i]->name, "group")) { + continue; + } + + /* check to see if this group is a bundle */ + if (7 >= sdp->attr[i]->value.slen || pj_strnicmp2(&sdp->attr[i]->value, "bundle ", 7)) { + continue; + } + + bundle_list = ast_alloca(sdp->attr[i]->value.slen - 6); + strncpy(bundle_list, sdp->attr[i]->value.ptr + 7, sdp->attr[i]->value.slen - 7); + bundle_list[sdp->attr[i]->value.slen - 7] = '\0'; + while (bundle_list) { + char *item; + RAII_VAR(struct bundle_assoc *, assoc, NULL, ao2_cleanup); + item = strsep(&bundle_list, " ,"); + if (!bundle_port) { + RAII_VAR(int *, port, ao2_alloc(sizeof(int), NULL), ao2_cleanup); + RAII_VAR(int *, port_match, NULL, ao2_cleanup); + bundle_port = get_bundle_port(sdp, item); + if (bundle_port < 0) { + return -1; + } + port_match = ao2_find(portlist, &bundle_port, OBJ_KEY); + if (port_match) { + /* bundle port aready consumed by a different bundle */ + return -1; + } + *port = bundle_port; + ao2_link(portlist, port); + } + assoc = ao2_alloc(sizeof(*assoc) + strlen(item), NULL); + if (!assoc) { + return -1; + } + + /* safe use of strcpy */ + strcpy(assoc->tag, item); + assoc->port = bundle_port; + ao2_link(bundle_assoc_list, assoc); + } + } + + /* validate all streams */ + for (i = 0; i < sdp->media_count; ++i) { + RAII_VAR(int *, port, ao2_alloc(sizeof(int), NULL), ao2_cleanup); + RAII_VAR(int *, port_match, NULL, ao2_cleanup); + RAII_VAR(int *, bundle_match, NULL, ao2_cleanup); + *port = sdp->media[i]->desc.port; + port_match = ao2_find(portlist, port, OBJ_KEY); + if (port_match) { + RAII_VAR(struct bundle_assoc *, assoc, NULL, ao2_cleanup); + pjmedia_sdp_attr *mid = media_get_mid(sdp->media[i]); + char *mid_val; + + if (!mid) { + /* not part of a bundle */ + return -1; + } + + mid_val = ast_alloca(mid->value.slen + 1); + strncpy(mid_val, mid->value.ptr, mid->value.slen); + mid_val[mid->value.slen] = '\0'; + + assoc = ao2_find(bundle_assoc_list, mid_val, OBJ_KEY); + if (!assoc || assoc->port != *port) { + /* This port already exists elsewhere in the SDP + * and is not an appropriate bundle port, fail + * catastrophically */ + return -1; + } + } + ao2_link(portlist, port); + } + return 0; +} + +static int handle_incoming_sdp(struct ast_sip_session *session, const pjmedia_sdp_session *sdp) +{ + int i; + if (validate_incoming_sdp(sdp)) { + return -1; + } + + for (i = 0; i < sdp->media_count; ++i) { + /* See if there are registered handlers for this media stream type */ + char media[20]; + struct ast_sip_session_sdp_handler *handler; + RAII_VAR(struct sdp_handler_list *, handler_list, NULL, ao2_cleanup); + + /* We need a null-terminated version of the media string */ + ast_copy_pj_str(media, &sdp->media[i]->desc.media, sizeof(media)); + + handler_list = ao2_find(sdp_handlers, media, OBJ_KEY); + if (!handler_list) { + ast_debug(1, "No registered SDP handlers for media type '%s'\n", media); + continue; + } + AST_LIST_TRAVERSE(&handler_list->list, handler, next) { + int res; + RAII_VAR(struct ast_sip_session_media *, session_media, NULL, ao2_cleanup); + session_media = ao2_find(session->media, handler_list->stream_type, OBJ_KEY); + if (!session_media || session_media->handler) { + /* There is only one slot for this stream type and it has already been claimed + * so it will go unhandled */ + break; + } + res = handler->negotiate_incoming_sdp_stream(session, session_media, sdp, sdp->media[i]); + if (res < 0) { + /* Catastrophic failure. Abort! */ + return -1; + } + if (res > 0) { + /* Handled by this handler. Move to the next stream */ + session_media->handler = handler; + break; + } + } + } + return 0; +} + +struct handle_negotiated_sdp_cb { + struct ast_sip_session *session; + const pjmedia_sdp_session *local; + const pjmedia_sdp_session *remote; +}; + +static int handle_negotiated_sdp_session_media(void *obj, void *arg, int flags) +{ + struct ast_sip_session_media *session_media = obj; + struct handle_negotiated_sdp_cb *callback_data = arg; + struct ast_sip_session *session = callback_data->session; + const pjmedia_sdp_session *local = callback_data->local; + const pjmedia_sdp_session *remote = callback_data->remote; + int i; + + for (i = 0; i < local->media_count; ++i) { + /* See if there are registered handlers for this media stream type */ + char media[20]; + struct ast_sip_session_sdp_handler *handler; + RAII_VAR(struct sdp_handler_list *, handler_list, NULL, ao2_cleanup); + + /* We need a null-terminated version of the media string */ + ast_copy_pj_str(media, &local->media[i]->desc.media, sizeof(media)); + + /* stream type doesn't match the one we're looking to fill */ + if (strcasecmp(session_media->stream_type, media)) { + continue; + } + + handler = session_media->handler; + if (handler) { + int res = handler->apply_negotiated_sdp_stream(session, session_media, local, local->media[i], remote, remote->media[i]); + if (res >= 0) { + return CMP_MATCH; + } + return 0; + } + + handler_list = ao2_find(sdp_handlers, media, OBJ_KEY); + if (!handler_list) { + ast_debug(1, "No registered SDP handlers for media type '%s'\n", media); + continue; + } + AST_LIST_TRAVERSE(&handler_list->list, handler, next) { + int res = handler->apply_negotiated_sdp_stream(session, session_media, local, local->media[i], remote, remote->media[i]); + if (res < 0) { + /* Catastrophic failure. Abort! */ + return 0; + } + if (res > 0) { + /* Handled by this handler. Move to the next stream */ + session_media->handler = handler; + return CMP_MATCH; + } + } + } + return CMP_MATCH; +} + +static int handle_negotiated_sdp(struct ast_sip_session *session, const pjmedia_sdp_session *local, const pjmedia_sdp_session *remote) +{ + RAII_VAR(struct ao2_iterator *, successful, NULL, ao2_iterator_cleanup); + struct handle_negotiated_sdp_cb callback_data = { + .session = session, + .local = local, + .remote = remote, + }; + + successful = ao2_callback(session->media, OBJ_MULTIPLE, handle_negotiated_sdp_session_media, &callback_data); + if (successful && ao2_container_count(successful->c) == ao2_container_count(session->media)) { + /* Nothing experienced a catastrophic failure */ + return 0; + } + return -1; +} + +AST_RWLIST_HEAD_STATIC(session_supplements, ast_sip_session_supplement); + +int ast_sip_session_register_supplement(struct ast_sip_session_supplement *supplement) +{ + struct ast_sip_session_supplement *iter; + int inserted = 0; + SCOPED_LOCK(lock, &session_supplements, AST_RWLIST_WRLOCK, AST_RWLIST_UNLOCK); + + AST_RWLIST_TRAVERSE_SAFE_BEGIN(&session_supplements, iter, next) { + if (iter->priority > supplement->priority) { + AST_RWLIST_INSERT_BEFORE_CURRENT(supplement, next); + inserted = 1; + break; + } + } + AST_RWLIST_TRAVERSE_SAFE_END; + + if (!inserted) { + AST_RWLIST_INSERT_TAIL(&session_supplements, supplement, next); + } + ast_module_ref(ast_module_info->self); + return 0; +} + +void ast_sip_session_unregister_supplement(struct ast_sip_session_supplement *supplement) +{ + struct ast_sip_session_supplement *iter; + SCOPED_LOCK(lock, &session_supplements, AST_RWLIST_WRLOCK, AST_RWLIST_UNLOCK); + AST_RWLIST_TRAVERSE_SAFE_BEGIN(&session_supplements, iter, next) { + if (supplement == iter) { + AST_RWLIST_REMOVE_CURRENT(next); + ast_module_unref(ast_module_info->self); + break; + } + } + AST_RWLIST_TRAVERSE_SAFE_END; +} + +static struct ast_sip_session_supplement *supplement_dup(const struct ast_sip_session_supplement *src) +{ + struct ast_sip_session_supplement *dst = ast_calloc(1, sizeof(*dst)); + if (!dst) { + return NULL; + } + /* Will need to revisit if shallow copy becomes an issue */ + *dst = *src; + return dst; +} + +#define DATASTORE_BUCKETS 53 +#define MEDIA_BUCKETS 7 + +static void session_datastore_destroy(void *obj) +{ + struct ast_datastore *datastore = obj; + + /* Using the destroy function (if present) destroy the data */ + if (datastore->info->destroy != NULL && datastore->data != NULL) { + datastore->info->destroy(datastore->data); + datastore->data = NULL; + } + + ast_free((void *) datastore->uid); + datastore->uid = NULL; +} + +struct ast_datastore *ast_sip_session_alloc_datastore(const struct ast_datastore_info *info, const char *uid) +{ + RAII_VAR(struct ast_datastore *, datastore, NULL, ao2_cleanup); + const char *uid_ptr = uid; + + if (!info) { + return NULL; + } + + datastore = ao2_alloc(sizeof(*datastore), session_datastore_destroy); + if (!datastore) { + return NULL; + } + + datastore->info = info; + if (ast_strlen_zero(uid)) { + /* They didn't provide an ID so we'll provide one ourself */ + struct ast_uuid *uuid = ast_uuid_generate(); + char uuid_buf[AST_UUID_STR_LEN]; + if (!uuid) { + return NULL; + } + uid_ptr = ast_uuid_to_str(uuid, uuid_buf, sizeof(uuid_buf)); + ast_free(uuid); + } + + datastore->uid = ast_strdup(uid_ptr); + if (!datastore->uid) { + return NULL; + } + + ao2_ref(datastore, +1); + return datastore; +} + +int ast_sip_session_add_datastore(struct ast_sip_session *session, struct ast_datastore *datastore) +{ + ast_assert(datastore != NULL); + ast_assert(datastore->info != NULL); + ast_assert(ast_strlen_zero(datastore->uid) == 0); + + if (!ao2_link(session->datastores, datastore)) { + return -1; + } + return 0; +} + +struct ast_datastore *ast_sip_session_get_datastore(struct ast_sip_session *session, const char *name) +{ + return ao2_find(session->datastores, name, OBJ_KEY); +} + +void ast_sip_session_remove_datastore(struct ast_sip_session *session, const char *name) +{ + ao2_callback(session->datastores, OBJ_KEY | OBJ_UNLINK | OBJ_NODATA, NULL, (void *) name); +} + +int ast_sip_session_get_identity(struct pjsip_rx_data *rdata, struct ast_party_id *id) +{ + /* XXX STUB + * This is low-priority as far as getting SIP working is concerned, so this + * will be addressed later. + * + * The idea here will be that the rdata will be examined for headers such as + * P-Asserted-Identity, Remote-Party-ID, and From in order to determine Identity + * information. + * + * For reference, Asterisk SCF code does something very similar to this, except in + * C++ and using its version of the ast_party_id struct, so using it as a basis + * would be a smart idea here. + */ + return 0; +} + +/*! + * \brief Structure used for sending delayed requests + * + * Requests are typically delayed because the current transaction + * state of an INVITE. Once the pending INVITE transaction terminates, + * the delayed request will be sent + */ +struct ast_sip_session_delayed_request { + /*! Method of the request */ + char method[15]; + /*! Callback to call when the delayed request is created. */ + ast_sip_session_request_creation_cb on_request_creation; + /*! Callback to call when the delayed request receives a response */ + ast_sip_session_response_cb on_response; + /*! Request to send */ + pjsip_tx_data *tdata; + AST_LIST_ENTRY(ast_sip_session_delayed_request) next; +}; + +static struct ast_sip_session_delayed_request *delayed_request_alloc(const char *method, + ast_sip_session_request_creation_cb on_request_creation, + ast_sip_session_response_cb on_response, + pjsip_tx_data *tdata) +{ + struct ast_sip_session_delayed_request *delay = ast_calloc(1, sizeof(*delay)); + if (!delay) { + return NULL; + } + ast_copy_string(delay->method, method, sizeof(delay->method)); + delay->on_request_creation = on_request_creation; + delay->on_response = on_response; + delay->tdata = tdata; + return delay; +} + +static int send_delayed_request(struct ast_sip_session *session, struct ast_sip_session_delayed_request *delay) +{ + ast_debug(3, "Sending delayed %s request to %s\n", delay->method, ast_sorcery_object_get_id(session->endpoint)); + + if (delay->tdata) { + ast_sip_session_send_request_with_cb(session, delay->tdata, delay->on_response); + return 0; + } + + if (!strcmp(delay->method, "INVITE")) { + ast_sip_session_refresh(session, delay->on_request_creation, + delay->on_response, AST_SIP_SESSION_REFRESH_METHOD_INVITE, 1); + } else if (!strcmp(delay->method, "UPDATE")) { + ast_sip_session_refresh(session, delay->on_request_creation, + delay->on_response, AST_SIP_SESSION_REFRESH_METHOD_UPDATE, 1); + } else { + ast_log(LOG_WARNING, "Unexpected delayed %s request with no existing request structure\n", delay->method); + return -1; + } + return 0; +} + +static int queued_delayed_request_send(void *data) +{ + RAII_VAR(struct ast_sip_session *, session, data, ao2_cleanup); + RAII_VAR(struct ast_sip_session_delayed_request *, delay, NULL, ast_free_ptr); + + delay = AST_LIST_REMOVE_HEAD(&session->delayed_requests, next); + if (!delay) { + return 0; + } + + return send_delayed_request(session, delay); +} + +static void queue_delayed_request(struct ast_sip_session *session) +{ + if (AST_LIST_EMPTY(&session->delayed_requests)) { + /* No delayed request to send, so just return */ + return; + } + + ast_debug(3, "Queuing delayed request to run for %s\n", + ast_sorcery_object_get_id(session->endpoint)); + + ao2_ref(session, +1); + ast_sip_push_task(session->serializer, queued_delayed_request_send, session); +} + +static int delay_request(struct ast_sip_session *session, ast_sip_session_request_creation_cb on_request, + ast_sip_session_response_cb on_response, const char *method, pjsip_tx_data *tdata) +{ + struct ast_sip_session_delayed_request *delay = delayed_request_alloc(method, + on_request, on_response, tdata); + + if (!delay) { + return -1; + } + + AST_LIST_INSERT_TAIL(&session->delayed_requests, delay, next); + return 0; +} + +static pjmedia_sdp_session *generate_session_refresh_sdp(struct ast_sip_session *session) +{ + pjsip_inv_session *inv_session = session->inv_session; + const pjmedia_sdp_session *previous_sdp; + + if (pjmedia_sdp_neg_was_answer_remote(inv_session->neg)) { + pjmedia_sdp_neg_get_active_remote(inv_session->neg, &previous_sdp); + } else { + pjmedia_sdp_neg_get_active_local(inv_session->neg, &previous_sdp); + } + return create_local_sdp(inv_session, session, previous_sdp); +} + +int ast_sip_session_refresh(struct ast_sip_session *session, + ast_sip_session_request_creation_cb on_request_creation, ast_sip_session_response_cb on_response, + enum ast_sip_session_refresh_method method, int generate_new_sdp) +{ + pjsip_inv_session *inv_session = session->inv_session; + pjmedia_sdp_session *new_sdp = NULL; + pjsip_tx_data *tdata; + + if (inv_session->state == PJSIP_INV_STATE_DISCONNECTED) { + /* Don't try to do anything with a hung-up call */ + ast_debug(3, "Not sending reinvite to %s because of disconnected state...\n", + ast_sorcery_object_get_id(session->endpoint)); + return 0; + } + + if (inv_session->invite_tsx) { + /* We can't send a reinvite yet, so delay it */ + ast_debug(3, "Delaying sending reinvite to %s because of outstanding transaction...\n", + ast_sorcery_object_get_id(session->endpoint)); + return delay_request(session, on_request_creation, on_response, "INVITE", NULL); + } + + if (generate_new_sdp) { + new_sdp = generate_session_refresh_sdp(session); + if (!new_sdp) { + ast_log(LOG_ERROR, "Failed to generate session refresh SDP. Not sending session refresh\n"); + return -1; + } + } + + if (method == AST_SIP_SESSION_REFRESH_METHOD_INVITE) { + if (pjsip_inv_reinvite(inv_session, NULL, new_sdp, &tdata)) { + ast_log(LOG_WARNING, "Failed to create reinvite properly.\n"); + return -1; + } + } else if (pjsip_inv_update(inv_session, NULL, new_sdp, &tdata)) { + ast_log(LOG_WARNING, "Failed to create UPDATE properly.\n"); + return -1; + } + if (on_request_creation) { + if (on_request_creation(session, tdata)) { + return -1; + } + } + ast_sip_session_send_request_with_cb(session, tdata, on_response); + return 0; +} + +void ast_sip_session_send_response(struct ast_sip_session *session, pjsip_tx_data *tdata) +{ + handle_outgoing_response(session, tdata); + pjsip_inv_send_msg(session->inv_session, tdata); + return; +} + +static pj_status_t session_load(pjsip_endpoint *endpt); +static pj_status_t session_start(void); +static pj_status_t session_stop(void); +static pj_status_t session_unload(void); +static pj_bool_t session_on_rx_request(pjsip_rx_data *rdata); + +static pjsip_module session_module = { + .name = {"Session Module", 14}, + .priority = PJSIP_MOD_PRIORITY_APPLICATION, + .load = session_load, + .unload = session_unload, + .start = session_start, + .stop = session_stop, + .on_rx_request = session_on_rx_request, +}; + +void ast_sip_session_send_request_with_cb(struct ast_sip_session *session, pjsip_tx_data *tdata, + ast_sip_session_response_cb on_response) +{ + pjsip_inv_session *inv_session = session->inv_session; + + if (inv_session->state == PJSIP_INV_STATE_DISCONNECTED) { + /* Don't try to do anything with a hung-up call */ + return; + } + + tdata->mod_data[session_module.id] = on_response; + handle_outgoing_request(session, tdata); + pjsip_inv_send_msg(session->inv_session, tdata); + return; +} + +void ast_sip_session_send_request(struct ast_sip_session *session, pjsip_tx_data *tdata) +{ + ast_sip_session_send_request_with_cb(session, tdata, NULL); +} + +/*! + * \brief Called when the PJSIP core loads us + * + * Since we already have Asterisk's fine module load/unload framework + * in use, we don't need to do anything special here. + */ +static pj_status_t session_load(pjsip_endpoint *endpt) +{ + return PJ_SUCCESS; +} + +/*! + * \brief Called when the PJSIP core starts us + * + * Since we already have Asterisk's fine module load/unload framework + * in use, we don't need to do anything special here. + */ +static pj_status_t session_start(void) +{ + return PJ_SUCCESS; +} + +/*! + * \brief Called when the PJSIP core stops us + * + * Since we already have Asterisk's fine module load/unload framework + * in use, we don't need to do anything special here. + */ +static pj_status_t session_stop(void) +{ + return PJ_SUCCESS; +} + +/*! + * \brief Called when the PJSIP core unloads us + * + * Since we already have Asterisk's fine module load/unload framework + * in use, we don't need to do anything special here. + */ +static pj_status_t session_unload(void) +{ + return PJ_SUCCESS; +} + +static int datastore_hash(const void *obj, int flags) +{ + const struct ast_datastore *datastore = obj; + const char *uid = flags & OBJ_KEY ? obj : datastore->uid; + + ast_assert(uid != NULL); + + return ast_str_hash(uid); +} + +static int datastore_cmp(void *obj, void *arg, int flags) +{ + const struct ast_datastore *datastore1 = obj; + const struct ast_datastore *datastore2 = arg; + const char *uid2 = flags & OBJ_KEY ? arg : datastore2->uid; + + ast_assert(datastore1->uid != NULL); + ast_assert(uid2 != NULL); + + return strcmp(datastore1->uid, uid2) ? 0 : CMP_MATCH | CMP_STOP; +} + +static void session_media_dtor(void *obj) +{ + struct ast_sip_session_media *session_media = obj; + if (session_media->handler) { + session_media->handler->stream_destroy(session_media); + } +} + +static void session_destructor(void *obj) +{ + struct ast_sip_session *session = obj; + struct ast_sip_session_supplement *supplement; + struct ast_sip_session_delayed_request *delay; + + ast_debug(3, "Destroying SIP session with endpoint %s\n", + ast_sorcery_object_get_id(session->endpoint)); + + while ((supplement = AST_LIST_REMOVE_HEAD(&session->supplements, next))) { + if (supplement->session_destroy) { + supplement->session_destroy(session); + } + ast_free(supplement); + } + + ast_taskprocessor_unreference(session->serializer); + ao2_cleanup(session->datastores); + ao2_cleanup(session->media); + AST_LIST_HEAD_DESTROY(&session->supplements); + while ((delay = AST_LIST_REMOVE_HEAD(&session->delayed_requests, next))) { + ast_free(delay); + } + ast_party_id_free(&session->id); + ao2_cleanup(session->endpoint); + ast_format_cap_destroy(session->req_caps); +} + +static int add_supplements(struct ast_sip_session *session) +{ + struct ast_sip_session_supplement *iter; + SCOPED_LOCK(lock, &session_supplements, AST_RWLIST_RDLOCK, AST_RWLIST_UNLOCK); + + AST_RWLIST_TRAVERSE(&session_supplements, iter, next) { + struct ast_sip_session_supplement *copy = supplement_dup(iter); + if (!copy) { + return -1; + } + AST_LIST_INSERT_TAIL(&session->supplements, copy, next); + } + return 0; +} + +static int add_session_media(void *obj, void *arg, int flags) +{ + struct sdp_handler_list *handler_list = obj; + struct ast_sip_session * session = arg; + RAII_VAR(struct ast_sip_session_media *, session_media, NULL, ao2_cleanup); + session_media = ao2_alloc(sizeof(*session_media) + strlen(handler_list->stream_type), session_media_dtor); + if (!session_media) { + return CMP_STOP; + } + /* Safe use of strcpy */ + strcpy(session_media->stream_type, handler_list->stream_type); + ao2_link(session->media, session_media); + return 0; +} + +struct ast_sip_session *ast_sip_session_alloc(struct ast_sip_endpoint *endpoint, pjsip_inv_session *inv_session) +{ + RAII_VAR(struct ast_sip_session *, session, ao2_alloc(sizeof(*session), session_destructor), ao2_cleanup); + struct ast_sip_session_supplement *iter; + if (!session) { + return NULL; + } + AST_LIST_HEAD_INIT(&session->supplements); + session->datastores = ao2_container_alloc(DATASTORE_BUCKETS, datastore_hash, datastore_cmp); + if (!session->datastores) { + return NULL; + } + + session->media = ao2_container_alloc(MEDIA_BUCKETS, session_media_hash, session_media_cmp); + if (!session->media) { + return NULL; + } + /* fill session->media with available types */ + ao2_callback(sdp_handlers, OBJ_NODATA, add_session_media, session); + + session->serializer = ast_sip_create_serializer(); + if (!session->serializer) { + return NULL; + } + ast_sip_dialog_set_serializer(inv_session->dlg, session->serializer); + ast_sip_dialog_set_endpoint(inv_session->dlg, endpoint); + ao2_ref(endpoint, +1); + inv_session->mod_data[session_module.id] = session; + session->endpoint = endpoint; + session->inv_session = inv_session; + session->req_caps = ast_format_cap_alloc_nolock(); + + if (add_supplements(session)) { + return NULL; + } + AST_LIST_TRAVERSE(&session->supplements, iter, next) { + if (iter->session_begin) { + iter->session_begin(session); + } + } + session->direct_media_cap = ast_format_cap_alloc_nolock(); + AST_LIST_HEAD_INIT_NOLOCK(&session->delayed_requests); + ast_party_id_init(&session->id); + ao2_ref(session, +1); + return session; +} + +static int session_outbound_auth(pjsip_dialog *dlg, pjsip_tx_data *tdata, void *user_data) +{ + pjsip_inv_session *inv = pjsip_dlg_get_inv_session(dlg); + struct ast_sip_session *session = inv->mod_data[session_module.id]; + + if (inv->state < PJSIP_INV_STATE_CONFIRMED && tdata->msg->line.req.method.id == PJSIP_INVITE_METHOD) { + pjsip_inv_uac_restart(inv, PJ_TRUE); + } + ast_sip_session_send_request(session, tdata); + return 0; +} + +struct ast_sip_session *ast_sip_session_create_outgoing(struct ast_sip_endpoint *endpoint, const char *location, const char *request_user, struct ast_format_cap *req_caps) +{ + const char *uri = NULL; + RAII_VAR(struct ast_sip_contact *, contact, NULL, ao2_cleanup); + pjsip_timer_setting timer; + pjsip_dialog *dlg; + struct pjsip_inv_session *inv_session; + RAII_VAR(struct ast_sip_session *, session, NULL, ao2_cleanup); + pjmedia_sdp_session *offer; + + /* If no location has been provided use the AOR list from the endpoint itself */ + location = S_OR(location, endpoint->aors); + + contact = ast_sip_location_retrieve_contact_from_aor_list(location); + if (!contact || ast_strlen_zero(contact->uri)) { + uri = location; + } else { + uri = contact->uri; + } + + /* If we still have no URI to dial fail to create the session */ + if (ast_strlen_zero(uri)) { + return NULL; + } + + if (!(dlg = ast_sip_create_dialog(endpoint, uri, request_user))) { + return NULL; + } + + if (ast_sip_dialog_setup_outbound_authentication(dlg, endpoint, session_outbound_auth, NULL)) { + pjsip_dlg_terminate(dlg); + return NULL; + } + + if (pjsip_inv_create_uac(dlg, NULL, endpoint->extensions, &inv_session) != PJ_SUCCESS) { + pjsip_dlg_terminate(dlg); + return NULL; + } + + pjsip_timer_setting_default(&timer); + timer.min_se = endpoint->min_se; + timer.sess_expires = endpoint->sess_expires; + pjsip_timer_init_session(inv_session, &timer); + + if (!(session = ast_sip_session_alloc(endpoint, inv_session))) { + pjsip_inv_terminate(inv_session, 500, PJ_FALSE); + return NULL; + } + + ast_format_cap_copy(session->req_caps, req_caps); + if ((pjsip_dlg_add_usage(dlg, &session_module, NULL) != PJ_SUCCESS) || + !(offer = create_local_sdp(inv_session, session, NULL))) { + pjsip_inv_terminate(inv_session, 500, PJ_FALSE); + return NULL; + } + + pjsip_inv_set_local_sdp(inv_session, offer); + + ao2_ref(session, +1); + return session; +} + +enum sip_get_destination_result { + /*! The extension was successfully found */ + SIP_GET_DEST_EXTEN_FOUND, + /*! The extension specified in the RURI was not found */ + SIP_GET_DEST_EXTEN_NOT_FOUND, + /*! The extension specified in the RURI was a partial match */ + SIP_GET_DEST_EXTEN_PARTIAL, + /*! The RURI is of an unsupported scheme */ + SIP_GET_DEST_UNSUPPORTED_URI, +}; + +/*! + * \brief Determine where in the dialplan a call should go + * + * This uses the username in the request URI to try to match + * an extension in the endpoint's configured context in order + * to route the call. + * + * \param session The inbound SIP session + * \param rdata The SIP INVITE + */ +static enum sip_get_destination_result get_destination(struct ast_sip_session *session, pjsip_rx_data *rdata) +{ + pjsip_uri *ruri = rdata->msg_info.msg->line.req.uri; + pjsip_sip_uri *sip_ruri; + if (!PJSIP_URI_SCHEME_IS_SIP(ruri) && !PJSIP_URI_SCHEME_IS_SIPS(ruri)) { + return SIP_GET_DEST_UNSUPPORTED_URI; + } + sip_ruri = pjsip_uri_get_uri(ruri); + ast_copy_pj_str(session->exten, &sip_ruri->user, sizeof(session->exten)); + if (ast_exists_extension(NULL, session->endpoint->context, session->exten, 1, NULL)) { + return SIP_GET_DEST_EXTEN_FOUND; + } + /* XXX In reality, we'll likely have further options so that partial matches + * can be indicated here, but for getting something up and running, we're going + * to return a "not exists" error here. + */ + return SIP_GET_DEST_EXTEN_NOT_FOUND; +} + +static pjsip_inv_session *pre_session_setup(pjsip_rx_data *rdata, const struct ast_sip_endpoint *endpoint) +{ + pjsip_tx_data *tdata; + pjsip_dialog *dlg; + pjsip_inv_session *inv_session; + unsigned int options = endpoint->extensions; + + if (pjsip_inv_verify_request(rdata, &options, NULL, NULL, ast_sip_get_pjsip_endpoint(), &tdata) != PJ_SUCCESS) { + if (tdata) { + pjsip_endpt_send_response2(ast_sip_get_pjsip_endpoint(), rdata, tdata, NULL, NULL); + } else { + pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 500, NULL, NULL, NULL); + } + return NULL; + } + if (pjsip_dlg_create_uas(pjsip_ua_instance(), rdata, NULL, &dlg) != PJ_SUCCESS) { + pjsip_endpt_send_response2(ast_sip_get_pjsip_endpoint(), rdata, tdata, NULL, NULL); + return NULL; + } + if (pjsip_inv_create_uas(dlg, rdata, NULL, 0, &inv_session) != PJ_SUCCESS) { + pjsip_endpt_send_response2(ast_sip_get_pjsip_endpoint(), rdata, tdata, NULL, NULL); + pjsip_dlg_terminate(dlg); + return NULL; + } + if (pjsip_dlg_add_usage(dlg, &session_module, NULL) != PJ_SUCCESS) { + if (pjsip_inv_initial_answer(inv_session, rdata, 500, NULL, NULL, &tdata) != PJ_SUCCESS) { + pjsip_inv_terminate(inv_session, 500, PJ_FALSE); + } + pjsip_inv_send_msg(inv_session, tdata); + return NULL; + } + return inv_session; +} + +static void handle_new_invite_request(pjsip_rx_data *rdata) +{ + RAII_VAR(struct ast_sip_endpoint *, endpoint, + ast_pjsip_rdata_get_endpoint(rdata), ao2_cleanup); + pjsip_tx_data *tdata = NULL; + pjsip_inv_session *inv_session = NULL; + struct ast_sip_session *session = NULL; + pjsip_timer_setting timer; + pjsip_rdata_sdp_info *sdp_info; + pjmedia_sdp_session *local = NULL; + + ast_assert(endpoint != NULL); + + inv_session = pre_session_setup(rdata, endpoint); + if (!inv_session) { + /* pre_session_setup() returns a response on failure */ + return; + } + + session = ast_sip_session_alloc(endpoint, inv_session); + if (!session) { + if (pjsip_inv_initial_answer(inv_session, rdata, 500, NULL, NULL, &tdata) == PJ_SUCCESS) { + pjsip_inv_terminate(inv_session, 500, PJ_FALSE); + } else { + pjsip_inv_send_msg(inv_session, tdata); + } + return; + } + + /* From this point on, any calls to pjsip_inv_terminate have the last argument as PJ_TRUE + * so that we will be notified so we can destroy the session properly + */ + + switch (get_destination(session, rdata)) { + case SIP_GET_DEST_EXTEN_FOUND: + /* Things worked. Keep going */ + break; + case SIP_GET_DEST_UNSUPPORTED_URI: + if (pjsip_inv_initial_answer(inv_session, rdata, 416, NULL, NULL, &tdata) == PJ_SUCCESS) { + ast_sip_session_send_response(session, tdata); + } else { + pjsip_inv_terminate(inv_session, 416, PJ_TRUE); + } + return; + case SIP_GET_DEST_EXTEN_NOT_FOUND: + case SIP_GET_DEST_EXTEN_PARTIAL: + default: + if (pjsip_inv_initial_answer(inv_session, rdata, 404, NULL, NULL, &tdata) == PJ_SUCCESS) { + ast_sip_session_send_response(session, tdata); + } else { + pjsip_inv_terminate(inv_session, 404, PJ_TRUE); + } + return; + }; + + if ((sdp_info = pjsip_rdata_get_sdp_info(rdata)) && (sdp_info->sdp_err == PJ_SUCCESS) && sdp_info->sdp) { + if (handle_incoming_sdp(session, sdp_info->sdp)) { + if (pjsip_inv_initial_answer(inv_session, rdata, 488, NULL, NULL, &tdata) == PJ_SUCCESS) { + ast_sip_session_send_response(session, tdata); + } else { + pjsip_inv_terminate(inv_session, 488, PJ_TRUE); + } + return; + } + /* We are creating a local SDP which is an answer to their offer */ + local = create_local_sdp(inv_session, session, sdp_info->sdp); + } else { + /* We are creating a local SDP which is an offer */ + local = create_local_sdp(inv_session, session, NULL); + } + + /* If we were unable to create a local SDP terminate the session early, it won't go anywhere */ + if (!local) { + if (pjsip_inv_initial_answer(inv_session, rdata, 500, NULL, NULL, &tdata) == PJ_SUCCESS) { + ast_sip_session_send_response(session, tdata); + } else { + pjsip_inv_terminate(inv_session, 500, PJ_TRUE); + } + return; + } else { + pjsip_inv_set_local_sdp(inv_session, local); + } + + pjsip_timer_setting_default(&timer); + timer.min_se = endpoint->min_se; + timer.sess_expires = endpoint->sess_expires; + pjsip_timer_init_session(inv_session, &timer); + + /* At this point, we've verified what we can, so let's go ahead and send a 100 Trying out */ + if (pjsip_inv_initial_answer(inv_session, rdata, 100, NULL, NULL, &tdata) != PJ_SUCCESS) { + pjsip_inv_terminate(inv_session, 500, PJ_TRUE); + return; + } + ast_sip_session_send_response(session, tdata); + + handle_incoming_request(session, rdata); +} + +static int has_supplement(struct ast_sip_session *session, pjsip_rx_data *rdata) +{ + struct ast_sip_session_supplement *supplement; + struct pjsip_method *method = &rdata->msg_info.msg->line.req.method; + + if (!session) { + return PJ_FALSE; + } + + AST_LIST_TRAVERSE(&session->supplements, supplement, next) { + if (!supplement->method || !pj_strcmp2(&method->name, supplement->method)) { + return PJ_TRUE; + } + } + return PJ_FALSE; +} +/*! + * \brief Called when a new SIP request comes into PJSIP + * + * This function is called under two circumstances + * 1) An out-of-dialog request is received by PJSIP + * 2) An in-dialog request that the inv_session layer does not + * handle is received (such as an in-dialog INFO) + * + * In all cases, there is very little we actually do in this function + * 1) For requests we don't handle, we return PJ_FALSE + * 2) For new INVITEs, throw the work into the SIP threadpool to be done + * there to free up the thread(s) handling incoming requests + * 3) For in-dialog requests we handle, we defer handling them until the + * on_inv_state_change() callback instead (where we will end up putting + * them into the threadpool). + */ +static pj_bool_t session_on_rx_request(pjsip_rx_data *rdata) +{ + pj_status_t handled = PJ_FALSE; + pjsip_dialog *dlg = pjsip_rdata_get_dlg(rdata); + pjsip_inv_session *inv_session; + + switch (rdata->msg_info.msg->line.req.method.id) { + case PJSIP_INVITE_METHOD: + if (dlg) { + ast_log(LOG_WARNING, "on_rx_request called for INVITE in mid-dialog?\n"); + break; + } + handled = PJ_TRUE; + handle_new_invite_request(rdata); + break; + default: + /* Handle other in-dialog methods if their supplements have been registered */ + handled = dlg && (inv_session = pjsip_dlg_get_inv_session(dlg)) && + has_supplement(inv_session->mod_data[session_module.id], rdata); + break; + } + + return handled; +} + +struct reschedule_reinvite_data { + struct ast_sip_session *session; + struct ast_sip_session_delayed_request *delay; +}; + +static struct reschedule_reinvite_data *reschedule_reinvite_data_alloc( + struct ast_sip_session *session, struct ast_sip_session_delayed_request *delay) +{ + struct reschedule_reinvite_data *rrd = ast_malloc(sizeof(*rrd)); + if (!rrd) { + return NULL; + } + ao2_ref(session, +1); + rrd->session = session; + rrd->delay = delay; + return rrd; +} + +static void reschedule_reinvite_data_destroy(struct reschedule_reinvite_data *rrd) +{ + ao2_cleanup(rrd->session); + ast_free(rrd->delay); + ast_free(rrd); +} + +static int really_resend_reinvite(void *data) +{ + RAII_VAR(struct reschedule_reinvite_data *, rrd, data, reschedule_reinvite_data_destroy); + + return send_delayed_request(rrd->session, rrd->delay); +} + +static void resend_reinvite(pj_timer_heap_t *timer, pj_timer_entry *entry) +{ + struct reschedule_reinvite_data *rrd = entry->user_data; + + ast_sip_push_task(rrd->session->serializer, really_resend_reinvite, entry->user_data); +} + +static void reschedule_reinvite(struct ast_sip_session *session, ast_sip_session_response_cb on_response, pjsip_tx_data *tdata) +{ + struct ast_sip_session_delayed_request *delay = delayed_request_alloc("INVITE", + NULL, on_response, tdata); + pjsip_inv_session *inv = session->inv_session; + struct reschedule_reinvite_data *rrd = reschedule_reinvite_data_alloc(session, delay); + pj_time_val tv; + + if (!rrd || !delay) { + return; + } + + tv.sec = 0; + if (inv->role == PJSIP_ROLE_UAC) { + tv.msec = 2100 + ast_random() % 2000; + } else { + tv.msec = ast_random() % 2000; + } + + pj_timer_entry_init(&session->rescheduled_reinvite, 0, rrd, resend_reinvite); + + pjsip_endpt_schedule_timer(ast_sip_get_pjsip_endpoint(), &session->rescheduled_reinvite, &tv); +} + +static void __print_debug_details(const char *function, pjsip_inv_session *inv, pjsip_transaction *tsx, pjsip_event *e) +{ + struct ast_sip_session *session; + ast_debug(5, "Function %s called on event %s\n", function, pjsip_event_str(e->type)); + if (!inv) { + ast_debug(5, "Transaction %p does not belong to an inv_session?\n", tsx); + ast_debug(5, "The transaction state is %s\n", pjsip_tsx_state_str(tsx->state)); + return; + } + session = inv->mod_data[session_module.id]; + if (!session) { + ast_debug(5, "inv_session %p has no ast session\n", inv); + } else { + ast_debug(5, "The state change pertains to the session with %s\n", + ast_sorcery_object_get_id(session->endpoint)); + } + if (inv->invite_tsx) { + ast_debug(5, "The inv session still has an invite_tsx (%p)\n", inv->invite_tsx); + } else { + ast_debug(5, "The inv session does NOT have an invite_tsx\n"); + } + if (tsx) { + ast_debug(5, "The transaction involved in this state change is %p\n", tsx); + ast_debug(5, "The current transaction state is %s\n", pjsip_tsx_state_str(tsx->state)); + ast_debug(5, "The transaction state change event is %s\n", pjsip_event_str(e->body.tsx_state.type)); + } else { + ast_debug(5, "There is no transaction involved in this state change\n"); + } + ast_debug(5, "The current inv state is %s\n", pjsip_inv_state_name(inv->state)); +} + +#define print_debug_details(inv, tsx, e) __print_debug_details(__PRETTY_FUNCTION__, (inv), (tsx), (e)) + +static void handle_incoming_request(struct ast_sip_session *session, pjsip_rx_data *rdata) +{ + struct ast_sip_session_supplement *supplement; + struct pjsip_request_line req = rdata->msg_info.msg->line.req; + + ast_debug(3, "Method is %.*s\n", (int) pj_strlen(&req.method.name), pj_strbuf(&req.method.name)); + AST_LIST_TRAVERSE(&session->supplements, supplement, next) { + if (supplement->incoming_request && ( + !supplement->method || !pj_strcmp2(&req.method.name, supplement->method))) { + supplement->incoming_request(session, rdata); + } + } +} + +static void handle_incoming_response(struct ast_sip_session *session, pjsip_rx_data *rdata) +{ + struct ast_sip_session_supplement *supplement; + struct pjsip_status_line status = rdata->msg_info.msg->line.status; + + ast_debug(3, "Response is %d %.*s\n", status.code, (int) pj_strlen(&status.reason), + pj_strbuf(&status.reason)); + + AST_LIST_TRAVERSE(&session->supplements, supplement, next) { + if (supplement->incoming_response && ( + !supplement->method || !pj_strcmp2(&rdata->msg_info.cseq->method.name, supplement->method))) { + supplement->incoming_response(session, rdata); + } + } +} + +static int handle_incoming(struct ast_sip_session *session, pjsip_rx_data *rdata) +{ + ast_debug(3, "Received %s\n", rdata->msg_info.msg->type == PJSIP_REQUEST_MSG ? + "request" : "response"); + + if (rdata->msg_info.msg->type == PJSIP_REQUEST_MSG) { + handle_incoming_request(session, rdata); + } else { + handle_incoming_response(session, rdata); + } + + return 0; +} + +static void handle_outgoing_request(struct ast_sip_session *session, pjsip_tx_data *tdata) +{ + struct ast_sip_session_supplement *supplement; + struct pjsip_request_line req = tdata->msg->line.req; + + ast_debug(3, "Method is %.*s\n", (int) pj_strlen(&req.method.name), pj_strbuf(&req.method.name)); + AST_LIST_TRAVERSE(&session->supplements, supplement, next) { + if (supplement->outgoing_request && + (!supplement->method || !pj_strcmp2(&req.method.name, supplement->method))) { + supplement->outgoing_request(session, tdata); + } + } +} + +static void handle_outgoing_response(struct ast_sip_session *session, pjsip_tx_data *tdata) +{ + struct ast_sip_session_supplement *supplement; + struct pjsip_status_line status = tdata->msg->line.status; + ast_debug(3, "Response is %d %.*s\n", status.code, (int) pj_strlen(&status.reason), + pj_strbuf(&status.reason)); + + AST_LIST_TRAVERSE(&session->supplements, supplement, next) { + /* XXX Not sure how to get the method from a response. + * For now, just call supplements on all responses, no + * matter the method. This is less than ideal + */ + if (supplement->outgoing_response) { + supplement->outgoing_response(session, tdata); + } + } +} + +static void handle_outgoing(struct ast_sip_session *session, pjsip_tx_data *tdata) +{ + ast_debug(3, "Sending %s\n", tdata->msg->type == PJSIP_REQUEST_MSG ? + "request" : "response"); + if (tdata->msg->type == PJSIP_REQUEST_MSG) { + handle_outgoing_request(session, tdata); + } else { + handle_outgoing_response(session, tdata); + } +} + +static int session_end(struct ast_sip_session *session) +{ + struct ast_sip_session_supplement *iter; + + /* Session is dead. Let's get rid of the reference to the session */ + AST_LIST_TRAVERSE(&session->supplements, iter, next) { + if (iter->session_end) { + iter->session_end(session); + } + } + + session->inv_session->mod_data[session_module.id] = NULL; + ast_sip_dialog_set_serializer(session->inv_session->dlg, NULL); + ast_sip_dialog_set_endpoint(session->inv_session->dlg, NULL); + ao2_cleanup(session); + return 0; +} + +static void session_inv_on_state_changed(pjsip_inv_session *inv, pjsip_event *e) +{ + struct ast_sip_session *session = inv->mod_data[session_module.id]; + + print_debug_details(inv, NULL, e); + + switch(e->type) { + case PJSIP_EVENT_TX_MSG: + handle_outgoing(session, e->body.tx_msg.tdata); + break; + case PJSIP_EVENT_RX_MSG: + handle_incoming(session, e->body.rx_msg.rdata); + break; + case PJSIP_EVENT_TSX_STATE: + ast_debug(3, "Source of transaction state change is %s\n", pjsip_event_str(e->body.tsx_state.type)); + /* Transaction state changes are prompted by some other underlying event. */ + switch(e->body.tsx_state.type) { + case PJSIP_EVENT_TX_MSG: + handle_outgoing(session, e->body.tsx_state.src.tdata); + break; + case PJSIP_EVENT_RX_MSG: + handle_incoming(session, e->body.tsx_state.src.rdata); + break; + case PJSIP_EVENT_TRANSPORT_ERROR: + case PJSIP_EVENT_TIMER: + case PJSIP_EVENT_USER: + case PJSIP_EVENT_UNKNOWN: + case PJSIP_EVENT_TSX_STATE: + /* Inception? */ + break; + } + break; + case PJSIP_EVENT_TRANSPORT_ERROR: + case PJSIP_EVENT_TIMER: + case PJSIP_EVENT_UNKNOWN: + case PJSIP_EVENT_USER: + default: + break; + } + + if (inv->state == PJSIP_INV_STATE_DISCONNECTED) { + session_end(session); + } +} + +static void session_inv_on_new_session(pjsip_inv_session *inv, pjsip_event *e) +{ + /* XXX STUB */ +} + +static void session_inv_on_tsx_state_changed(pjsip_inv_session *inv, pjsip_transaction *tsx, pjsip_event *e) +{ + struct ast_sip_session *session = inv->mod_data[session_module.id]; + print_debug_details(inv, tsx, e); + if (!session) { + /* Transaction likely timed out after the call was hung up. Just + * ignore such transaction changes + */ + return; + } + switch (e->body.tsx_state.type) { + case PJSIP_EVENT_TX_MSG: + /* When we create an outgoing request, we do not have access to the transaction that + * is created. Instead, We have to place transaction-specific data in the tdata. Here, + * we transfer the data into the transaction. This way, when we receive a response, we + * can dig this data out again + */ + tsx->mod_data[session_module.id] = e->body.tsx_state.src.tdata->mod_data[session_module.id]; + break; + case PJSIP_EVENT_RX_MSG: + if (tsx->method.id == PJSIP_INVITE_METHOD) { + if (tsx->role == PJSIP_ROLE_UAC && tsx->state == PJSIP_TSX_STATE_COMPLETED) { + /* This means we got a non 2XX final response to our outgoing INVITE */ + if (tsx->status_code == PJSIP_SC_REQUEST_PENDING) { + reschedule_reinvite(session, tsx->mod_data[session_module.id], tsx->last_tx); + return; + } else { + /* Other failures result in destroying the session. */ + pjsip_tx_data *tdata; + pjsip_inv_end_session(inv, 500, NULL, &tdata); + ast_sip_session_send_request(session, tdata); + } + } + } else { + if (tsx->role == PJSIP_ROLE_UAS && tsx->state == PJSIP_TSX_STATE_TRYING) { + handle_incoming_request(session, e->body.tsx_state.src.rdata); + } + } + if (tsx->mod_data[session_module.id]) { + ast_sip_session_response_cb cb = tsx->mod_data[session_module.id]; + cb(session, e->body.tsx_state.src.rdata); + } + case PJSIP_EVENT_TRANSPORT_ERROR: + case PJSIP_EVENT_TIMER: + case PJSIP_EVENT_USER: + case PJSIP_EVENT_UNKNOWN: + case PJSIP_EVENT_TSX_STATE: + /* Inception? */ + break; + } + + /* Terminated INVITE transactions always should result in queuing delayed requests, + * no matter what event caused the transaction to terminate + */ + if (tsx->method.id == PJSIP_INVITE_METHOD && tsx->state == PJSIP_TSX_STATE_TERMINATED) { + queue_delayed_request(session); + } +} + +static int add_sdp_streams(void *obj, void *arg, void *data, int flags) +{ + struct ast_sip_session_media *session_media = obj; + pjmedia_sdp_session *answer = arg; + struct ast_sip_session *session = data; + struct ast_sip_session_sdp_handler *handler = session_media->handler; + RAII_VAR(struct sdp_handler_list *, handler_list, NULL, ao2_cleanup); + + if (handler) { + /* if an already assigned handler does not handle the session_media or reports a catastrophic error, fail */ + if (handler->create_outgoing_sdp_stream(session, session_media, answer) <= 0) { + return 0; + } + return CMP_MATCH; + } + + handler_list = ao2_find(sdp_handlers, session_media->stream_type, OBJ_KEY); + if (!handler_list) { + return CMP_MATCH; + } + + /* no handler for this stream type and we have a list to search */ + AST_LIST_TRAVERSE(&handler_list->list, handler, next) { + int res = handler->create_outgoing_sdp_stream(session, session_media, answer); + if (res < 0) { + /* catastrophic error */ + return 0; + } + if (res > 0) { + /* handled */ + return CMP_MATCH; + } + } + + /* streams that weren't handled won't be included in generated outbound SDP */ + return CMP_MATCH; +} + +static struct pjmedia_sdp_session *create_local_sdp(pjsip_inv_session *inv, struct ast_sip_session *session, const pjmedia_sdp_session *offer) +{ + RAII_VAR(struct ao2_iterator *, successful, NULL, ao2_iterator_cleanup); + static const pj_str_t STR_ASTERISK = { "Asterisk", 8 }; + static const pj_str_t STR_IN = { "IN", 2 }; + static const pj_str_t STR_IP4 = { "IP4", 3 }; + static const pj_str_t STR_IP6 = { "IP6", 3 }; + pjmedia_sdp_session *local; + + if (!(local = PJ_POOL_ZALLOC_T(inv->pool_prov, pjmedia_sdp_session))) { + return NULL; + } + + if (!offer) { + local->origin.version = local->origin.id = (pj_uint32_t)(ast_random()); + } else { + local->origin.version = offer->origin.version + 1; + local->origin.id = offer->origin.id; + } + + local->origin.user = STR_ASTERISK; + local->origin.net_type = STR_IN; + local->origin.addr_type = session->endpoint->rtp_ipv6 ? STR_IP6 : STR_IP4; + local->origin.addr = *pj_gethostname(); + local->name = local->origin.user; + + /* Now let the handlers add streams of various types, pjmedia will automatically reorder the media streams for us */ + successful = ao2_callback_data(session->media, OBJ_MULTIPLE, add_sdp_streams, local, session); + if (!successful || ao2_container_count(successful->c) != ao2_container_count(session->media)) { + /* Something experienced a catastrophic failure */ + return NULL; + } + + /* Use the connection details of the first media stream if possible for SDP level */ + if (local->media_count) { + local->conn = local->media[0]->conn; + } + + return local; +} + +static void session_inv_on_rx_offer(pjsip_inv_session *inv, const pjmedia_sdp_session *offer) +{ + struct ast_sip_session *session = inv->mod_data[session_module.id]; + pjmedia_sdp_session *answer; + + if (handle_incoming_sdp(session, offer)) { + return; + } + + if ((answer = create_local_sdp(inv, session, offer))) { + pjsip_inv_set_sdp_answer(inv, answer); + } +} + +#if 0 +static void session_inv_on_create_offer(pjsip_inv_session *inv, pjmedia_sdp_session **p_offer) +{ + /* XXX STUB */ +} +#endif + +static void session_inv_on_media_update(pjsip_inv_session *inv, pj_status_t status) +{ + struct ast_sip_session *session = inv->mod_data[session_module.id]; + const pjmedia_sdp_session *local, *remote; + + if (!session->channel) { + /* If we don't have a channel. We really don't care about media updates. + * Just ignore + */ + return; + } + + if ((status != PJ_SUCCESS) || (pjmedia_sdp_neg_get_active_local(inv->neg, &local) != PJ_SUCCESS) || + (pjmedia_sdp_neg_get_active_remote(inv->neg, &remote) != PJ_SUCCESS)) { + ast_channel_hangupcause_set(session->channel, AST_CAUSE_BEARERCAPABILITY_NOTAVAIL); + ast_queue_hangup(session->channel); + return; + } + + handle_negotiated_sdp(session, local, remote); +} + +static pjsip_redirect_op session_inv_on_redirected(pjsip_inv_session *inv, const pjsip_uri *target, const pjsip_event *e) +{ + /* XXX STUB */ + return PJSIP_REDIRECT_REJECT; +} + +static pjsip_inv_callback inv_callback = { + .on_state_changed = session_inv_on_state_changed, + .on_new_session = session_inv_on_new_session, + .on_tsx_state_changed = session_inv_on_tsx_state_changed, + .on_rx_offer = session_inv_on_rx_offer, + .on_media_update = session_inv_on_media_update, + .on_redirected = session_inv_on_redirected, +}; + +/*! \brief Hook for modifying outgoing messages with SDP to contain the proper address information */ +static void session_outgoing_nat_hook(pjsip_tx_data *tdata, struct ast_sip_transport *transport) +{ + struct ast_sip_nat_hook *hook = tdata->mod_data[session_module.id]; + struct pjmedia_sdp_session *sdp; + int stream; + + /* SDP produced by us directly will never be multipart */ + if (hook || !tdata->msg->body || pj_stricmp2(&tdata->msg->body->content_type.type, "application") || + pj_stricmp2(&tdata->msg->body->content_type.subtype, "sdp") || ast_strlen_zero(transport->external_media_address)) { + return; + } + + sdp = tdata->msg->body->data; + + for (stream = 0; stream < sdp->media_count; ++stream) { + /* See if there are registered handlers for this media stream type */ + char media[20]; + struct ast_sip_session_sdp_handler *handler; + RAII_VAR(struct sdp_handler_list *, handler_list, NULL, ao2_cleanup); + + /* We need a null-terminated version of the media string */ + ast_copy_pj_str(media, &sdp->media[stream]->desc.media, sizeof(media)); + + handler_list = ao2_find(sdp_handlers, media, OBJ_KEY); + if (!handler_list) { + ast_debug(1, "No registered SDP handlers for media type '%s'\n", media); + continue; + } + AST_LIST_TRAVERSE(&handler_list->list, handler, next) { + if (handler->change_outgoing_sdp_stream_media_address) { + handler->change_outgoing_sdp_stream_media_address(tdata, sdp->media[stream], transport); + } + } + } + + /* We purposely do this so that the hook will not be invoked multiple times, ie: if a retransmit occurs */ + tdata->mod_data[session_module.id] = nat_hook; +} + +static int load_module(void) +{ + pjsip_endpoint *endpt; + if (!ast_sip_get_sorcery() || !ast_sip_get_pjsip_endpoint()) { + return AST_MODULE_LOAD_DECLINE; + } + if (!(nat_hook = ast_sorcery_alloc(ast_sip_get_sorcery(), "nat_hook", NULL))) { + return AST_MODULE_LOAD_DECLINE; + } + nat_hook->outgoing_external_message = session_outgoing_nat_hook; + ast_sorcery_create(ast_sip_get_sorcery(), nat_hook); + sdp_handlers = ao2_container_alloc(SDP_HANDLER_BUCKETS, + sdp_handler_list_hash, sdp_handler_list_cmp); + if (!sdp_handlers) { + return AST_MODULE_LOAD_DECLINE; + } + endpt = ast_sip_get_pjsip_endpoint(); + pjsip_inv_usage_init(endpt, &inv_callback); + pjsip_100rel_init_module(endpt); + pjsip_timer_init_module(endpt); + if (ast_sip_register_service(&session_module)) { + return AST_MODULE_LOAD_DECLINE; + } + return AST_MODULE_LOAD_SUCCESS; +} + +static int unload_module(void) +{ + ast_sip_unregister_service(&session_module); + if (nat_hook) { + ast_sorcery_delete(ast_sip_get_sorcery(), nat_hook); + nat_hook = NULL; + } + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS | AST_MODFLAG_LOAD_ORDER, "SIP Session resource", + .load = load_module, + .unload = unload_module, + .load_pri = AST_MODPRI_APP_DEPEND, + ); diff --git a/res/res_sip_session.exports.in b/res/res_sip_session.exports.in new file mode 100644 index 000000000..08c6f3937 --- /dev/null +++ b/res/res_sip_session.exports.in @@ -0,0 +1,18 @@ +{ + global: + LINKER_SYMBOL_PREFIXast_sip_session_register_sdp_handler; + LINKER_SYMBOL_PREFIXast_sip_session_unregister_sdp_handler; + LINKER_SYMBOL_PREFIXast_sip_session_register_supplement; + LINKER_SYMBOL_PREFIXast_sip_session_unregister_supplement; + LINKER_SYMBOL_PREFIXast_sip_session_alloc_datastore; + LINKER_SYMBOL_PREFIXast_sip_session_add_datastore; + LINKER_SYMBOL_PREFIXast_sip_session_get_datastore; + LINKER_SYMBOL_PREFIXast_sip_session_remove_datastore; + LINKER_SYMBOL_PREFIXast_sip_session_get_identity; + LINKER_SYMBOL_PREFIXast_sip_session_refresh; + LINKER_SYMBOL_PREFIXast_sip_session_send_response; + LINKER_SYMBOL_PREFIXast_sip_session_send_request; + LINKER_SYMBOL_PREFIXast_sip_session_create_outgoing; + local: + *; +}; diff --git a/res/res_sorcery_config.c b/res/res_sorcery_config.c index 509538f5a..c6ec62ed6 100644 --- a/res/res_sorcery_config.c +++ b/res/res_sorcery_config.c @@ -199,7 +199,6 @@ static void sorcery_config_retrieve_multiple(const struct ast_sorcery *sorcery, if (!config_objects) { return; } - ao2_callback(config_objects, 0, sorcery_config_fields_cmp, ¶ms); } |