core/ari/pjsip: Add refer mechanism

This change adds support for refers that are not session based. It
includes a refer implementation for the PJSIP technology which results
in out-of-dialog REFERs being sent to a PJSIP endpoint. These can be
triggered using the new ARI endpoint `/endpoints/refer`.

Resolves: #71

UserNote: There is a new ARI endpoint `/endpoints/refer` for referring
an endpoint to some URI or endpoint.
This commit is contained in:
Maximilian Fridrich
2023-05-10 15:53:33 +02:00
parent c08c458fa2
commit 57f77e8218
13 changed files with 2813 additions and 734 deletions

View File

@@ -195,571 +195,6 @@ static enum pjsip_status_code check_content_type_in_dialog(const pjsip_rx_data *
return res;
}
/*!
* \brief Find a contact and insert a "user@" into its URI.
*
* \param to Original destination (for error messages only)
* \param endpoint_name Endpoint name (for error messages only)
* \param aors Command separated list of AORs
* \param user The user to insert in the contact URI
* \param uri Pointer to buffer in which to return the URI
*
* \return 0 Success
* \return -1 Fail
*
* \note If the contact URI found for the endpoint already has a user in
* its URI, it will be replaced.
*/
static int insert_user_in_contact_uri(const char *to, const char *endpoint_name, const char *aors,
const char *user, char **uri)
{
char *scheme = NULL;
char *contact_uri = NULL;
char *after_scheme = NULL;
char *host;
struct ast_sip_contact *contact = NULL;
contact = ast_sip_location_retrieve_contact_from_aor_list(aors);
if (!contact) {
/*
* We're getting the contact using the same method as
* ast_sip_create_request() so if there's no contact
* we can never send this message.
*/
ast_log(LOG_WARNING, "Dest: '%s' MSG SEND FAIL: Couldn't find contact for endpoint '%s'\n",
to, endpoint_name);
return -1;
}
contact_uri = ast_strdupa(contact->uri);
ao2_cleanup(contact);
ast_debug(3, "Dest: '%s' User: '%s' Endpoint: '%s' ContactURI: '%s'\n", to, user, endpoint_name, contact_uri);
/*
* Contact URIs must have a scheme so we must insert the user between it and the host.
*/
scheme = contact_uri;
after_scheme = strchr(contact_uri, ':');
if (!after_scheme) {
/* A contact URI without a scheme? Something's wrong. Bail */
ast_log(LOG_WARNING, "Dest: '%s' MSG SEND FAIL: There was no scheme in the contact URI '%s'\n",
to, contact_uri);
return -1;
}
/*
* Terminate the scheme.
*/
*after_scheme = '\0';
after_scheme++;
/*
* If the contact_uri already has a user, the host starts after the '@', otherwise
* the host is at after_scheme.
*
* We're going to ignore the existing user.
*/
host = strchr(after_scheme, '@');
if (host) {
host++;
} else {
host = after_scheme;
}
*uri = ast_malloc(strlen(scheme) + strlen(user) + strlen(host) + 3 /* One for the ':', '@' and terminating NULL */);
sprintf(*uri, "%s:%s@%s", scheme, user, host); /* Safe */
return 0;
}
/*!
* \internal
* \brief Get endpoint and URI when the destination is only a single token
*
* "to" could be one of the following:
* \verbatim
endpoint_name
hostname
* \endverbatim
*
* \param to Destination specified in MessageSend
* \param destination
* \param uri Pointer to URI variable. Must be freed by caller
* \return endpoint
*/
static struct ast_sip_endpoint *handle_single_token(const char *to, char *destination, char **uri) {
char *endpoint_name = NULL;
struct ast_sip_endpoint *endpoint = NULL;
struct ast_sip_contact *contact = NULL;
/*
* If "to" is just one token, it could be an endpoint name
* or a hostname without a scheme.
*/
endpoint = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", destination);
if (!endpoint) {
/*
* We can only assume it's a hostname.
*/
char *temp_uri = ast_malloc(strlen(destination) + strlen("sip:") + 1);
sprintf(temp_uri, "sip:%s", destination);
*uri = temp_uri;
endpoint = ast_sip_default_outbound_endpoint();
ast_debug(3, "Dest: '%s' Didn't find endpoint so adding scheme and using URI '%s' with default endpoint\n",
to, *uri);
return endpoint;
}
/*
* It's an endpoint
*/
endpoint_name = destination;
contact = ast_sip_location_retrieve_contact_from_aor_list(endpoint->aors);
if (!contact) {
/*
* We're getting the contact using the same method as
* ast_sip_create_request() so if there's no contact
* we can never send this message.
*/
ast_log(LOG_WARNING, "Dest: '%s' MSG SEND FAIL: Found endpoint '%s' but didn't find an aor/contact for it\n",
to, endpoint_name);
ao2_cleanup(endpoint);
*uri = NULL;
return NULL;
}
*uri = ast_strdup(contact->uri);
ast_debug(3, "Dest: '%s' Found endpoint '%s' and found contact with URI '%s'\n",
to, endpoint_name, *uri);
ao2_cleanup(contact);
return endpoint;
}
/*!
* \internal
* \brief Get endpoint and URI when the destination contained a '/'
*
* "to" could be one of the following:
* \verbatim
endpoint/aor
endpoint/<sip[s]:host>
endpoint/<sip[s]:user@host>
endpoint/"Bob" <sip[s]:host>
endpoint/"Bob" <sip[s]:user@host>
endpoint/sip[s]:host
endpoint/sip[s]:user@host
endpoint/host
endpoint/user@host
* \endverbatim
*
* \param to Destination specified in MessageSend
* \param uri Pointer to URI variable. Must be freed by caller
* \param destination, slash, atsign, scheme
* \return endpoint
*/
static struct ast_sip_endpoint *handle_slash(const char *to, char *destination, char **uri,
char *slash, char *atsign, char *scheme)
{
char *endpoint_name = NULL;
struct ast_sip_endpoint *endpoint = NULL;
struct ast_sip_contact *contact = NULL;
char *user = NULL;
char *afterslash = slash + 1;
struct ast_sip_aor *aor;
if (ast_begins_with(destination, "PJSIP/")) {
ast_debug(3, "Dest: '%s' Dialplan format'\n", to);
/*
* This has to be the form PJSIP/user@endpoint
*/
if (!atsign || strchr(afterslash, '/')) {
/*
* If there's no "user@" or there's a slash somewhere after
* "PJSIP/" then we go no further.
*/
*uri = NULL;
ast_log(LOG_WARNING,
"Dest: '%s' MSG SEND FAIL: Destinations beginning with 'PJSIP/' must be in the form of 'PJSIP/user@endpoint'\n",
to);
return NULL;
}
*atsign = '\0';
user = afterslash;
endpoint_name = atsign + 1;
ast_debug(3, "Dest: '%s' User: '%s' Endpoint: '%s'\n", to, user, endpoint_name);
} else {
/*
* Either...
* endpoint/aor
* endpoint/uri
*/
*slash = '\0';
endpoint_name = destination;
ast_debug(3, "Dest: '%s' Endpoint: '%s'\n", to, endpoint_name);
}
endpoint = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", endpoint_name);
if (!endpoint) {
*uri = NULL;
ast_log(LOG_WARNING, "Dest: '%s' MSG SEND FAIL: Didn't find endpoint with name '%s'\n",
to, endpoint_name);
return NULL;
}
if (scheme) {
/*
* If we found a scheme, then everything after the slash MUST be a URI.
* We don't need to do any further modification.
*/
*uri = ast_strdup(afterslash);
ast_debug(3, "Dest: '%s' Found endpoint '%s' and found URI '%s' after '/'\n",
to, endpoint_name, *uri);
return endpoint;
}
if (user) {
/*
* This has to be the form PJSIP/user@endpoint
*/
int rc;
/*
* Set the return URI to be the endpoint's contact URI with the user
* portion set to the user that was specified before the endpoint name.
*/
rc = insert_user_in_contact_uri(to, endpoint_name, endpoint->aors, user, uri);
if (rc != 0) {
/*
* insert_user_in_contact_uri prints the warning message.
*/
ao2_cleanup(endpoint);
endpoint = NULL;
*uri = NULL;
}
ast_debug(3, "Dest: '%s' User: '%s' Endpoint: '%s' URI: '%s'\n", to, user,
endpoint_name, *uri);
return endpoint;
}
/*
* We're now left with two possibilities...
* endpoint/aor
* endpoint/uri-without-scheme
*/
aor = ast_sip_location_retrieve_aor(afterslash);
if (!aor) {
/*
* It's probably a URI without a scheme but we don't have a way to tell
* for sure. We're going to assume it is and prepend it with a scheme.
*/
*uri = ast_malloc(strlen(afterslash) + strlen("sip:") + 1);
sprintf(*uri, "sip:%s", afterslash);
ast_debug(3, "Dest: '%s' Found endpoint '%s' but didn't find aor after '/' so using URI '%s'\n",
to, endpoint_name, *uri);
return endpoint;
}
/*
* Only one possibility left... There was an aor name after the slash.
*/
ast_debug(3, "Dest: '%s' Found endpoint '%s' and found aor '%s' after '/'\n",
to, endpoint_name, ast_sorcery_object_get_id(aor));
contact = ast_sip_location_retrieve_first_aor_contact(aor);
if (!contact) {
/*
* An aor without a contact is useless and since
* ast_sip_create_message() won't be able to find one
* either, we just need to bail.
*/
ast_log(LOG_WARNING, "Dest: '%s' MSG SEND FAIL: Found endpoint '%s' but didn't find contact for aor '%s'\n",
to, endpoint_name, ast_sorcery_object_get_id(aor));
ao2_cleanup(aor);
ao2_cleanup(endpoint);
*uri = NULL;
return NULL;
}
*uri = ast_strdup(contact->uri);
ast_debug(3, "Dest: '%s' Found endpoint '%s' and found contact with URI '%s' for aor '%s'\n",
to, endpoint_name, *uri, ast_sorcery_object_get_id(aor));
ao2_cleanup(contact);
ao2_cleanup(aor);
return endpoint;
}
/*!
* \internal
* \brief Get endpoint and URI when the destination contained a '\@' but no '/' or scheme
*
* "to" could be one of the following:
* \verbatim
<sip[s]:user@host>
"Bob" <sip[s]:user@host>
sip[s]:user@host
user@host
* \endverbatim
*
* \param to Destination specified in MessageSend
* \param uri Pointer to URI variable. Must be freed by caller
* \param destination, slash, atsign, scheme
* \return endpoint
*/
static struct ast_sip_endpoint *handle_atsign(const char *to, char *destination, char **uri,
char *slash, char *atsign, char *scheme)
{
char *endpoint_name = NULL;
struct ast_sip_endpoint *endpoint = NULL;
struct ast_sip_contact *contact = NULL;
char *afterat = atsign + 1;
*atsign = '\0';
endpoint_name = destination;
/* Apparently there may be ';<user_options>' after the endpoint name ??? */
AST_SIP_USER_OPTIONS_TRUNCATE_CHECK(endpoint_name);
endpoint = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", endpoint_name);
if (!endpoint) {
/*
* It's probably a uri with a user but without a scheme but we don't have a way to tell.
* We're going to assume it is and prepend it with a scheme.
*/
*uri = ast_malloc(strlen(to) + strlen("sip:") + 1);
sprintf(*uri, "sip:%s", to);
endpoint = ast_sip_default_outbound_endpoint();
ast_debug(3, "Dest: '%s' Didn't find endpoint before the '@' so using URI '%s' with default endpoint\n",
to, *uri);
return endpoint;
}
/*
* OK, it's an endpoint and a domain (which we ignore)
*/
contact = ast_sip_location_retrieve_contact_from_aor_list(endpoint->aors);
if (!contact) {
/*
* We're getting the contact using the same method as
* ast_sip_create_request() so if there's no contact
* we can never send this message.
*/
ao2_cleanup(endpoint);
endpoint = NULL;
*uri = NULL;
ast_log(LOG_WARNING, "Dest: '%s' MSG SEND FAIL: Found endpoint '%s' but didn't find contact\n",
to, endpoint_name);
return NULL;
}
*uri = ast_strdup(contact->uri);
ao2_cleanup(contact);
ast_debug(3, "Dest: '%s' Found endpoint '%s' and found contact with URI '%s' (discarding domain %s)\n",
to, endpoint_name, *uri, afterat);
return endpoint;
}
/*!
* \internal
* \brief Retrieves an endpoint and URI from the "to" string.
*
* This URI is used as the Request URI.
*
* Expects the given 'to' to be in one of the following formats:
* Why we allow so many is a mystery.
*
* Basic:
*
* endpoint : We'll get URI from the default aor/contact
* endpoint/aor : We'll get the URI from the specific aor/contact
* endpoint@domain : We toss the domain part and just use the endpoint
*
* These all use the endpoint and specified URI:
* \verbatim
endpoint/<sip[s]:host>
endpoint/<sip[s]:user@host>
endpoint/"Bob" <sip[s]:host>
endpoint/"Bob" <sip[s]:user@host>
endpoint/sip[s]:host
endpoint/sip[s]:user@host
endpoint/host
endpoint/user@host
\endverbatim
*
* These all use the default endpoint and specified URI:
* \verbatim
<sip[s]:host>
<sip[s]:user@host>
"Bob" <sip[s]:host>
"Bob" <sip[s]:user@host>
sip[s]:host
sip[s]:user@host
\endverbatim
*
* These use the default endpoint and specified host:
* \verbatim
host
user@host
\endverbatim
*
* This form is similar to a dialstring:
* \verbatim
PJSIP/user@endpoint
\endverbatim
*
* In this case, the user will be added to the endpoint contact's URI.
* If the contact URI already has a user, it will be replaced.
*
* The ones that have the sip[s] scheme are the easiest to parse.
* The rest all have some issue.
*
* endpoint vs host : We have to test for endpoint first
* endpoint/aor vs endpoint/host : We have to test for aor first
* What if there's an aor with the same
* name as the host?
* endpoint@domain vs user@host : We have to test for endpoint first.
* What if there's an endpoint with the
* same name as the user?
*
* \param to 'To' field with possible endpoint
* \param uri Pointer to a char* which will be set to the URI.
* Must be ast_free'd by the caller.
*
* \note The logic below could probably be condensed but then it wouldn't be
* as clear.
*/
static struct ast_sip_endpoint *get_outbound_endpoint(const char *to, char **uri)
{
char *destination;
char *slash = NULL;
char *atsign = NULL;
char *scheme = NULL;
struct ast_sip_endpoint *endpoint = NULL;
destination = ast_strdupa(to);
slash = strchr(destination, '/');
atsign = strchr(destination, '@');
scheme = S_OR(strstr(destination, "sip:"), strstr(destination, "sips:"));
if (!slash && !atsign && !scheme) {
/*
* If there's only a single token, it can be either...
* endpoint
* host
*/
return handle_single_token(to, destination, uri);
}
if (slash) {
/*
* If there's a '/', then the form must be one of the following...
* PJSIP/user@endpoint
* endpoint/aor
* endpoint/uri
*/
return handle_slash(to, destination, uri, slash, atsign, scheme);
}
if (!endpoint && atsign && !scheme) {
/*
* If there's an '@' but no scheme then it's either following an endpoint name
* and being followed by a domain name (which we discard).
* OR is's a user@host uri without a scheme. It's probably the latter but because
* endpoint@domain looks just like user@host, we'll test for endpoint first.
*/
return handle_atsign(to, destination, uri, slash, atsign, scheme);
}
/*
* If all else fails, we assume it's a URI or just a hostname.
*/
if (scheme) {
*uri = ast_strdup(destination);
ast_debug(3, "Dest: '%s' Didn't find an endpoint but did find a scheme so using URI '%s' with default endpoint\n",
to, *uri);
} else {
*uri = ast_malloc(strlen(destination) + strlen("sip:") + 1);
sprintf(*uri, "sip:%s", destination);
ast_debug(3, "Dest: '%s' Didn't find an endpoint and didn't find scheme so adding scheme and using URI '%s' with default endpoint\n",
to, *uri);
}
endpoint = ast_sip_default_outbound_endpoint();
return endpoint;
}
/*!
* \internal
* \brief Replace the To URI in the tdata with the supplied one
*
* \param tdata the outbound message data structure
* \param to URI to replace the To URI with
*
* \return 0: success, -1: failure
*/
static int update_to_uri(pjsip_tx_data *tdata, char *to)
{
pjsip_name_addr *parsed_name_addr;
pjsip_sip_uri *sip_uri;
pjsip_name_addr *tdata_name_addr;
pjsip_sip_uri *tdata_sip_uri;
char *buf = NULL;
#define DEBUG_BUF_SIZE 256
parsed_name_addr = (pjsip_name_addr *) pjsip_parse_uri(tdata->pool, to, strlen(to),
PJSIP_PARSE_URI_AS_NAMEADDR);
if (!parsed_name_addr || (!PJSIP_URI_SCHEME_IS_SIP(parsed_name_addr->uri)
&& !PJSIP_URI_SCHEME_IS_SIPS(parsed_name_addr->uri))) {
ast_log(LOG_WARNING, "To address '%s' is not a valid SIP/SIPS URI\n", to);
return -1;
}
sip_uri = pjsip_uri_get_uri(parsed_name_addr->uri);
if (DEBUG_ATLEAST(3)) {
buf = ast_alloca(DEBUG_BUF_SIZE);
pjsip_uri_print(PJSIP_URI_IN_FROMTO_HDR, sip_uri, buf, DEBUG_BUF_SIZE);
ast_debug(3, "Parsed To: %.*s %s\n", (int)parsed_name_addr->display.slen,
parsed_name_addr->display.ptr, buf);
}
tdata_name_addr = (pjsip_name_addr *) PJSIP_MSG_TO_HDR(tdata->msg)->uri;
if (!tdata_name_addr || (!PJSIP_URI_SCHEME_IS_SIP(tdata_name_addr->uri)
&& !PJSIP_URI_SCHEME_IS_SIPS(tdata_name_addr->uri))) {
/* Highly unlikely but we have to check */
ast_log(LOG_WARNING, "tdata To address '%s' is not a valid SIP/SIPS URI\n", to);
return -1;
}
tdata_sip_uri = pjsip_uri_get_uri(tdata_name_addr->uri);
if (DEBUG_ATLEAST(3)) {
buf[0] = '\0';
pjsip_uri_print(PJSIP_URI_IN_FROMTO_HDR, tdata_sip_uri, buf, DEBUG_BUF_SIZE);
ast_debug(3, "Original tdata To: %.*s %s\n", (int)tdata_name_addr->display.slen,
tdata_name_addr->display.ptr, buf);
}
/* Replace the uri */
pjsip_sip_uri_assign(tdata->pool, tdata_sip_uri, sip_uri);
/* The display name isn't part of the URI so we need to replace it separately */
pj_strdup(tdata->pool, &tdata_name_addr->display, &parsed_name_addr->display);
if (DEBUG_ATLEAST(3)) {
buf[0] = '\0';
pjsip_uri_print(PJSIP_URI_IN_FROMTO_HDR, tdata_sip_uri, buf, 256);
ast_debug(3, "New tdata To: %.*s %s\n", (int)tdata_name_addr->display.slen,
tdata_name_addr->display.ptr, buf);
}
return 0;
#undef DEBUG_BUF_SIZE
}
/*!
* \internal
* \brief Update the display name in the To uri in the tdata with the one from the supplied uri
@@ -790,77 +225,6 @@ static int update_to_display_name(pjsip_tx_data *tdata, char *to)
return -1;
}
/*!
* \internal
* \brief Overwrite fields in the outbound 'From' header
*
* The outbound 'From' header is created/added in ast_sip_create_request with
* default data. If available that data may be info specified in the 'from_user'
* and 'from_domain' options found on the endpoint. That information will be
* overwritten with data in the given 'from' parameter.
*
* \param tdata the outbound message data structure
* \param from info to copy into the header
*
* \return 0: success, -1: failure
*/
static int update_from(pjsip_tx_data *tdata, char *from)
{
pjsip_name_addr *name_addr;
pjsip_sip_uri *uri;
pjsip_name_addr *parsed_name_addr;
if (ast_strlen_zero(from)) {
return 0;
}
name_addr = (pjsip_name_addr *) PJSIP_MSG_FROM_HDR(tdata->msg)->uri;
uri = pjsip_uri_get_uri(name_addr);
parsed_name_addr = (pjsip_name_addr *) pjsip_parse_uri(tdata->pool, from,
strlen(from), PJSIP_PARSE_URI_AS_NAMEADDR);
if (parsed_name_addr) {
pjsip_sip_uri *parsed_uri;
if (!PJSIP_URI_SCHEME_IS_SIP(parsed_name_addr->uri)
&& !PJSIP_URI_SCHEME_IS_SIPS(parsed_name_addr->uri)) {
ast_log(LOG_WARNING, "From address '%s' is not a valid SIP/SIPS URI\n", from);
return -1;
}
parsed_uri = pjsip_uri_get_uri(parsed_name_addr->uri);
if (pj_strlen(&parsed_name_addr->display)) {
pj_strdup(tdata->pool, &name_addr->display, &parsed_name_addr->display);
}
/* Unlike the To header, we only want to replace the user, host and port */
pj_strdup(tdata->pool, &uri->user, &parsed_uri->user);
pj_strdup(tdata->pool, &uri->host, &parsed_uri->host);
uri->port = parsed_uri->port;
return 0;
} else {
/* assume it is 'user[@domain]' format */
char *domain = strchr(from, '@');
if (domain) {
pj_str_t pj_from;
pj_strset3(&pj_from, from, domain);
pj_strdup(tdata->pool, &uri->user, &pj_from);
pj_strdup2(tdata->pool, &uri->host, domain + 1);
} else {
pj_strdup2(tdata->pool, &uri->user, from);
}
return 0;
}
return -1;
}
/*!
* \internal
* \brief Checks if the given msg var name should be blocked.
@@ -1252,7 +616,7 @@ static int msg_send(void *data)
ast_debug(3, "mdata From: %s msg From: %s mdata Destination: %s msg To: %s\n",
mdata->from, ast_msg_get_from(mdata->msg), mdata->destination, ast_msg_get_to(mdata->msg));
endpoint = get_outbound_endpoint(mdata->destination, &uri);
endpoint = ast_sip_get_endpoint(mdata->destination, 1, &uri);
if (!endpoint) {
ast_log(LOG_ERROR,
"PJSIP MESSAGE - Could not find endpoint '%s' and no default outbound endpoint configured\n",
@@ -1290,7 +654,7 @@ static int msg_send(void *data)
if (ast_begins_with(msg_to, "pjsip:")) {
msg_to += 6;
}
update_to_uri(tdata, msg_to);
ast_sip_update_to_uri(tdata, msg_to);
} else {
/*
* If there was no To in the message, it's still possible
@@ -1301,9 +665,9 @@ static int msg_send(void *data)
}
if (!ast_strlen_zero(mdata->from)) {
update_from(tdata, mdata->from);
ast_sip_update_from(tdata, mdata->from);
} else if (!ast_strlen_zero(ast_msg_get_from(mdata->msg))) {
update_from(tdata, (char *)ast_msg_get_from(mdata->msg));
ast_sip_update_from(tdata, (char *)ast_msg_get_from(mdata->msg));
}
#ifdef TEST_FRAMEWORK