#include "asterisk/logger.h"
#include "asterisk/module.h"
#include "asterisk/strings.h"
+#include "asterisk/vector.h"
-static pjsip_www_authenticate_hdr *get_auth_header(pjsip_rx_data *challenge,
- const void *start)
-{
- pjsip_hdr_e search_type;
+pj_str_t supported_digest_algorithms[] = {
+ { "MD5", 3}
+};
+/*!
+ * \internal
+ * \brief Determine proper authenticate header
+ *
+ * We need to search for different headers depending on whether
+ * the response code from the UAS/Proxy was 401 or 407.
+ */
+static pjsip_hdr_e get_auth_search_type(pjsip_rx_data *challenge)
+{
if (challenge->msg_info.msg->line.status.code == PJSIP_SC_UNAUTHORIZED) {
- search_type = PJSIP_H_WWW_AUTHENTICATE;
+ return PJSIP_H_WWW_AUTHENTICATE;
} else if (challenge->msg_info.msg->line.status.code == PJSIP_SC_PROXY_AUTHENTICATION_REQUIRED) {
- search_type = PJSIP_H_PROXY_AUTHENTICATE;
+ return PJSIP_H_PROXY_AUTHENTICATE;
} else {
ast_log(LOG_ERROR,
"Status code %d was received when it should have been 401 or 407.\n",
challenge->msg_info.msg->line.status.code);
- return NULL ;
+ return PJSIP_H_OTHER;
}
+}
- return pjsip_msg_find_hdr(challenge->msg_info.msg, search_type, start);
+/*!
+ * \internal
+ * \brief Determine if digest algorithm in the header is one we support
+ *
+ * \retval 1 If we support the algorithm
+ * \retval 0 If we do not
+ *
+ */
+static int is_digest_algorithm_supported(pjsip_www_authenticate_hdr *auth_hdr)
+{
+ int digest;
+ /* An empty digest is assumed to be md5 */
+ if (pj_strlen(&auth_hdr->challenge.digest.algorithm) == 0) {
+ return 1;
+ }
+
+ for (digest = 0; digest < ARRAY_LEN(supported_digest_algorithms); digest++) {
+ if (pj_stricmp(&auth_hdr->challenge.digest.algorithm, &supported_digest_algorithms[digest]) == 0) {
+ return 1;
+ }
+ }
+ return 0;
}
-static int set_outbound_authentication_credentials(pjsip_auth_clt_sess *auth_sess,
- const struct ast_sip_auth_vector *auth_vector, pjsip_rx_data *challenge,
- pjsip_www_authenticate_hdr *auth_hdr)
+/*!
+ * \internal
+ * \brief Initialize pjproject with a valid set of credentials
+ *
+ * RFC7616 and RFC8760 allow more than one WWW-Authenticate or
+ * Proxy-Authenticate header per realm, each with different digest
+ * algorithms (including new ones like SHA-256 and SHA-512-256). However,
+ * thankfully, a UAS can NOT send back multiple Authenticate headers for
+ * the same realm with the same digest algorithm. The UAS is also
+ * supposed to send the headers in order of preference with the first one
+ * being the most preferred.
+ *
+ * We're supposed to send an Authorization header for the first one we
+ * encounter for a realm that we can support.
+ *
+ * The UAS can also send multiple realms, especially when it's a proxy
+ * that has forked the request in which case the proxy will aggregate all
+ * of the Authenticate and then them all back to the UAC.
+ *
+ * It doesn't stop there though... Each realm can require a different
+ * username from the others. There's also nothing preventing each digest
+ * algorithm from having a unique password although I'm not sure if
+ * that adds any benefit.
+ *
+ * So now... For each Authenticate header we encounter, we have to
+ * determine if we support the digest algorithm and, if not, just skip the
+ * header. We then have to find an auth object that matches the realm AND
+ * the digest algorithm or find a wildcard object that matches the digest
+ * algorithm. If we find one, we add it to the results vector and read the
+ * next Authenticate header. If the next header is for the same realm AND
+ * we already added an auth object for that realm, we skip the header.
+ * Otherwise we repeat the process for the next header.
+ *
+ * In the end, we'll have accumulated a list of credentials we can pass to
+ * pjproject that it can use to add Authentication headers to a request.
+ *
+ * \NOTE: Neither we nor pjproject can currently handle digest algorithms
+ * other than MD5. We don't even have a place for it in the ast_sip_auth
+ * object. For this reason, we just skip processing any Authenticate
+ * header that's not MD5. When we support the others, we'll move the
+ * check into the loop that searches the objects.
+ */
+static pj_status_t set_outbound_authentication_credentials(pjsip_auth_clt_sess *auth_sess,
+ const struct ast_sip_auth_objects_vector *auth_objects_vector, pjsip_rx_data *challenge,
+ struct ast_str **realms)
{
- size_t auth_size = AST_VECTOR_SIZE(auth_vector);
- struct ast_sip_auth **auths = ast_alloca(auth_size * sizeof(*auths));
- pjsip_cred_info *auth_creds = ast_alloca(auth_size * sizeof(*auth_creds));
- int res = 0;
int i;
+ size_t auth_object_count;
+ pjsip_www_authenticate_hdr *auth_hdr = NULL;
+ pj_status_t res = PJ_SUCCESS;
+ pjsip_hdr_e search_type;
+ size_t cred_count;
+ pjsip_cred_info *creds_array;
- if (ast_sip_retrieve_auths(auth_vector, auths)) {
- res = -1;
- goto cleanup;
+ /*
+ * Normally vector elements are pointers to something else, usually
+ * structures. In this case however, the elements are the
+ * structures themselves instead of pointers to them. This is due
+ * to the fact that pjsip_auth_clt_set_credentials() expects an
+ * array of structues, not an array of pointers to structures.
+ * Thankfully, vectors allow you to "steal" their underlying
+ * arrays, in this case an array of pjsip_cred_info structures,
+ * which we'll pass to pjsip_auth_clt_set_credentials() at the
+ * end.
+ */
+ AST_VECTOR(cred_info, pjsip_cred_info) auth_creds;
+
+ search_type = get_auth_search_type(challenge);
+ if (search_type == PJSIP_H_OTHER) {
+ /*
+ * The status code on the response wasn't 401 or 407
+ * so there are no WWW-Authenticate or Proxy-Authenticate
+ * headers to process.
+ */
+ return PJ_ENOTSUP;
}
- for (i = 0; i < auth_size; ++i) {
- if (ast_strlen_zero(auths[i]->realm)) {
- auth_creds[i].realm = auth_hdr->challenge.common.realm;
+ auth_object_count = AST_VECTOR_SIZE(auth_objects_vector);
+ if (auth_object_count == 0) {
+ /* This shouldn't happen but we'll check anyway. */
+ return PJ_EINVAL;
+ }
+
+ /*
+ * The number of pjsip_cred_infos we send to pjproject can
+ * vary based on the number of acceptable headers received
+ * and the number of acceptable auth objects on the endpoint
+ * so we just use a vector to accumulate them.
+ *
+ * NOTE: You have to call AST_VECTOR_FREE() on the vector
+ * but you don't have to free the elements because they're
+ * actual structures, not pointers to structures.
+ */
+ if (AST_VECTOR_INIT(&auth_creds, 5) != 0) {
+ return PJ_ENOMEM;
+ }
+
+ /*
+ * It's going to be rare that we actually have more than one
+ * WWW-Authentication header or more than one auth object to
+ * match to it so the following nested loop should be fine.
+ */
+ while ((auth_hdr = pjsip_msg_find_hdr(challenge->msg_info.msg,
+ search_type, auth_hdr ? auth_hdr->next : NULL))) {
+ int exact_match_index = -1;
+ int wildcard_match_index = -1;
+ int match_index = 0;
+ pjsip_cred_info auth_cred;
+ struct ast_sip_auth *auth = NULL;
+
+ memset(&auth_cred, 0, sizeof(auth_cred));
+ /*
+ * Since we only support the MD5 algorithm at the current time,
+ * there's no sense searching for auth objects that match the algorithm.
+ * In fact, the auth_object structure doesn't even have a member
+ * for it.
+ *
+ * When we do support more algorithms, this check will need to be
+ * moved inside the auth object loop below.
+ *
+ * Note: The header may not have specified an algorithm at all in which
+ * case it's assumed to be MD5. is_digest_algorithm_supported() returns
+ * true for that case.
+ */
+ if (!is_digest_algorithm_supported(auth_hdr)) {
+ ast_debug(3, "Skipping header with realm '%.*s' and unsupported '%.*s' algorithm \n",
+ (int)auth_hdr->challenge.digest.realm.slen, auth_hdr->challenge.digest.realm.ptr,
+ (int)auth_hdr->challenge.digest.algorithm.slen, auth_hdr->challenge.digest.algorithm.ptr);
+ continue;
+ }
+
+ /*
+ * Appending the realms is strictly so digest_create_request_with_auth()
+ * can display good error messages. Since we only support one algorithm,
+ * there can't be more than one header with the same realm. No need to worry
+ * about duplicate realms until then.
+ */
+ if (*realms) {
+ ast_str_append(realms, 0, "%.*s, ",
+ (int)auth_hdr->challenge.digest.realm.slen, auth_hdr->challenge.digest.realm.ptr);
+ }
+
+ ast_debug(3, "Searching auths to find matching ones for header with realm '%.*s' and algorithm '%.*s'\n",
+ (int)auth_hdr->challenge.digest.realm.slen, auth_hdr->challenge.digest.realm.ptr,
+ (int)auth_hdr->challenge.digest.algorithm.slen, auth_hdr->challenge.digest.algorithm.ptr);
+
+ /*
+ * Now that we have a valid header, we can loop over the auths available to
+ * find either an exact realm match or, failing that, a wildcard auth (an
+ * auth with an empty or "*" realm).
+ *
+ * NOTE: We never use the global default realm when we're the UAC responding
+ * to a 401 or 407. We only use that when we're the UAS (handled elsewhere)
+ * and the auth object didn't have a realm.
+ */
+ for (i = 0; i < auth_object_count; ++i) {
+ auth = AST_VECTOR_GET(auth_objects_vector, i);
+
+ /*
+ * If this auth object's realm exactly matches the one
+ * from the header, we can just break out and use it.
+ *
+ * NOTE: If there's more than one auth object for an endpoint with
+ * a matching realm it's a misconfiguration. We'll only use the first.
+ */
+ if (pj_stricmp2(&auth_hdr->challenge.digest.realm, auth->realm) == 0) {
+ ast_debug(3, "Found matching auth '%s' with realm '%s'\n", ast_sorcery_object_get_id(auth),
+ auth->realm);
+ exact_match_index = i;
+ /*
+ * If we found an exact realm match, there's no need to keep
+ * looking for a wildcard.
+ */
+ break;
+ }
+
+ /*
+ * If this auth object's realm is empty or a "*", it's a wildcard
+ * auth object. We going to save its index but keep iterating over
+ * the vector in case we find an exact match later.
+ *
+ * NOTE: If there's more than one wildcard auth object for an endpoint
+ * it's a misconfiguration. We'll only use the first.
+ */
+ if (wildcard_match_index < 0
+ && (ast_strlen_zero(auth->realm) || ast_strings_equal(auth->realm, "*"))) {
+ ast_debug(3, "Found wildcard auth '%s' for realm '%.*s'\n", ast_sorcery_object_get_id(auth),
+ (int)auth_hdr->challenge.digest.realm.slen, auth_hdr->challenge.digest.realm.ptr);
+ wildcard_match_index = i;
+ }
+ }
+
+ if (exact_match_index < 0 && wildcard_match_index < 0) {
+ /*
+ * Didn't find either a wildcard or an exact realm match.
+ * Move on to the next header.
+ */
+ ast_debug(3, "No auth matching realm or no wildcard found for realm '%.*s'\n",
+ (int)auth_hdr->challenge.digest.realm.slen, auth_hdr->challenge.digest.realm.ptr);
+ continue;
+ }
+
+ if (exact_match_index >= 0) {
+ /*
+ * If we found an exact match, we'll always prefer that.
+ */
+ match_index = exact_match_index;
+ auth = AST_VECTOR_GET(auth_objects_vector, match_index);
+ ast_debug(3, "Using matched auth '%s' with realm '%.*s'\n", ast_sorcery_object_get_id(auth),
+ (int)auth_hdr->challenge.digest.realm.slen, auth_hdr->challenge.digest.realm.ptr);
} else {
- pj_cstr(&auth_creds[i].realm, auths[i]->realm);
+ /*
+ * We'll only use the wildcard if we didn't find an exact match.
+ */
+ match_index = wildcard_match_index;
+ auth = AST_VECTOR_GET(auth_objects_vector, match_index);
+ ast_debug(3, "Using wildcard auth '%s' for realm '%.*s'\n", ast_sorcery_object_get_id(auth),
+ (int)auth_hdr->challenge.digest.realm.slen, auth_hdr->challenge.digest.realm.ptr);
}
- pj_cstr(&auth_creds[i].username, auths[i]->auth_user);
- pj_cstr(&auth_creds[i].scheme, "digest");
- switch (auths[i]->type) {
+
+ /*
+ * Copy the fields from the auth_object to the
+ * pjsip_cred_info structure.
+ */
+ auth_cred.realm = auth_hdr->challenge.common.realm;
+ pj_cstr(&auth_cred.username, auth->auth_user);
+ pj_cstr(&auth_cred.scheme, "digest");
+ switch (auth->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;
+ pj_cstr(&auth_cred.data, auth->auth_pass);
+ auth_cred.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;
+ pj_cstr(&auth_cred.data, auth->md5_creds);
+ auth_cred.data_type = PJSIP_CRED_DATA_DIGEST;
break;
case AST_SIP_AUTH_TYPE_GOOGLE_OAUTH:
/* nothing to do. handled seperately in res_pjsip_outbound_registration */
break;
case AST_SIP_AUTH_TYPE_ARTIFICIAL:
- ast_log(LOG_ERROR, "Trying to set artificial outbound auth credentials shouldn't happen.\n");
- break;
+ ast_log(LOG_ERROR,
+ "Trying to set artificial outbound auth credentials shouldn't happen.\n");
+ continue;
+ } /* End auth object loop */
+
+ /*
+ * Because the vector contains actual structures and not pointers
+ * to structures, the call to AST_VECTOR_APPEND results in a simple
+ * assign of one structure to another, effectively copying the auth_cred
+ * structure contents to the array element.
+ *
+ * Also note that the calls to pj_cstr above set their respective
+ * auth_cred fields to the _pointers_ of their corresponding auth
+ * object fields. This is safe because the call to
+ * pjsip_auth_clt_set_credentials() below strdups them before we
+ * return to the calling function which decrements the reference
+ * counts.
+ */
+ res = AST_VECTOR_APPEND(&auth_creds, auth_cred);
+ if (res != PJ_SUCCESS) {
+ res = PJ_ENOMEM;
+ goto cleanup;
}
+ } /* End header loop */
+
+ if (*realms && ast_str_strlen(*realms)) {
+ /*
+ * Again, this is strictly so digest_create_request_with_auth()
+ * can display good error messages.
+ *
+ * Chop off the trailing ", " on the last realm.
+ */
+ ast_str_truncate(*realms, ast_str_strlen(*realms) - 2);
}
- pjsip_auth_clt_set_credentials(auth_sess, auth_size, auth_creds);
+ if (AST_VECTOR_SIZE(&auth_creds) == 0) {
+ /* No matching auth objects were found. */
+ res = PJSIP_ENOCREDENTIAL;
+ goto cleanup;
+ }
+
+ /*
+ * Here's where we steal the cred info structures from the vector.
+ *
+ * The steal effectively returns a pointer to the underlying
+ * array of pjsip_cred_info structures which is exactly what we need
+ * to pass to pjsip_auth_clt_set_credentials().
+ *
+ * <struct cred info><struct cred info>...<struct cred info>
+ * ^pointer
+ *
+ * Since we stole the array from the vector, we have to free it ourselves.
+ *
+ * We also have to copy the size before we steal because stealing
+ * resets the vector size to 0.
+ */
+ cred_count = AST_VECTOR_SIZE(&auth_creds);
+ creds_array = AST_VECTOR_STEAL_ELEMENTS(&auth_creds);
+
+ res = pjsip_auth_clt_set_credentials(auth_sess, cred_count, creds_array);
+ ast_free(creds_array);
+ if (res == PJ_SUCCESS) {
+ ast_debug(3, "Set %"PRIu64" credentials in auth session\n", cred_count);
+ } else {
+ ast_log(LOG_ERROR, "Failed to set %"PRIu64" credentials in auth session\n", cred_count);
+ }
cleanup:
- ast_sip_cleanup_auths(auths, auth_size);
+ AST_VECTOR_FREE(&auth_creds);
return res;
}
-static int digest_create_request_with_auth(const struct ast_sip_auth_vector *auths,
+/*!
+ * \internal
+ * \brief Create new tdata with auth based on original tdata
+ * \param auth_ids_vector Vector of auth IDs retrieved from endpoint
+ * \param challenge rdata of the response from the UAS with challenge
+ * \param old_request tdata from the original request
+ * \param new_request tdata of the new request with the auth
+ *
+ * This function is what's registered with ast_sip_register_outbound_authenticator()
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ */
+static int digest_create_request_with_auth(const struct ast_sip_auth_vector *auth_ids_vector,
pjsip_rx_data *challenge, pjsip_tx_data *old_request, pjsip_tx_data **new_request)
{
pjsip_auth_clt_sess auth_sess;
pjsip_cseq_hdr *cseq;
pj_status_t status;
+ struct ast_sip_auth_objects_vector auth_objects_vector;
+ size_t auth_object_count = 0;
struct ast_sip_endpoint *endpoint;
char *id = NULL;
const char *id_type;
- pjsip_www_authenticate_hdr *auth_hdr;
- struct ast_str *realms;
+ struct ast_str *realms = NULL;
pjsip_dialog *dlg;
+ int res = -1;
+
+ /*
+ * Some older compilers have an issue with initializing structures with
+ * pjsip_auth_clt_sess auth_sess = { 0, };
+ * so we'll just do it the old fashioned way.
+ */
+ memset(&auth_sess, 0, sizeof(auth_sess));
dlg = pjsip_rdata_get_dlg(challenge);
if (dlg) {
+ /* The only thing we use endpoint for is to get an id for error/debug messages */
endpoint = ast_sip_dialog_get_endpoint(dlg);
id = endpoint ? ast_strdupa(ast_sorcery_object_get_id(endpoint)) : NULL;
ao2_cleanup(endpoint);
id_type = "Endpoint";
}
+
/* If there was no dialog, then this is probably a REGISTER so no endpoint */
if (!id) {
+ /* The only thing we use the address for is to get an id for error/debug messages */
id = ast_alloca(AST_SOCKADDR_BUFLEN);
pj_sockaddr_print(&challenge->pkt_info.src_addr, id, AST_SOCKADDR_BUFLEN, 3);
id_type = "Host";
}
- auth_hdr = get_auth_header(challenge, NULL);
- if (auth_hdr == NULL) {
- ast_log(LOG_ERROR, "%s: '%s': Unable to find authenticate header in challenge.\n",
- id_type, id);
+ if (!auth_ids_vector || AST_VECTOR_SIZE(auth_ids_vector) == 0) {
+ ast_log(LOG_ERROR, "%s: '%s': There were no auth ids available\n", id_type, id);
+ return -1;
+ }
+
+ if (AST_VECTOR_INIT(&auth_objects_vector, AST_VECTOR_SIZE(auth_ids_vector)) != 0) {
+ ast_log(LOG_ERROR, "%s: '%s': Couldn't initialize auth object vector\n", id_type, id);
return -1;
}
+ /*
+ * We don't really care about ast_sip_retrieve_auths_vector()'s return code
+ * because we're checking the count of objects in the vector.
+ *
+ * Don't forget to call
+ * ast_sip_cleanup_auth_objects_vector(&auth_objects_vector);
+ * AST_VECTOR_FREE(&auth_objects_vector);
+ * when you're done with the vector
+ */
+ ast_sip_retrieve_auths_vector(auth_ids_vector, &auth_objects_vector);
+ auth_object_count = AST_VECTOR_SIZE(&auth_objects_vector);
+ if (auth_object_count == 0) {
+ /*
+ * If none of the auth ids were found, we can't continue.
+ * We're OK if there's at least one left.
+ * ast_sip_retrieve_auths_vector() will print a warning for every
+ * id that wasn't found.
+ */
+ res = -1;
+ goto cleanup;
+ }
+
if (pjsip_auth_clt_init(&auth_sess, ast_sip_get_pjsip_endpoint(),
old_request->pool, 0) != PJ_SUCCESS) {
ast_log(LOG_ERROR, "%s: '%s': Failed to initialize client authentication session\n",
id_type, id);
- return -1;
+ res = -1;
+ goto cleanup;
}
- if (set_outbound_authentication_credentials(&auth_sess, auths, challenge, auth_hdr)) {
- ast_log(LOG_WARNING, "%s: '%s': Failed to set authentication credentials\n",
- id_type, id);
-#if defined(HAVE_PJSIP_AUTH_CLT_DEINIT)
- /* In case it is not a noop here in the future. */
- pjsip_auth_clt_deinit(&auth_sess);
-#endif
- return -1;
+ /*
+ * realms is used only for displaying good error messages.
+ */
+ realms = ast_str_create(32);
+ if (!realms) {
+ res = -1;
+ goto cleanup;
}
+ /*
+ * Load pjproject with the valid credentials for the Authentication headers
+ * received on the 401 or 407 response.
+ */
+ status = set_outbound_authentication_credentials(&auth_sess, &auth_objects_vector, challenge, &realms);
+ switch (status) {
+ case PJ_SUCCESS:
+ break;
+ case PJSIP_ENOCREDENTIAL:
+ ast_log(LOG_WARNING,
+ "%s: '%s': No auth objects matching realm(s) '%s' from challenge found.\n", id_type, id,
+ realms ? ast_str_buffer(realms) : "<none>");
+ res = -1;
+ goto cleanup;
+ default:
+ ast_log(LOG_WARNING, "%s: '%s': Failed to set authentication credentials\n", id_type, id);
+ res = -1;
+ goto cleanup;
+ }
+
+ /*
+ * reinit_req actually creates the Authorization headers to send on
+ * the next request. If reinit_req already has a cached credential
+ * from an earlier successful authorization, it'll use it. Otherwise
+ * it'll create a new authorization and cache it.
+ */
status = pjsip_auth_clt_reinit_req(&auth_sess, challenge, old_request, new_request);
-#if defined(HAVE_PJSIP_AUTH_CLT_DEINIT)
- /* Release any cached auths */
- pjsip_auth_clt_deinit(&auth_sess);
-#endif
switch (status) {
case PJ_SUCCESS:
cseq = pjsip_msg_find_hdr((*new_request)->msg, PJSIP_H_CSEQ, NULL);
ast_assert(cseq != NULL);
++cseq->cseq;
- return 0;
+ res = 0;
+ goto cleanup;
case PJSIP_ENOCREDENTIAL:
- realms = ast_str_create(32);
- if (realms) {
- ast_str_append(&realms, 0, "%.*s", (int)auth_hdr->challenge.common.realm.slen,
- auth_hdr->challenge.common.realm.ptr);
- while((auth_hdr = get_auth_header(challenge, auth_hdr->next))) {
- ast_str_append(&realms, 0, ",%.*s", (int)auth_hdr->challenge.common.realm.slen,
- auth_hdr->challenge.common.realm.ptr);
- }
- }
+ /*
+ * This should be rare since set_outbound_authentication_credentials()
+ * did the matching but you never know.
+ */
ast_log(LOG_WARNING,
- "%s: '%s': Unable to create request with auth. "
- "No auth credentials for realm(s) '%s' in challenge.\n", id_type, id,
- realms ? ast_str_buffer(realms) : "<unknown>");
- ast_free(realms);
+ "%s: '%s': No auth objects matching realm(s) '%s' from challenge found.\n", id_type, id,
+ realms ? ast_str_buffer(realms) : "<none>");
break;
case PJSIP_EAUTHSTALECOUNT:
ast_log(LOG_WARNING,
id_type, id);
break;
}
+ res = -1;
+
+cleanup:
+#if defined(HAVE_PJSIP_AUTH_CLT_DEINIT)
+ /* Release any cached auths */
+ pjsip_auth_clt_deinit(&auth_sess);
+#endif
+
+ ast_sip_cleanup_auth_objects_vector(&auth_objects_vector);
+ AST_VECTOR_FREE(&auth_objects_vector);
+ ast_free(realms);
- return -1;
+ return res;
}
static struct ast_sip_outbound_authenticator digest_authenticator = {
--- /dev/null
+From bdbeb7c4b2b11efc2e59f5dee7aa4360a2bc9fff Mon Sep 17 00:00:00 2001
+From: sauwming <ming@teluu.com>
+Date: Thu, 22 Apr 2021 14:03:28 +0800
+Subject: [PATCH 90/90] Skip unsupported digest algorithm (#2408)
+
+Co-authored-by: Nanang Izzuddin <nanang@teluu.com>
+---
+ pjsip/src/pjsip/sip_auth_client.c | 32 +++++--
+ tests/pjsua/scripts-sipp/uas-auth-two-algo.py | 7 ++
+ .../pjsua/scripts-sipp/uas-auth-two-algo.xml | 83 +++++++++++++++++++
+ 3 files changed, 117 insertions(+), 5 deletions(-)
+ create mode 100644 tests/pjsua/scripts-sipp/uas-auth-two-algo.py
+ create mode 100644 tests/pjsua/scripts-sipp/uas-auth-two-algo.xml
+
+diff --git a/pjsip/src/pjsip/sip_auth_client.c b/pjsip/src/pjsip/sip_auth_client.c
+index 828b04db9..7eb2f5cd1 100644
+--- a/pjsip/src/pjsip/sip_auth_client.c
++++ b/pjsip/src/pjsip/sip_auth_client.c
+@@ -1042,7 +1042,7 @@ static pj_status_t process_auth( pj_pool_t *req_pool,
+ pjsip_hdr *hdr;
+ pj_status_t status;
+
+- /* See if we have sent authorization header for this realm */
++ /* See if we have sent authorization header for this realm (and scheme) */
+ hdr = tdata->msg->hdr.next;
+ while (hdr != &tdata->msg->hdr) {
+ if ((hchal->type == PJSIP_H_WWW_AUTHENTICATE &&
+@@ -1052,7 +1052,8 @@ static pj_status_t process_auth( pj_pool_t *req_pool,
+ {
+ sent_auth = (pjsip_authorization_hdr*) hdr;
+ if (pj_stricmp(&hchal->challenge.common.realm,
+- &sent_auth->credential.common.realm )==0)
++ &sent_auth->credential.common.realm)==0 &&
++ pj_stricmp(&hchal->scheme, &sent_auth->scheme)==0)
+ {
+ /* If this authorization has empty response, remove it. */
+ if (pj_stricmp(&sent_auth->scheme, &pjsip_DIGEST_STR)==0 &&
+@@ -1062,6 +1063,14 @@ static pj_status_t process_auth( pj_pool_t *req_pool,
+ hdr = hdr->next;
+ pj_list_erase(sent_auth);
+ continue;
++ } else
++ if (pj_stricmp(&sent_auth->scheme, &pjsip_DIGEST_STR)==0 &&
++ pj_stricmp(&sent_auth->credential.digest.algorithm,
++ &hchal->challenge.digest.algorithm)!=0)
++ {
++ /* Same 'digest' scheme but different algo */
++ hdr = hdr->next;
++ continue;
+ } else {
+ /* Found previous authorization attempt */
+ break;
+@@ -1155,9 +1164,10 @@ PJ_DEF(pj_status_t) pjsip_auth_clt_reinit_req( pjsip_auth_clt_sess *sess,
+ {
+ pjsip_tx_data *tdata;
+ const pjsip_hdr *hdr;
+- unsigned chal_cnt;
++ unsigned chal_cnt, auth_cnt;
+ pjsip_via_hdr *via;
+ pj_status_t status;
++ pj_status_t last_auth_err;
+
+ PJ_ASSERT_RETURN(sess && rdata && old_request && new_request,
+ PJ_EINVAL);
+@@ -1178,6 +1188,8 @@ PJ_DEF(pj_status_t) pjsip_auth_clt_reinit_req( pjsip_auth_clt_sess *sess,
+ */
+ hdr = rdata->msg_info.msg->hdr.next;
+ chal_cnt = 0;
++ auth_cnt = 0;
++ last_auth_err = PJSIP_EAUTHNOAUTH;
+ while (hdr != &rdata->msg_info.msg->hdr) {
+ pjsip_cached_auth *cached_auth;
+ const pjsip_www_authenticate_hdr *hchal;
+@@ -1222,8 +1234,13 @@ PJ_DEF(pj_status_t) pjsip_auth_clt_reinit_req( pjsip_auth_clt_sess *sess,
+ */
+ status = process_auth(tdata->pool, hchal, tdata->msg->line.req.uri,
+ tdata, sess, cached_auth, &hauth);
+- if (status != PJ_SUCCESS)
+- return status;
++ if (status != PJ_SUCCESS) {
++ last_auth_err = status;
++
++ /* Process next header. */
++ hdr = hdr->next;
++ continue;
++ }
+
+ if (pj_pool_get_used_size(cached_auth->pool) >
+ PJSIP_AUTH_CACHED_POOL_MAX_SIZE)
+@@ -1236,12 +1253,17 @@ PJ_DEF(pj_status_t) pjsip_auth_clt_reinit_req( pjsip_auth_clt_sess *sess,
+
+ /* Process next header. */
+ hdr = hdr->next;
++ auth_cnt++;
+ }
+
+ /* Check if challenge is present */
+ if (chal_cnt == 0)
+ return PJSIP_EAUTHNOCHAL;
+
++ /* Check if any authorization header has been created */
++ if (auth_cnt == 0)
++ return last_auth_err;
++
+ /* Remove branch param in Via header. */
+ via = (pjsip_via_hdr*) pjsip_msg_find_hdr(tdata->msg, PJSIP_H_VIA, NULL);
+ via->branch_param.slen = 0;
+diff --git a/tests/pjsua/scripts-sipp/uas-auth-two-algo.py b/tests/pjsua/scripts-sipp/uas-auth-two-algo.py
+new file mode 100644
+index 000000000..c79c9f6d3
+--- /dev/null
++++ b/tests/pjsua/scripts-sipp/uas-auth-two-algo.py
+@@ -0,0 +1,7 @@
++# $Id$
++#
++import inc_const as const
++
++PJSUA = ["--null-audio --max-calls=1 --id=sip:a@localhost --username=a --realm=* --registrar=$SIPP_URI"]
++
++PJSUA_EXPECTS = [[0, "registration success", ""]]
+diff --git a/tests/pjsua/scripts-sipp/uas-auth-two-algo.xml b/tests/pjsua/scripts-sipp/uas-auth-two-algo.xml
+new file mode 100644
+index 000000000..bd4871940
+--- /dev/null
++++ b/tests/pjsua/scripts-sipp/uas-auth-two-algo.xml
+@@ -0,0 +1,83 @@
++<?xml version="1.0" encoding="ISO-8859-1" ?>
++<!DOCTYPE scenario SYSTEM "sipp.dtd">
++
++<scenario name="Basic UAS responder">
++ <recv request="REGISTER" crlf="true">
++ </recv>
++
++ <send>
++ <![CDATA[
++ SIP/2.0 100 Trying
++ [last_Via:];received=1.1.1.1;rport=1111
++ [last_From:]
++ [last_To:];tag=[call_number]
++ [last_Call-ID:]
++ [last_CSeq:]
++ Content-Length: 0
++ ]]>
++ </send>
++
++ <send>
++ <![CDATA[
++ SIP/2.0 401 Unauthorized
++ [last_Via:];received=1.1.1.1;rport=1111
++ [last_From:]
++ [last_To:];tag=[call_number]
++ [last_Call-ID:]
++ [last_CSeq:]
++ WWW-Authenticate: Digest realm="sip.linphone.org", nonce="PARV4gAAAADgw3asAADW8zsi5BEAAAAA", opaque="+GNywA==", algorithm=SHA-256, qop="auth"
++ WWW-Authenticate: Digest realm="sip.linphone.org", nonce="PARV4gAAAADgw3asAADW8zsi5BEAAAAA", opaque="+GNywA==", algorithm=MD5, qop="auth"
++ WWW-Authenticate: Digest realm="sip.linphone.org", nonce="PARV4gAAAADgw3asAADW8zsi5BEAAAAA", opaque="+GNywA==", algorithm=MD2, qop="auth"
++ Content-Length: 0
++ ]]>
++ </send>
++
++ <recv request="REGISTER" crlf="true">
++ <action>
++ <ereg regexp=".*"
++ search_in="hdr"
++ header="Authorization:"
++ assign_to="have_auth" />
++ </action>
++ </recv>
++
++ <nop next="resp_okay" test="have_auth" />
++
++ <send next="end">
++ <![CDATA[
++ SIP/2.0 403 no auth
++ [last_Via:];received=1.1.1.1;rport=1111
++ [last_From:]
++ [last_To:];tag=[call_number]
++ [last_Call-ID:]
++ [last_CSeq:]
++ [last_Contact:]
++ Content-Length: 0
++ ]]>
++ </send>
++
++ <label id="resp_okay" />
++
++ <send>
++ <![CDATA[
++ SIP/2.0 200 OK
++ [last_Via:];received=1.1.1.1;rport=1111
++ [last_From:]
++ [last_To:];tag=[call_number]
++ [last_Call-ID:]
++ [last_CSeq:]
++ [last_Contact:]
++ Content-Length: 0
++ ]]>
++ </send>
++
++ <label id="end" />
++
++ <!-- definition of the response time repartition table (unit is ms) -->
++ <ResponseTimeRepartition value="10, 20, 30, 40, 50, 100, 150, 200"/>
++
++ <!-- definition of the call length repartition table (unit is ms) -->
++ <CallLengthRepartition value="10, 50, 100, 500, 1000, 5000, 10000"/>
++
++</scenario>
++
+--
+2.31.1
+