[Asterisk-code-review] res_pjsip_outbound_authenticator_digest: Be tolerant of RFC8760 UASs (asterisk[master])

George Joseph asteriskteam at digium.com
Thu May 13 14:15:49 CDT 2021


George Joseph has uploaded this change for review. ( https://gerrit.asterisk.org/c/asterisk/+/15904 )


Change subject: res_pjsip_outbound_authenticator_digest: Be tolerant of RFC8760 UASs
......................................................................

res_pjsip_outbound_authenticator_digest: Be tolerant of RFC8760 UASs

RFC7616 and RFC8760 allow more than one WWW-Authenticate or
Proxy-Autnenticate header per realm, each with different digest
algorithms (including new ones like SHA-256 and SHA-512-256).
Thankfully however 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 headers and then send 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.

Changes:

 * Added a new API ast_sip_retrieve_auths_vector() that takes in
   a vector of auth ids (usually supplied on a call to
   ast_sip_create_request_with_auth()) and populates another
   vector with the actual objects.

 * Refactored res_pjsip_outbound_authenticator_digest to handle
   multiple Authenticate headers and set the stage for handling
   additional digest algorithms.

 * Added a pjproject patch that allows them to ignore digest
   algorithms they don't support.  This patch has already been
   merged upstream.

 * Updated documentation for auth objects in the XML and
   in pjsip.conf.sample.

 * Although res_pjsip_authenticator_digest isn't affected
   by this change, some debugging and a testsuite AMI event
   was added to facilitate testing.

Discovered during OpenSIPit 2021.

ASTERISK-29397

Change-Id: I3aef5ce4fe1d27e48d61268520f284d15d650281
---
M configs/samples/pjsip.conf.sample
M include/asterisk/res_pjsip.h
M res/res_pjsip.c
M res/res_pjsip/pjsip_configuration.c
M res/res_pjsip_authenticator_digest.c
M res/res_pjsip_outbound_authenticator_digest.c
A third-party/pjproject/patches/0090-Skip-unsupported-digest-algorithm-2408.patch
7 files changed, 771 insertions(+), 97 deletions(-)



  git pull ssh://gerrit.asterisk.org:29418/asterisk refs/changes/04/15904/1

diff --git a/configs/samples/pjsip.conf.sample b/configs/samples/pjsip.conf.sample
index 7b4c7aa..874fcc3 100644
--- a/configs/samples/pjsip.conf.sample
+++ b/configs/samples/pjsip.conf.sample
@@ -971,10 +971,11 @@
                ; The input to the hash function must be in the
                ; following format:
                ; <username>:<realm>:<password>
-               ; For incoming authentication (asterisk is the server),
+               ; For incoming authentication (asterisk is the UAS),
                ; the realm must match either the realm set in this object
                ; or the default set in in the "global" object.
-               ; For outgoing authentication (asterisk is the client),
+               ;
+               ; For outgoing authentication (asterisk is the UAC),
                ; the realm must match what the server will be sending
                ; in their WWW-Authenticate header.  It can't be blank
                ; unless you expect the server to be sending a blank
@@ -985,16 +986,22 @@
                ; Note the '-n'.  You don't want a newline to be part
                ; of the hash.  (default: "")
 ;password=     ; PlainText password used for authentication (default: "")
-;realm=        ; For incoming authentication (asterisk is the server),
+;realm=        ; For incoming authentication (asterisk is the UAS),
                ; this is the realm to be sent on WWW-Authenticate
                ; headers.  If not specified, the global object's
                ; "default_realm" will be used.
-               ; For outgoing authentication (asterisk is the client), this
+               ;
+               ; For outgoing authentication (asterisk is the UAS), this
                ; must either be the realm the server is expected to send,
-               ; or blank to automatically use the realm sent by the server.
-               ; If you have multiple auth object for an endpoint, the realm
-               ; is also used to match the auth object to the realm the
-               ; server sends.  (default: "")
+               ; or left blank or contain a single '*' to automatically
+               ; use the realm sent by the server. If you have multiple
+               ; auth object for an endpoint, the realm is also used to
+               ; match the auth object to the realm the server sent.
+               ; Using the same auth section for inbound and outbound
+               ; authentication is not recommended.  There is a difference in
+               ; meaning for an empty realm setting between inbound and outbound
+               ; authentication uses.
+               ; (default: "")
 ;type=         ; Must be auth (default: "")
 ;username=     ; Username to use for account (default: "")
 
diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h
index 2020ca8..7083b25 100644
--- a/include/asterisk/res_pjsip.h
+++ b/include/asterisk/res_pjsip.h
@@ -2499,12 +2499,48 @@
  * Call this function once you have completed operating on auths
  * retrieved from \ref ast_sip_retrieve_auths
  *
- * \param auths An vector of auth structures to clean up
- * \param num_auths The number of auths in the vector
+ * \param auths An array of auth object pointers to clean up
+ * \param num_auths The number of auths in the array
  */
 void ast_sip_cleanup_auths(struct ast_sip_auth *auths[], size_t num_auths);
 
 /*!
+ * \brief Retrieve relevant SIP auth structures from sorcery as a vector
+ *
+ * \param auth_ids Vector of sorcery IDs of auth credentials to retrieve
+ * \param[out] auth_objects A pointer ast_sip_auth_objects_vector to hold the objects
+ *
+ * \retval 0 Success
+ * \retval -1 Number of auth objects found is less than the number of names supplied.
+ *
+ * \WARNING The number of auth objects retrieved may be less than the
+ * number of auth ids supplied if auth objects couldn't be found for
+ * some of them.
+ *
+ * \NOTE Since the ref count on all auith objects returned has been
+ * bumped, you must call ast_sip_cleanup_auth_objects_vector() to decrement
+ * the ref count on all of the auth objects in the vector,
+ * then call AST_VECTOR_FREE() on the vector itself.
+ *
+ */
+AST_VECTOR(ast_sip_auth_objects_vector, struct ast_sip_auth *);
+int ast_sip_retrieve_auths_vector(const struct ast_sip_auth_vector *auth_ids,
+	struct ast_sip_auth_objects_vector *auth_objects);
+
+/*!
+ * \brief Clean up retrieved auth objects in vector
+ *
+ * Call this function once you have completed operating on auths
+ * retrieved from \ref ast_sip_retrieve_auths_vector.  All
+ * auth objects will have their reference counts decremented and
+ * the vector size will be reset to 0.  You must still call
+ * AST_VECTOR_FREE() on the vector itself.
+ *
+ * \param auth_objects A vector of auth structures to clean up
+ */
+#define ast_sip_cleanup_auth_objects_vector(auth_objects) AST_VECTOR_RESET(auth_objects, ao2_cleanup)
+
+/*!
  * \brief Checks if the given content type matches type/subtype.
  *
  * Compares the pjsip_media_type with the passed type and subtype and
diff --git a/res/res_pjsip.c b/res/res_pjsip.c
index 775b63f..8c6fe73 100644
--- a/res/res_pjsip.c
+++ b/res/res_pjsip.c
@@ -1504,13 +1504,24 @@
 						This option specifies which of the password style config options should be read
 						when trying to authenticate an endpoint inbound request. If set to <literal>userpass</literal>
 						then we'll read from the 'password' option. For <literal>md5</literal> we'll read
-						from 'md5_cred'. If set to <literal>google_oauth</literal> then we'll read from the refresh_token/oauth_clientid/oauth_secret fields.
+						from 'md5_cred'.  The following values are valid:
 						</para>
 						<enumlist>
 							<enum name="md5"/>
 							<enum name="userpass"/>
 							<enum name="google_oauth"/>
 						</enumlist>
+						<para>
+							If set to <literal>google_oauth</literal> then we'll read from the
+							refresh_token/oauth_clientid/oauth_secret fields.						</para>
+						<note>
+							<para>
+								This setting only describes whether the password is in
+								plain text or has been pre-hashed with MD5.  It doesn't describe
+								the acceptable digest algorithms we'll accept in a received
+								challenge.
+							</para>
+						</note>
 					</description>
 				</configOption>
 				<configOption name="nonce_lifetime" default="32">
@@ -1542,11 +1553,12 @@
 						<para>
 						</para>
 						<para>
-						For outgoing authentication (asterisk is the client),
+						For outgoing authentication (asterisk is the UAC),
 						the realm must match what the server will be sending
 						in their WWW-Authenticate header.  It can't be blank
 						unless you expect the server to be sending a blank
-						realm in the header.
+						realm in the header.  You can't use pre-hashed
+						paswords with a wildcard auth object.
 						You can generate the hash with the following shell
 						command:
 						</para>
@@ -1578,7 +1590,7 @@
 				<configOption name="realm" default="">
 					<synopsis>SIP realm for endpoint</synopsis>
 					<description><para>
-						For incoming authentication (asterisk is the server),
+						For incoming authentication (asterisk is the UAS),
 						this is the realm to be sent on WWW-Authenticate
 						headers.  If not specified, the <replaceable>global</replaceable>
 						object's <variable>default_realm</variable> will be used.
@@ -1586,12 +1598,12 @@
 						<para>
 						</para>
 						<para>
-						For outgoing authentication (asterisk is the client), this
+						For outgoing authentication (asterisk is the UAS), this
 						must either be the realm the server is expected to send,
-						or blank to automatically use the realm sent by the server.
-						If you have multiple auth object for an endpoint, the realm
-						is also used to match the auth object to the realm the
-						server sent.
+						or left blank or contain a single '*' to automatically
+						use the realm sent by the server. If you have multiple
+						auth object for an endpoint, the realm is also used to
+						match the auth object to the realm the server sent.
 						</para>
 						<para>
 						</para>
@@ -1600,7 +1612,19 @@
 						Using the same auth section for inbound and outbound
 						authentication is not recommended.  There is a difference in
 						meaning for an empty realm setting between inbound and outbound
-						authentication uses.</para></note>
+						authentication uses.
+						</para>
+						</note>
+						<para>
+						</para>
+						<note>
+							<para>
+								If more than one auth object with the same realm or
+								more than one wildcard auth object associated to
+								an endpoint, we can only use the first one of
+								each defined on the endpoint.
+							</para>
+						</note>
 					</description>
 				</configOption>
 				<configOption name="type">
diff --git a/res/res_pjsip/pjsip_configuration.c b/res/res_pjsip/pjsip_configuration.c
index 6d12c6f..6defa7c 100644
--- a/res/res_pjsip/pjsip_configuration.c
+++ b/res/res_pjsip/pjsip_configuration.c
@@ -2382,6 +2382,25 @@
 	}
 }
 
+int ast_sip_retrieve_auths_vector(const struct ast_sip_auth_vector *auth_ids,
+	struct ast_sip_auth_objects_vector *auth_objects)
+{
+	int i;
+
+	for (i = 0; i < AST_VECTOR_SIZE(auth_ids); ++i) {
+		/* Using AST_VECTOR_GET is safe since the vector is immutable */
+		const char *name = AST_VECTOR_GET(auth_ids, i);
+		struct ast_sip_auth *auth_object = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), SIP_SORCERY_AUTH_TYPE, name);
+		if (!auth_object) {
+			ast_log(LOG_WARNING, "Auth object '%s' could not be found\n", name);
+		} else {
+			AST_VECTOR_APPEND(auth_objects, auth_object);
+		}
+	}
+
+	return AST_VECTOR_SIZE(auth_objects) == AST_VECTOR_SIZE(auth_ids) ? 0 : -1;
+}
+
 struct ast_sorcery *ast_sip_get_sorcery(void)
 {
 	return sip_sorcery;
diff --git a/res/res_pjsip_authenticator_digest.c b/res/res_pjsip_authenticator_digest.c
index 518ef73..231a7e5 100644
--- a/res/res_pjsip_authenticator_digest.c
+++ b/res/res_pjsip_authenticator_digest.c
@@ -24,6 +24,7 @@
 #include "asterisk/logger.h"
 #include "asterisk/module.h"
 #include "asterisk/strings.h"
+#include "asterisk/test.h"
 
 /*** MODULEINFO
 	<depend>pjproject</depend>
@@ -296,7 +297,7 @@
  */
 enum digest_verify_result {
 	/*! Authentication credentials incorrect */
-	AUTH_FAIL,
+	AUTH_FAIL = 0,
 	/*! Authentication credentials correct */
 	AUTH_SUCCESS,
 	/*! Authentication credentials correct but nonce mismatch */
@@ -305,6 +306,12 @@
 	AUTH_NOAUTH,
 };
 
+static char *verify_result_str[] = {
+	"FAIL",
+	"SUCCESS",
+	"STALE",
+	"NOAUTH"
+};
 /*!
  * \brief astobj2 callback for verifying incoming credentials
  *
@@ -320,6 +327,7 @@
 	int response_code;
 	pjsip_auth_srv auth_server;
 	int stale = 0;
+	int res = AUTH_FAIL;
 
 	if (!find_challenge(rdata, auth)) {
 		/* Couldn't find a challenge with a sane nonce.
@@ -336,17 +344,26 @@
 
 	if (authed == PJ_SUCCESS) {
 		if (stale) {
-			return AUTH_STALE;
+			res = AUTH_STALE;
 		} else {
-			return AUTH_SUCCESS;
+			res = AUTH_SUCCESS;
 		}
 	}
 
 	if (authed == PJSIP_EAUTHNOAUTH) {
-		return AUTH_NOAUTH;
+		res = AUTH_NOAUTH;
 	}
 
-	return AUTH_FAIL;
+	ast_debug(3, "Realm: %s  Username: %s  Result: %s\n",
+		auth->realm, auth->auth_user, verify_result_str[res]);
+
+	ast_test_suite_event_notify("INCOMING_AUTH_VERIFY_RESULT",
+		"Realm: %s\r\n"
+		"Username: %s\r\n"
+		"Status: %s",
+		auth->realm, auth->auth_user, verify_result_str[res]);
+
+	return res;
 }
 
 /*!
diff --git a/res/res_pjsip_outbound_authenticator_digest.c b/res/res_pjsip_outbound_authenticator_digest.c
index b1011b0..9c523a0 100644
--- a/res/res_pjsip_outbound_authenticator_digest.c
+++ b/res/res_pjsip_outbound_authenticator_digest.c
@@ -30,131 +30,485 @@
 #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)
+pj_str_t supported_digest_algorithms[] = {
+	{ "MD5", 3}
+};
+
+/*!
+ * \internal
+ * \brief Determine proper authenticate header
+ *
+ * We need to search for different headers dependging 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)
 {
-	pjsip_hdr_e search_type;
-
 	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);
-
 }
 
-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 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)
 {
-	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;
+	int digest;
 
-	if (ast_sip_retrieve_auths(auth_vector, auths)) {
-		res = -1;
-		goto cleanup;
+	/* An empty digest is assumed to be md5 */
+	if (pj_strlen(&auth_hdr->challenge.digest.algorithm) == 0) {
+		return 1;
 	}
 
-	for (i = 0; i < auth_size; ++i) {
-		if (ast_strlen_zero(auths[i]->realm)) {
-			auth_creds[i].realm = auth_hdr->challenge.common.realm;
-		} else {
-			pj_cstr(&auth_creds[i].realm, auths[i]->realm);
+	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;
 		}
-		pj_cstr(&auth_creds[i].username, auths[i]->auth_user);
-		pj_cstr(&auth_creds[i].scheme, "digest");
-		switch (auths[i]->type) {
+	}
+	return 0;
+}
+
+/*!
+ * \internal
+ * \brief Initialize pjproject with a valid set of credentials
+ *
+ * RFC7616 and RFC8760 allow more than one WWW-Authenticate or
+ * Proxy-Autnenticate 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)
+{
+	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;
+
+	/*
+	 * 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;
+	}
+
+	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 dupplicate 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 {
+			/*
+			 * 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);
+		}
+
+		/*
+		 * 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.
+		 */
+		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;
 
 	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;
+	}
+
+	memset(&auth_sess, 0, sizeof(auth_sess));
 	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:
@@ -167,22 +521,16 @@
 		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,
@@ -198,8 +546,19 @@
 			id_type, id);
 		break;
 	}
+	res = -1;
 
-	return -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 res;
 }
 
 static struct ast_sip_outbound_authenticator digest_authenticator = {
diff --git a/third-party/pjproject/patches/0090-Skip-unsupported-digest-algorithm-2408.patch b/third-party/pjproject/patches/0090-Skip-unsupported-digest-algorithm-2408.patch
new file mode 100644
index 0000000..a2db220
--- /dev/null
+++ b/third-party/pjproject/patches/0090-Skip-unsupported-digest-algorithm-2408.patch
@@ -0,0 +1,212 @@
+From bdbeb7c4b2b11efc2e59f5dee7aa4360a2bc9fff Mon Sep 17 00:00:00 2001
+From: sauwming <ming at 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 at 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 at 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
+

-- 
To view, visit https://gerrit.asterisk.org/c/asterisk/+/15904
To unsubscribe, or for help writing mail filters, visit https://gerrit.asterisk.org/settings

Gerrit-Project: asterisk
Gerrit-Branch: master
Gerrit-Change-Id: I3aef5ce4fe1d27e48d61268520f284d15d650281
Gerrit-Change-Number: 15904
Gerrit-PatchSet: 1
Gerrit-Owner: George Joseph <gjoseph at digium.com>
Gerrit-MessageType: newchange
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.digium.com/pipermail/asterisk-code-review/attachments/20210513/1b65fce7/attachment-0001.html>


More information about the asterisk-code-review mailing list