res_pjsip: Add ability to identify by Authorization username
authorGeorge Joseph <george.joseph@fairview5.com>
Tue, 8 Mar 2016 00:34:31 +0000 (17:34 -0700)
committerGeorge Joseph <gjoseph@digium.com>
Wed, 27 Apr 2016 21:33:51 +0000 (16:33 -0500)
A feature of chan_sip that service providers relied upon was the ability to
identify by the Authorization username.  This is most often used when customers
have a PBX that needs to register rather than identify by IP address.  From my
own experiance, this is pretty common with small businesses who otherwise
don't need a static IP.

In this scenario, a register from the customer's PBX may succeed because From
will usually contain the PBXs account id but an INVITE will contain the caller
id.  With nothing recognizable in From, the service provider's Asterisk can
never match to an endpoint and the INVITE just stays unauthorized.

The fixes:

A new value "auth_username" has been added to endpoint/identify_by that
will use the username and digest fields in the Authorization header
instead of username and domain in the the From header to match an endpoint,
or the To header to match an aor.  This code as added to
res_pjsip_endpoint_identifier_user rather than creating a new module.

Although identify_by was always a comma-separated list, there was only
1 choice so order wasn't preserved.  So to keep the order, a vector was added
to the end of ast_sip_endpoint.  This is only used by res_pjsip_registrar
to find the aor.  The res_pjsip_endpoint_identifier_* modules are called in
globals/endpoint_identifier_order.

Along the way, the logic in res_pjsip_registrar was corrected to match
most-specific to least-specific as res_pjsip_endpoint_identifier_user does.

The order is:

username@domain
username@domain_alias
username

Auth by username does present 1 problem however, the first INVITE won't have
an Authorization header so the distributor, not finding a match on anything,
sends a securty_alert.  It still sends a 401 with a challenge so the next
INVITE will have the Authorization header and presumably succeed.  As a result
though, that first security alert is actually a false alarm.

To address this, a new feature has been added to pjsip_distributor that keeps
track of unidentified requests and only sends the security alert if a
configurable number of unidentified requests come from the same IP in a
configurable amout of time.  Those configuration options have been added to
the global config object.  This feature is only used when auth_username
is enabled.

Finally, default_realm was added to the globals object to replace the hard
coded "asterisk" used when an endpoint is not yet identified.

The testsuite tests all pass but new tests are forthcoming for this new
feature.

ASTERISK-25835 #close
Reported-by: Ross Beer

Change-Id: I30ba62d208e6f63439600916fcd1c08a365ed69d

CHANGES
configs/samples/pjsip.conf.sample
contrib/ast-db-manage/config/versions/65eb22eb195_add_unidentified_request_options_to_.py [new file with mode: 0644]
include/asterisk/res_pjsip.h
res/res_pjsip.c
res/res_pjsip/config_global.c
res/res_pjsip/pjsip_configuration.c
res/res_pjsip/pjsip_distributor.c
res/res_pjsip_authenticator_digest.c
res/res_pjsip_endpoint_identifier_user.c
res/res_pjsip_registrar.c

diff --git a/CHANGES b/CHANGES
index 4844f85..74fefd5 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -397,6 +397,20 @@ res_pjsip
    dynamically create and destroy a NoOp priority 1 extension
    for a given endpoint who registers or unregisters with us.
 
+ * Endpoints and aors can now be identified by the username and realm in an
+   incoming Authorization header.  To use this feature, add "auth_username"
+   to your endpoint's "identify_by" list.  You can combine "auth_username"
+   and the original "username" to test both the From/To and Authorization
+   headers.  For endpoints, the order is controlled by the global
+   "endpoint_identifier_order" setting.  For matching aors to an endpoint
+   for inbound registration, the order is controlled by this option.
+
+ * In conjunction with the "auth_username" change, 3 new options have been
+   added to the global configuration object that control how many unidentified
+   requests over a certain period from the same IP address can be received
+   before a security altert is generated.  A new CLI command
+   "pjsip show unidentified_requests" will list the current candidates.
+
 res_pjsip_history
 ------------------
  * A new module, res_pjsip_history, has been added that provides SIP history
index 6a08edc..cd87091 100644 (file)
                                 ; the specified address. (default: "no")
 ;force_rport=yes        ; Force use of return port (default: "yes")
 ;ice_support=no ; Enable the ICE mechanism to help traverse NAT (default: "no")
-;identify_by=username   ; Way s for Endpoint to be identified (default:
-                        ; "username")
+;identify_by=username   ; A comma-separated list of ways the Endpoint or AoR can be
+                        ; identified.
+                        ; "username": Identify by the From or To username and domain
+                        ; "auth_username": Identify by the Authorization username and realm
+                        : In all cases, if an exact match on username and domain/realm fails,
+                        ; the match will be retried with just the username.
+                        ; (default: "username")
 ;redirect_method=user   ; How redirects received from an endpoint are handled
                         ; (default: "user")
 ;mailboxes=     ; NOTIFY the endpoint when state changes for any of the specified mailboxes.
                        ; (default: "no")
 ;endpoint_identifier_order=ip,username,anonymous
             ; The order by which endpoint identifiers are given priority.
-            ; Identifier names are derived from res_pjsip_endpoint_identifier_*
-            ; modules. (default: ip,username,anonymous)
+            ; Currently, "ip", "username", "auth_username" and "anonymous" are valid
+            ; identifiers as registered by the res_pjsip_endpoint_identifier_* modules.
+            ; Some modules like res_pjsip_endpoint_identifier_user register more than
+            ; one identifier. Use the CLI command "pjsip show identifiers" to see the
+            ; identifiers currently available.
+            ; (default: ip,username,anonymous)
 ;max_initial_qualify_time=4 ; The maximum amount of time (in seconds) from
                             ; startup that qualifies should be attempted on all
                             ; contacts.  If greater than the qualify_frequency
                    ; The voicemail extension to send in the NOTIFY Message-Account header
                    ; if not set on endpoint or aor.
                    ; (default: "")
-
+;
+; The following unidentified_request options are only used when "auth_username"
+; matching is enabled in "endpoint_identifier_order".
+;
+;unidentified_request_count=5   ; The number of unidentified requests that can be
+                                ; received from a single IP address in
+                                ; unidentified_request_period seconds before a security
+                                ; event is generated. (default: 5)
+;unidentified_request_period=5  ; See above.  (default: 5 seconds)
+;unidentified_request_prune_interval=30
+                                ; The interval at which unidentified requests
+                                ; are check to see if they can be pruned.  If they're
+                                ; older than twice the unidentified_request_period,
+                                ; they're pruned.
+;
+;default_from_user=asterisk     ; When Asterisk generates an outgoing SIP request, the
+                                ; From header username will be set to this value if
+                                ; there is no better option (such as CallerID or
+                                ; endpoint/from_user) to be used
+;default_realm=asterisk         ; When Asterisk generates a challenge, the realm will be
+                                ; set to this value if there is no better option (such as
+                                ; auth/realm) to be used
 
 ; MODULE PROVIDING BELOW SECTION(S): res_pjsip_acl
 ;==========================ACL SECTION OPTIONS=========================
diff --git a/contrib/ast-db-manage/config/versions/65eb22eb195_add_unidentified_request_options_to_.py b/contrib/ast-db-manage/config/versions/65eb22eb195_add_unidentified_request_options_to_.py
new file mode 100644 (file)
index 0000000..e0453a5
--- /dev/null
@@ -0,0 +1,27 @@
+"""Add unidentified request options to global
+
+Revision ID: 65eb22eb195
+Revises: 8d478ab86e29
+Create Date: 2016-03-11 11:58:51.567959
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '65eb22eb195'
+down_revision = '8d478ab86e29'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+    op.add_column('ps_globals', sa.Column('unidentified_request_count', sa.Integer))
+    op.add_column('ps_globals', sa.Column('unidentified_request_period', sa.Integer))
+    op.add_column('ps_globals', sa.Column('unidentified_request_prune_interval', sa.Integer))
+    op.add_column('ps_globals', sa.Column('default_realm', sa.String(40)))
+
+def downgrade():
+    op.drop_column('ps_globals', 'unidentified_request_count')
+    op.drop_column('ps_globals', 'unidentified_request_period')
+    op.drop_column('ps_globals', 'unidentified_request_prune_interval')
+    op.drop_column('ps_globals', 'default_realm')
index fa4e22c..6939d27 100644 (file)
@@ -398,7 +398,10 @@ AST_VECTOR(ast_sip_auth_vector, const char *);
 enum ast_sip_endpoint_identifier_type {
        /*! Identify based on user name in From header */
        AST_SIP_ENDPOINT_IDENTIFY_BY_USERNAME = (1 << 0),
+       /*! Identify based on user name in Auth header first, then From header */
+       AST_SIP_ENDPOINT_IDENTIFY_BY_AUTH_USERNAME = (1 << 1),
 };
+AST_VECTOR(ast_sip_identify_by_vector, enum ast_sip_endpoint_identifier_type);
 
 enum ast_sip_session_refresh_method {
        /*! Use reinvite to negotiate direct media */
@@ -712,6 +715,8 @@ struct ast_sip_endpoint {
        enum ast_sip_dtmf_mode dtmf;
        /*! Method(s) by which the endpoint should be identified. */
        enum ast_sip_endpoint_identifier_type ident_method;
+       /*! Order of the method(s) by which the endpoint should be identified. */
+       struct ast_sip_identify_by_vector ident_method_order;
        /*! Boolean indicating if ringing should be sent as inband progress */
        unsigned int inband_progress;
        /*! Pointer to the persistent Asterisk endpoint */
@@ -2408,6 +2413,18 @@ char *ast_sip_get_endpoint_identifier_order(void);
 char *ast_sip_get_default_voicemail_extension(void);
 
 /*!
+ * \brief Retrieve the global default realm.
+ *
+ * This is the value placed in outbound challenges' realm if there
+ * is no better option (such as an auth-configured realm).
+ *
+ * \param[out] realm The default realm
+ * \param size The buffer size of realm
+ * \return nothing
+ */
+void ast_sip_get_default_realm(char *realm, size_t size);
+
+/*!
  * \brief Retrieve the global default from user.
  *
  * This is the value placed in outbound requests' From header if there
@@ -2562,5 +2579,15 @@ int ast_sip_set_tpselector_from_transport_name(const char *transport_name, pjsip
 void ast_sip_modify_id_header(pj_pool_t *pool, pjsip_fromto_hdr *id_hdr,
        const struct ast_party_id *id);
 
+/*!
+ * \brief Retrieve the unidentified request security event thresholds
+ * \since 13.8.0
+ *
+ * \param count The maximum number of unidentified requests per source ip to accumulate before emitting a security event
+ * \param period The period in seconds over which to accumulate unidentified requests
+ * \param prune_interval The interval in seconds at which expired entries will be pruned
+ */
+void ast_sip_get_unidentified_request_thresholds(unsigned int *count, unsigned int *period,
+       unsigned int *prune_interval);
 
 #endif /* _RES_PJSIP_H */
index daab0af..6890e63 100644 (file)
                                <configOption name="identify_by" default="username,location">
                                        <synopsis>Way(s) for Endpoint to be identified</synopsis>
                                        <description><para>
-                                               An endpoint can be identified in multiple ways. Currently, the only supported
-                                               option is <literal>username</literal>, which matches the endpoint based on the
-                                               username in the From header.
+                                               Endpoints and aors can be identified in multiple ways. Currently, the supported
+                                               options are <literal>username</literal>, which matches the endpoint or aor id based on
+                                               the username and domain in the From header (or To header for aors), and
+                                               <literal>auth_username</literal>, which matches the endpoint or aor id based on the
+                                               username and realm in the Authentication header.  In all cases, if an exact match
+                                               on both username and domain/realm fails, the match will be retried with just the username.
                                                </para>
+                                               <note><para>
+                                               Identification by auth_username has some security considerations because an
+                                               Authentication header is not present on the first message of a dialog when
+                                               digest authentication is used.  The client can't generate it until the server
+                                               sends the challenge in a 401 response.  Since Asterisk normally sends a security
+                                               event when an incoming request can't be matched to an endpoint, using auth_username
+                                               requires that the security event be deferred until a request is received with
+                                               the Authentication header and only generated if the username doesn't result in a
+                                               match.  This may result in a delay before an attack is recognized.  You can control
+                                               how many unmatched requests are received from a single ip address before a security
+                                               event is generated using the unidentified_request parameters in the "global"
+                                               configuration object.
+                                               </para></note>
                                                <note><para>Endpoints can also be identified by IP address; however, that method
                                                of identification is not handled by this configuration option. See the documentation
                                                for the <literal>identify</literal> configuration section for more details on that
-                                               method of endpoint identification. If this option is set to <literal>username</literal>
-                                               and an <literal>identify</literal> configuration section exists for the endpoint, then
-                                               the endpoint can be identified in multiple ways.</para></note>
+                                               method of endpoint identification. If this option is set and an <literal>identify</literal>
+                                               configuration section exists for the endpoint, then the endpoint can be identified in
+                                               multiple ways.</para></note>
                                                <enumlist>
                                                        <enum name="username" />
+                                                       <enum name="auth_username" />
                                                </enumlist>
                                        </description>
                                </configOption>
                                        <synopsis>The maximum amount of time from startup that qualifies should be attempted on all contacts.
                                        If greater than the qualify_frequency for an aor, qualify_frequency will be used instead.</synopsis>
                                </configOption>
+                               <configOption name="unidentified_request_period" default="5">
+                                       <synopsis>The number of seconds over which to accumulate unidentified requests.</synopsis>
+                                       <description><para>
+                                       If <literal>unidentified_request_count</literal> unidentified requests are received
+                                       during <literal>unidentified_request_period</literal>, a security event will be generated.
+                                       </para></description>
+                               </configOption>
+                               <configOption name="unidentified_request_count" default="5">
+                                       <synopsis>The number of unidentified requests from a single IP to allow.</synopsis>
+                                       <description><para>
+                                       If <literal>unidentified_request_count</literal> unidentified requests are received
+                                       during <literal>unidentified_request_period</literal>, a security event will be generated.
+                                       </para></description>
+                               </configOption>
+                               <configOption name="unidentified_request_prune_interval" default="30">
+                                       <synopsis>The interval at which unidentified requests are older than
+                                       twice the unidentified_request_period are pruned.</synopsis>
+                               </configOption>
                                <configOption name="type">
                                        <synopsis>Must be of type 'global'.</synopsis>
                                </configOption>
                                <configOption name="endpoint_identifier_order" default="ip,username,anonymous">
                                        <synopsis>The order by which endpoint identifiers are processed and checked.
                                         Identifier names are usually derived from and can be found in the endpoint
-                                        identifier module itself (res_pjsip_endpoint_identifier_*)</synopsis>
+                                        identifier module itself (res_pjsip_endpoint_identifier_*).
+                                        You can use the CLI command "pjsip show identifiers" to see the
+                                        identifiers currently available.</synopsis>
+                    <description>
+                                               <note><para>
+                                               One of the identifiers is "auth_username" which matches on the username in
+                                               an Authentication header.  This method has some security considerations because an
+                                               Authentication header is not present on the first message of a dialog when
+                                               digest authentication is used.  The client can't generate it until the server
+                                               sends the challenge in a 401 response.  Since Asterisk normally sends a security
+                                               event when an incoming request can't be matched to an endpoint, using auth_username
+                                               requires that the security event be deferred until a request is received with
+                                               the Authentication header and only generated if the username doesn't result in a
+                                               match.  This may result in a delay before an attack is recognized.  You can control
+                                               how many unmatched requests are received from a single ip address before a security
+                                               event is generated using the unidentified_request parameters.
+                                               </para></note>
+                    </description>
                                </configOption>
                                <configOption name="default_from_user" default="asterisk">
                                        <synopsis>When Asterisk generates an outgoing SIP request, the From header username will be
                                         set to this value if there is no better option (such as CallerID) to be
                                         used.</synopsis>
                                </configOption>
+                               <configOption name="default_realm" default="asterisk">
+                                       <synopsis>When Asterisk generates an challenge, the digest will be
+                                        set to this value if there is no better option (such as auth/realm) to be
+                                        used.</synopsis>
+                               </configOption>
                        </configObject>
                </configFile>
        </configInfo>
index ccfec54..6bb6888 100644 (file)
 #define DEFAULT_ENDPOINT_IDENTIFIER_ORDER "ip,username,anonymous"
 #define DEFAULT_MAX_INITIAL_QUALIFY_TIME 0
 #define DEFAULT_FROM_USER "asterisk"
+#define DEFAULT_REALM "asterisk"
 #define DEFAULT_REGCONTEXT ""
 #define DEFAULT_CONTACT_EXPIRATION_CHECK_INTERVAL 30
 #define DEFAULT_DISABLE_MULTI_DOMAIN 0
 #define DEFAULT_VOICEMAIL_EXTENSION ""
+#define DEFAULT_UNIDENTIFIED_REQUEST_COUNT 5
+#define DEFAULT_UNIDENTIFIED_REQUEST_PERIOD 5
+#define DEFAULT_UNIDENTIFIED_REQUEST_PRUNE_INTERVAL 30
 
 static char default_useragent[256];
 
@@ -56,6 +60,8 @@ struct global_config {
                AST_STRING_FIELD(default_from_user);
                /*! Default voicemail extension */
                AST_STRING_FIELD(default_voicemail_extension);
+               /*! Realm to use in challenges before an endpoint is identified */
+               AST_STRING_FIELD(default_realm);
        );
        /* Value to put in Max-Forwards header */
        unsigned int max_forwards;
@@ -67,6 +73,12 @@ struct global_config {
        unsigned int contact_expiration_check_interval;
        /*! Nonzero to disable multi domain support */
        unsigned int disable_multi_domain;
+       /* The maximum number of unidentified requests per source IP address before a security event is logged */
+       unsigned int unidentified_request_count;
+       /* The period during which unidentified requests are accumulated */
+       unsigned int unidentified_request_period;
+       /* Interval at which expired unidentifed requests will be pruned */
+       unsigned int unidentified_request_prune_interval;
 };
 
 static void global_destructor(void *obj)
@@ -255,6 +267,40 @@ unsigned int ast_sip_get_max_initial_qualify_time(void)
        return time;
 }
 
+void ast_sip_get_unidentified_request_thresholds(unsigned int *count, unsigned int *period,
+       unsigned int *prune_interval)
+{
+       struct global_config *cfg;
+
+       cfg = get_global_cfg();
+       if (!cfg) {
+               *count = DEFAULT_UNIDENTIFIED_REQUEST_COUNT;
+               *period = DEFAULT_UNIDENTIFIED_REQUEST_PERIOD;
+               *prune_interval = DEFAULT_UNIDENTIFIED_REQUEST_PRUNE_INTERVAL;
+               return;
+       }
+
+       *count = cfg->unidentified_request_count;
+       *period = cfg->unidentified_request_period;
+       *prune_interval = cfg->unidentified_request_prune_interval;
+
+       ao2_ref(cfg, -1);
+       return;
+}
+
+void ast_sip_get_default_realm(char *realm, size_t size)
+{
+       struct global_config *cfg;
+
+       cfg = get_global_cfg();
+       if (!cfg) {
+               ast_copy_string(realm, DEFAULT_REALM, size);
+       } else {
+               ast_copy_string(realm, cfg->default_realm, size);
+               ao2_ref(cfg, -1);
+       }
+}
+
 void ast_sip_get_default_from_user(char *from_user, size_t size)
 {
        struct global_config *cfg;
@@ -393,6 +439,17 @@ int ast_sip_initialize_sorcery_global(void)
                OPT_UINT_T, 0, FLDSET(struct global_config, contact_expiration_check_interval));
        ast_sorcery_object_field_register(sorcery, "global", "disable_multi_domain", "no",
                OPT_BOOL_T, 1, FLDSET(struct global_config, disable_multi_domain));
+       ast_sorcery_object_field_register(sorcery, "global", "unidentified_request_count",
+               __stringify(DEFAULT_UNIDENTIFIED_REQUEST_COUNT),
+               OPT_UINT_T, 0, FLDSET(struct global_config, unidentified_request_count));
+       ast_sorcery_object_field_register(sorcery, "global", "unidentified_request_period",
+               __stringify(DEFAULT_UNIDENTIFIED_REQUEST_PERIOD),
+               OPT_UINT_T, 0, FLDSET(struct global_config, unidentified_request_period));
+       ast_sorcery_object_field_register(sorcery, "global", "unidentified_request_prune_interval",
+               __stringify(DEFAULT_UNIDENTIFIED_REQUEST_PRUNE_INTERVAL),
+               OPT_UINT_T, 0, FLDSET(struct global_config, unidentified_request_prune_interval));
+       ast_sorcery_object_field_register(sorcery, "global", "default_realm", DEFAULT_REALM,
+               OPT_STRINGFIELD_T, 0, STRFLDSET(struct global_config, default_realm));
 
        if (ast_sorcery_instance_observer_add(sorcery, &observer_callbacks_global)) {
                return -1;
index baa5063..c370ab7 100644 (file)
@@ -476,6 +476,16 @@ static int ident_handler(const struct aco_option *opt, struct ast_variable *var,
        struct ast_sip_endpoint *endpoint = obj;
        char *idents = ast_strdupa(var->value);
        char *val;
+       enum ast_sip_endpoint_identifier_type method;
+
+       /*
+        * If there's already something in the vector when we get here,
+        * it's the default value so we need to clean it out.
+        */
+       if (AST_VECTOR_SIZE(&endpoint->ident_method_order)) {
+               AST_VECTOR_RESET(&endpoint->ident_method_order, AST_VECTOR_ELEM_CLEANUP_NOOP);
+               endpoint->ident_method = 0;
+       }
 
        while ((val = ast_strip(strsep(&idents, ",")))) {
                if (ast_strlen_zero(val)) {
@@ -483,27 +493,55 @@ static int ident_handler(const struct aco_option *opt, struct ast_variable *var,
                }
 
                if (!strcasecmp(val, "username")) {
-                       endpoint->ident_method |= AST_SIP_ENDPOINT_IDENTIFY_BY_USERNAME;
+                       method = AST_SIP_ENDPOINT_IDENTIFY_BY_USERNAME;
+               } else  if (!strcasecmp(val, "auth_username")) {
+                       method = AST_SIP_ENDPOINT_IDENTIFY_BY_AUTH_USERNAME;
                } else {
                        ast_log(LOG_ERROR, "Unrecognized identification method %s specified for endpoint %s\n",
                                        val, ast_sorcery_object_get_id(endpoint));
+                       AST_VECTOR_RESET(&endpoint->ident_method_order, AST_VECTOR_ELEM_CLEANUP_NOOP);
+                       endpoint->ident_method = 0;
                        return -1;
                }
+
+               endpoint->ident_method |= method;
+               AST_VECTOR_APPEND(&endpoint->ident_method_order, method);
        }
+
        return 0;
 }
 
 static int ident_to_str(const void *obj, const intptr_t *args, char **buf)
 {
        const struct ast_sip_endpoint *endpoint = obj;
-       switch (endpoint->ident_method) {
-       case AST_SIP_ENDPOINT_IDENTIFY_BY_USERNAME :
-               *buf = "username"; break;
-       default:
+       int methods;
+       char *method;
+       int i;
+       int j = 0;
+
+       methods = AST_VECTOR_SIZE(&endpoint->ident_method_order);
+       if (!methods) {
                return 0;
        }
 
-       *buf = ast_strdup(*buf);
+       if (!(*buf = ast_calloc(MAX_OBJECT_FIELD, sizeof(char)))) {
+               return -1;
+       }
+
+       for (i = 0; i < methods; i++) {
+               switch (AST_VECTOR_GET(&endpoint->ident_method_order, i)) {
+               case AST_SIP_ENDPOINT_IDENTIFY_BY_USERNAME :
+                       method = "username";
+                       break;
+               case AST_SIP_ENDPOINT_IDENTIFY_BY_AUTH_USERNAME :
+                       method = "auth_username";
+                       break;
+               default:
+                       continue;
+               }
+               j = sprintf(*buf + j, "%s%s", method, i < methods - 1 ? "," : "");
+       }
+
        return 0;
 }
 
@@ -1849,6 +1887,7 @@ static void endpoint_destructor(void* obj)
        endpoint->pickup.named_pickupgroups = ast_unref_namedgroups(endpoint->pickup.named_pickupgroups);
        ao2_cleanup(endpoint->persistent);
        ast_variables_destroy(endpoint->channel_vars);
+       AST_VECTOR_FREE(&endpoint->ident_method_order);
 }
 
 static int init_subscription_configuration(struct ast_sip_endpoint_subscription_configuration *subscription)
@@ -1893,6 +1932,11 @@ void *ast_sip_endpoint_alloc(const char *name)
                return NULL;
        }
        ast_party_id_init(&endpoint->id.self);
+
+       if (AST_VECTOR_INIT(&endpoint->ident_method_order, 1)) {
+               return NULL;
+       }
+
        return endpoint;
 }
 
index 834ca10..cbe9557 100644 (file)
@@ -24,6 +24,7 @@
 #include "include/res_pjsip_private.h"
 #include "asterisk/taskprocessor.h"
 #include "asterisk/threadpool.h"
+#include "asterisk/res_pjsip_cli.h"
 
 static int distribute(void *data);
 static pj_bool_t distributor(pjsip_rx_data *rdata);
@@ -37,6 +38,26 @@ static pjsip_module distributor_mod = {
        .on_rx_response = distributor,
 };
 
+struct ast_sched_context *prune_context;
+
+/* From the auth/realm realtime column size */
+#define MAX_REALM_LENGTH 40
+static char default_realm[MAX_REALM_LENGTH + 1];
+
+#define DEFAULT_SUSPECTS_BUCKETS 53
+
+static struct ao2_container *unidentified_requests;
+static unsigned int unidentified_count;
+static unsigned int unidentified_period;
+static unsigned int unidentified_prune_interval;
+static int using_auth_username;
+
+struct unidentified_request{
+       struct timeval first_seen;
+       int count;
+       char src_name[];
+};
+
 /*!
  * \internal
  * \brief Record the task's serializer name on the tdata structure.
@@ -322,7 +343,7 @@ static int create_artificial_auth(void)
                return -1;
        }
 
-       ast_string_field_set(artificial_auth, realm, "asterisk");
+       ast_string_field_set(artificial_auth, realm, default_realm);
        ast_string_field_set(artificial_auth, auth_user, "");
        ast_string_field_set(artificial_auth, auth_pass, "");
        artificial_auth->type = AST_SIP_AUTH_TYPE_ARTIFICIAL;
@@ -359,27 +380,65 @@ struct ast_sip_endpoint *ast_sip_get_artificial_endpoint(void)
        return artificial_endpoint;
 }
 
-static void log_unidentified_request(pjsip_rx_data *rdata)
+static void log_unidentified_request(pjsip_rx_data *rdata, unsigned int count, unsigned int period)
 {
        char from_buf[PJSIP_MAX_URL_SIZE];
        char callid_buf[PJSIP_MAX_URL_SIZE];
        pjsip_uri_print(PJSIP_URI_IN_FROMTO_HDR, rdata->msg_info.from->uri, from_buf, PJSIP_MAX_URL_SIZE);
        ast_copy_pj_str(callid_buf, &rdata->msg_info.cid->id, PJSIP_MAX_URL_SIZE);
-       ast_log(LOG_NOTICE, "Request from '%s' failed for '%s:%d' (callid: %s) - No matching endpoint found\n",
-               from_buf, rdata->pkt_info.src_name, rdata->pkt_info.src_port, callid_buf);
+       if (count) {
+               ast_log(LOG_NOTICE, "Request from '%s' failed for '%s:%d' (callid: %s) - No matching endpoint found"
+                       " after %u tries in %.3f ms\n",
+                       from_buf, rdata->pkt_info.src_name, rdata->pkt_info.src_port, callid_buf, count, period / 1000.0);
+       } else {
+               ast_log(LOG_NOTICE, "Request from '%s' failed for '%s:%d' (callid: %s) - No matching endpoint found",
+                       from_buf, rdata->pkt_info.src_name, rdata->pkt_info.src_port, callid_buf);
+       }
+}
+
+static void check_endpoint(pjsip_rx_data *rdata, struct unidentified_request *unid,
+       const char *name)
+{
+       int64_t ms = ast_tvdiff_ms(ast_tvnow(), unid->first_seen);
+
+       ao2_wrlock(unid);
+       unid->count++;
+
+       if (ms < (unidentified_period * 1000) && unid->count >= unidentified_count) {
+               log_unidentified_request(rdata, unid->count, ms);
+               ast_sip_report_invalid_endpoint(name, rdata);
+       }
+       ao2_unlock(unid);
 }
 
 static pj_bool_t endpoint_lookup(pjsip_rx_data *rdata)
 {
        struct ast_sip_endpoint *endpoint;
+       struct unidentified_request *unid;
        int is_ack = rdata->msg_info.msg->line.req.method.id == PJSIP_ACK_METHOD;
 
        endpoint = rdata->endpt_info.mod_data[endpoint_mod.id];
        if (endpoint) {
+               /*
+                * ao2_find with OBJ_UNLINK always write locks the container before even searching
+                * for the object.  Since the majority case is that the object won't be found, do
+                * the find without OBJ_UNLINK to prevent the unnecessary write lock, then unlink
+                * if needed.
+                */
+               if ((unid = ao2_find(unidentified_requests, rdata->pkt_info.src_name, OBJ_SEARCH_KEY))) {
+                       ao2_unlink(unidentified_requests, unid);
+                       ao2_ref(unid, -1);
+               }
                return PJ_FALSE;
        }
 
        endpoint = ast_sip_identify_endpoint(rdata);
+       if (endpoint) {
+               if ((unid = ao2_find(unidentified_requests, rdata->pkt_info.src_name, OBJ_SEARCH_KEY))) {
+                       ao2_unlink(unidentified_requests, unid);
+                       ao2_ref(unid, -1);
+               }
+       }
 
        if (!endpoint && !is_ack) {
                char name[AST_UUID_STR_LEN] = "";
@@ -397,8 +456,32 @@ static pj_bool_t endpoint_lookup(pjsip_rx_data *rdata)
                        ast_copy_pj_str(name, &sip_from->user, sizeof(name));
                }
 
-               log_unidentified_request(rdata);
-               ast_sip_report_invalid_endpoint(name, rdata);
+               if ((unid = ao2_find(unidentified_requests, rdata->pkt_info.src_name, OBJ_SEARCH_KEY))) {
+                       check_endpoint(rdata, unid, name);
+                       ao2_ref(unid, -1);
+               } else if (using_auth_username) {
+                       ao2_wrlock(unidentified_requests);
+                       /* The check again with the write lock held allows us to eliminate the DUPS_REPLACE and sort_fn */
+                       if ((unid = ao2_find(unidentified_requests, rdata->pkt_info.src_name, OBJ_SEARCH_KEY | OBJ_NOLOCK))) {
+                               check_endpoint(rdata, unid, name);
+                       } else {
+                               unid = ao2_alloc_options(sizeof(*unid) + strlen(rdata->pkt_info.src_name) + 1, NULL,
+                                       AO2_ALLOC_OPT_LOCK_RWLOCK);
+                               if (!unid) {
+                                       ao2_unlock(unidentified_requests);
+                                       return PJ_TRUE;
+                               }
+                               strcpy(unid->src_name, rdata->pkt_info.src_name); /* Safe */
+                               unid->first_seen = ast_tvnow();
+                               unid->count = 1;
+                               ao2_link_flags(unidentified_requests, unid, OBJ_NOLOCK);
+                       }
+                       ao2_ref(unid, -1);
+                       ao2_unlock(unidentified_requests);
+               } else {
+                       log_unidentified_request(rdata, 0, 0);
+                       ast_sip_report_invalid_endpoint(name, rdata);
+               }
        }
        rdata->endpt_info.mod_data[endpoint_mod.id] = endpoint;
        return PJ_FALSE;
@@ -413,6 +496,8 @@ static pj_bool_t authenticate(pjsip_rx_data *rdata)
 
        if (!is_ack && ast_sip_requires_authentication(endpoint, rdata)) {
                pjsip_tx_data *tdata;
+               struct unidentified_request *unid;
+
                pjsip_endpt_create_response(ast_sip_get_pjsip_endpoint(), rdata, 401, NULL, &tdata);
                switch (ast_sip_check_authentication(endpoint, rdata, tdata)) {
                case AST_SIP_AUTHENTICATION_CHALLENGE:
@@ -421,6 +506,11 @@ static pj_bool_t authenticate(pjsip_rx_data *rdata)
                        pjsip_endpt_send_response2(ast_sip_get_pjsip_endpoint(), rdata, tdata, NULL, NULL);
                        return PJ_TRUE;
                case AST_SIP_AUTHENTICATION_SUCCESS:
+                       /* See note in endpoint_lookup about not holding an unnecessary write lock */
+                       if ((unid = ao2_find(unidentified_requests, rdata->pkt_info.src_name, OBJ_SEARCH_KEY))) {
+                               ao2_unlink(unidentified_requests, unid);
+                               ao2_ref(unid, -1);
+                       }
                        ast_sip_report_auth_success(endpoint, rdata);
                        pjsip_tx_data_dec_ref(tdata);
                        return PJ_FALSE;
@@ -480,31 +570,287 @@ struct ast_sip_endpoint *ast_pjsip_rdata_get_endpoint(pjsip_rx_data *rdata)
        return endpoint;
 }
 
+static int suspects_sort(const void *obj, const void *arg, int flags)
+{
+       const struct unidentified_request *object_left = obj;
+       const struct unidentified_request *object_right = arg;
+       const char *right_key = arg;
+       int cmp;
+
+       switch (flags & OBJ_SEARCH_MASK) {
+       case OBJ_SEARCH_OBJECT:
+               right_key = object_right->src_name;
+               /* Fall through */
+       case OBJ_SEARCH_KEY:
+               cmp = strcmp(object_left->src_name, right_key);
+               break;
+       case OBJ_SEARCH_PARTIAL_KEY:
+               cmp = strncmp(object_left->src_name, right_key, strlen(right_key));
+               break;
+       default:
+               cmp = 0;
+               break;
+       }
+       return cmp;
+}
+
+static int suspects_compare(void *obj, void *arg, int flags)
+{
+       const struct unidentified_request *object_left = obj;
+       const struct unidentified_request *object_right = arg;
+       const char *right_key = arg;
+       int cmp = 0;
+
+       switch (flags & OBJ_SEARCH_MASK) {
+       case OBJ_SEARCH_OBJECT:
+               right_key = object_right->src_name;
+               /* Fall through */
+       case OBJ_SEARCH_KEY:
+               if (strcmp(object_left->src_name, right_key) == 0) {
+                       cmp = CMP_MATCH | CMP_STOP;
+               }
+               break;
+       case OBJ_SEARCH_PARTIAL_KEY:
+               if (strncmp(object_left->src_name, right_key, strlen(right_key)) == 0) {
+                       cmp = CMP_MATCH;
+               }
+               break;
+       default:
+               cmp = 0;
+               break;
+       }
+       return cmp;
+}
+
+static int suspects_hash(const void *obj, int flags) {
+       const struct unidentified_request *object_left = obj;
+
+       if (flags & OBJ_SEARCH_OBJECT) {
+               return ast_str_hash(object_left->src_name);
+       } else if (flags & OBJ_SEARCH_KEY) {
+               return ast_str_hash(obj);
+       }
+       return -1;
+}
+
+static struct ao2_container *cli_unid_get_container(const char *regex)
+{
+       struct ao2_container *s_container;
+
+       s_container = ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_NOLOCK, 0,
+               suspects_sort, suspects_compare);
+       if (!s_container) {
+               return NULL;
+       }
+
+       if (ao2_container_dup(s_container, unidentified_requests, 0)) {
+               ao2_ref(s_container, -1);
+               return NULL;
+       }
+
+       return s_container;
+}
+
+static int cli_unid_iterate(void *container, ao2_callback_fn callback, void *args)
+{
+       ao2_callback(container, 0, callback, args);
+
+       return 0;
+}
+
+static void *cli_unid_retrieve_by_id(const char *id)
+{
+       return ao2_find(unidentified_requests, id, OBJ_SEARCH_KEY);
+}
+
+static const char *cli_unid_get_id(const void *obj)
+{
+       const struct unidentified_request *unid = obj;
+
+       return unid->src_name;
+}
+
+static int cli_unid_print_header(void *obj, void *arg, int flags)
+{
+       struct ast_sip_cli_context *context = arg;
+       RAII_VAR(struct ast_sip_cli_formatter_entry *, formatter_entry, NULL, ao2_cleanup);
+
+       int indent = CLI_INDENT_TO_SPACES(context->indent_level);
+       int filler = CLI_LAST_TABSTOP - indent - 7;
+
+       ast_assert(context->output_buffer != NULL);
+
+       ast_str_append(&context->output_buffer, 0,
+               "%*s:  <IP Address%*.*s>  <Count> <Age(sec)>\n",
+               indent, "Request", filler, filler, CLI_HEADER_FILLER);
+
+       return 0;
+}
+static int cli_unid_print_body(void *obj, void *arg, int flags)
+{
+       struct unidentified_request *unid = obj;
+       struct ast_sip_cli_context *context = arg;
+       int indent;
+       int flexwidth;
+       int64_t ms = ast_tvdiff_ms(ast_tvnow(), unid->first_seen);
+
+       ast_assert(context->output_buffer != NULL);
+
+       indent = CLI_INDENT_TO_SPACES(context->indent_level);
+       flexwidth = CLI_LAST_TABSTOP - 4;
+
+       ast_str_append(&context->output_buffer, 0, "%*s:  %-*.*s  %7d %10.3f\n",
+               indent,
+               "Request",
+               flexwidth, flexwidth,
+               unid->src_name, unid->count,  ms / 1000.0);
+
+       return 0;
+}
+
+static struct ast_cli_entry cli_commands[] = {
+       AST_CLI_DEFINE(ast_sip_cli_traverse_objects, "Show PJSIP Unidentified Requests",
+               .command = "pjsip show unidentified_requests",
+               .usage = "Usage: pjsip show unidentified_requests\n"
+                               "       Show the PJSIP Unidentified Requests\n"),
+};
+
+struct ast_sip_cli_formatter_entry *unid_formatter;
+
+static int expire_requests(void *object, void *arg, int flags)
+{
+       struct unidentified_request *unid = object;
+       int *maxage = arg;
+       int64_t ms = ast_tvdiff_ms(ast_tvnow(), unid->first_seen);
+
+       if (ms > (*maxage) * 2 * 1000) {
+               return CMP_MATCH;
+       }
+
+       return 0;
+}
+
+static int prune_task(const void *data)
+{
+       unsigned int maxage;
+
+       ast_sip_get_unidentified_request_thresholds(&unidentified_count, &unidentified_period, &unidentified_prune_interval);
+       maxage = unidentified_period * 2;
+       ao2_callback(unidentified_requests, OBJ_MULTIPLE | OBJ_NODATA | OBJ_UNLINK, expire_requests, &maxage);
+
+       return unidentified_prune_interval * 1000;
+}
+
+static int clean_task(const void *data)
+{
+       return 0;
+}
+
+static void global_loaded(const char *object_type)
+{
+       char *identifier_order = ast_sip_get_endpoint_identifier_order();
+       char *io_copy = ast_strdupa(identifier_order);
+       char *identify_method;
+
+       ast_free(identifier_order);
+       using_auth_username = 0;
+       while ((identify_method = ast_strip(strsep(&io_copy, ",")))) {
+               if (!strcmp(identify_method, "auth_username")) {
+                       using_auth_username = 1;
+                       break;
+               }
+       }
+
+       ast_sip_get_default_realm(default_realm, sizeof(default_realm));
+       ast_sip_get_unidentified_request_thresholds(&unidentified_count, &unidentified_period, &unidentified_prune_interval);
+
+       /* Clean out the old task, if any */
+       ast_sched_clean_by_callback(prune_context, prune_task, clean_task);
+       if (ast_sched_add_variable(prune_context, unidentified_prune_interval * 1000, prune_task, NULL, 1) < 0) {
+               return;
+       }
+}
+
+/*! \brief Observer which is used to update our interval and default_realm when the global setting changes */
+static struct ast_sorcery_observer global_observer = {
+       .loaded = global_loaded,
+};
+
+
 int ast_sip_initialize_distributor(void)
 {
+       unidentified_requests = ao2_container_alloc_hash(AO2_ALLOC_OPT_LOCK_RWLOCK, 0,
+               DEFAULT_SUSPECTS_BUCKETS, suspects_hash, NULL, suspects_compare);
+       if (!unidentified_requests) {
+               return -1;
+       }
+
+       prune_context = ast_sched_context_create();
+       if (!prune_context) {
+               ast_sip_destroy_distributor();
+               return -1;
+       }
+
+       if (ast_sched_start_thread(prune_context)) {
+               ast_sip_destroy_distributor();
+               return -1;
+       }
+
+       ast_sorcery_observer_add(ast_sip_get_sorcery(), "global", &global_observer);
+       ast_sorcery_reload_object(ast_sip_get_sorcery(), "global");
+
        if (create_artificial_endpoint() || create_artificial_auth()) {
+               ast_sip_destroy_distributor();
                return -1;
        }
 
        if (internal_sip_register_service(&distributor_mod)) {
+               ast_sip_destroy_distributor();
                return -1;
        }
        if (internal_sip_register_service(&endpoint_mod)) {
+               ast_sip_destroy_distributor();
                return -1;
        }
        if (internal_sip_register_service(&auth_mod)) {
+               ast_sip_destroy_distributor();
                return -1;
        }
 
+       unid_formatter = ao2_alloc(sizeof(struct ast_sip_cli_formatter_entry), NULL);
+       if (!unid_formatter) {
+               ast_log(LOG_ERROR, "Unable to allocate memory for unid_formatter\n");
+               return -1;
+       }
+       unid_formatter->name = "unidentified_request";
+       unid_formatter->print_header = cli_unid_print_header;
+       unid_formatter->print_body = cli_unid_print_body;
+       unid_formatter->get_container = cli_unid_get_container;
+       unid_formatter->iterate = cli_unid_iterate;
+       unid_formatter->get_id = cli_unid_get_id;
+       unid_formatter->retrieve_by_id = cli_unid_retrieve_by_id;
+       ast_sip_register_cli_formatter(unid_formatter);
+       ast_cli_register_multiple(cli_commands, ARRAY_LEN(cli_commands));
+
        return 0;
 }
 
 void ast_sip_destroy_distributor(void)
 {
+       ast_cli_unregister_multiple(cli_commands, ARRAY_LEN(cli_commands));
+       ast_sip_unregister_cli_formatter(unid_formatter);
+
        internal_sip_unregister_service(&distributor_mod);
        internal_sip_unregister_service(&endpoint_mod);
        internal_sip_unregister_service(&auth_mod);
 
        ao2_cleanup(artificial_auth);
        ao2_cleanup(artificial_endpoint);
+       ao2_cleanup(unidentified_requests);
+
+       ast_sorcery_observer_remove(ast_sip_get_sorcery(), "global", &global_observer);
+
+       if (prune_context) {
+               ast_sched_context_destroy(prune_context);
+       }
 }
index e610bb8..4a31edf 100644 (file)
        <support_level>core</support_level>
  ***/
 
+/* From the auth/realm realtime column size */
+#define MAX_REALM_LENGTH 40
+static char default_realm[MAX_REALM_LENGTH + 1];
+
 AO2_GLOBAL_OBJ_STATIC(entity_id);
 
 /*!
@@ -409,7 +413,7 @@ static enum ast_sip_check_auth_result digest_check_auth(struct ast_sip_endpoint
 
        for (i = 0; i < auth_size; ++i) {
                if (ast_strlen_zero(auths[i]->realm)) {
-                       ast_string_field_set(auths[i], realm, "asterisk");
+                       ast_string_field_set(auths[i], realm, default_realm);
                }
                verify_res[i] = verify(auths[i], rdata, tdata->pool);
                if (verify_res[i] == AUTH_SUCCESS) {
@@ -456,6 +460,16 @@ static int build_entity_id(void)
        return 0;
 }
 
+static void global_loaded(const char *object_type)
+{
+       ast_sip_get_default_realm(default_realm, sizeof(default_realm));
+}
+
+/*! \brief Observer which is used to update our default_realm when the global setting changes */
+static struct ast_sorcery_observer global_observer = {
+       .loaded = global_loaded,
+};
+
 static int reload_module(void)
 {
        if (build_entity_id()) {
@@ -471,6 +485,10 @@ static int load_module(void)
        if (build_entity_id()) {
                return AST_MODULE_LOAD_DECLINE;
        }
+
+       ast_sorcery_observer_add(ast_sip_get_sorcery(), "global", &global_observer);
+       ast_sorcery_reload_object(ast_sip_get_sorcery(), "global");
+
        if (ast_sip_register_authenticator(&digest_authenticator)) {
                ao2_global_obj_release(entity_id);
                return AST_MODULE_LOAD_DECLINE;
@@ -480,6 +498,7 @@ static int load_module(void)
 
 static int unload_module(void)
 {
+       ast_sorcery_observer_remove(ast_sip_get_sorcery(), "global", &global_observer);
        ast_sip_unregister_authenticator(&digest_authenticator);
        ao2_global_obj_release(entity_id);
        return 0;
index 12af2ed..172566a 100644 (file)
@@ -29,7 +29,7 @@
 #include "asterisk/res_pjsip.h"
 #include "asterisk/module.h"
 
-static int get_endpoint_details(pjsip_rx_data *rdata, char *endpoint, size_t endpoint_size, char *domain, size_t domain_size)
+static int get_from_header(pjsip_rx_data *rdata, char *username, size_t username_size, char *domain, size_t domain_size)
 {
        pjsip_uri *from = rdata->msg_info.from->uri;
        pjsip_sip_uri *sip_from;
@@ -37,11 +37,28 @@ static int get_endpoint_details(pjsip_rx_data *rdata, char *endpoint, size_t end
                return -1;
        }
        sip_from = (pjsip_sip_uri *) pjsip_uri_get_uri(from);
-       ast_copy_pj_str(endpoint, &sip_from->user, endpoint_size);
+       ast_copy_pj_str(username, &sip_from->user, username_size);
        ast_copy_pj_str(domain, &sip_from->host, domain_size);
        return 0;
 }
 
+static pjsip_authorization_hdr *get_auth_header(pjsip_rx_data *rdata, char *username,
+       size_t username_size, char *realm, size_t realm_size, pjsip_authorization_hdr *start)
+{
+       pjsip_authorization_hdr *header;
+
+       header = pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_AUTHORIZATION, start);
+
+       if (!header || pj_stricmp2(&header->scheme, "digest")) {
+               return NULL;
+       }
+
+       ast_copy_pj_str(username, &header->credential.digest.username, username_size);
+       ast_copy_pj_str(realm, &header->credential.digest.realm, realm_size);
+
+       return header;
+}
+
 static int find_transport_state_in_use(void *obj, void *arg, int flags)
 {
        struct ast_sip_transport_state *transport_state = obj;
@@ -56,34 +73,30 @@ static int find_transport_state_in_use(void *obj, void *arg, int flags)
        return 0;
 }
 
-static struct ast_sip_endpoint *username_identify(pjsip_rx_data *rdata)
+static struct ast_sip_endpoint *find_endpoint(pjsip_rx_data *rdata, char *endpoint_name,
+       char *domain_name)
 {
-       char endpoint_name[64], domain_name[64], id[AST_UUID_STR_LEN];
+       char id[AST_UUID_STR_LEN];
        struct ast_sip_endpoint *endpoint;
        RAII_VAR(struct ast_sip_domain_alias *, alias, NULL, ao2_cleanup);
        RAII_VAR(struct ao2_container *, transport_states, NULL, ao2_cleanup);
        RAII_VAR(struct ast_sip_transport_state *, transport_state, NULL, ao2_cleanup);
        RAII_VAR(struct ast_sip_transport *, transport, NULL, ao2_cleanup);
 
-       if (get_endpoint_details(rdata, endpoint_name, sizeof(endpoint_name), domain_name, sizeof(domain_name))) {
-               return NULL;
-       }
-
        if (!ast_sip_get_disable_multi_domain()) {
                /* Attempt to find the endpoint given the name and domain provided */
                snprintf(id, sizeof(id), "%s@%s", endpoint_name, domain_name);
                if ((endpoint = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", id))) {
-                       goto done;
+                       return endpoint;
                }
 
                /* See if an alias exists for the domain provided */
                if ((alias = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "domain_alias", domain_name))) {
                        snprintf(id, sizeof(id), "%s@%s", endpoint_name, alias->domain);
                        if ((endpoint = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", id))) {
-                               goto done;
+                               return endpoint;
                        }
                }
-
                /* See if the transport this came in on has a provided domain */
                if ((transport_states = ast_sip_get_transport_states())
                        && (transport_state = ao2_callback(transport_states, 0, find_transport_state_in_use, rdata))
@@ -91,41 +104,95 @@ static struct ast_sip_endpoint *username_identify(pjsip_rx_data *rdata)
                        && !ast_strlen_zero(transport->domain)) {
                        snprintf(id, sizeof(id), "anonymous@%s", transport->domain);
                        if ((endpoint = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", id))) {
-                               goto done;
+                               return endpoint;
                        }
                }
        }
 
        /* Fall back to no domain */
-       endpoint = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", endpoint_name);
+       return ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "endpoint", endpoint_name);
+}
 
-done:
-       if (endpoint) {
-               if (!(endpoint->ident_method & AST_SIP_ENDPOINT_IDENTIFY_BY_USERNAME)) {
-                       ao2_ref(endpoint, -1);
-                       return NULL;
-               }
-               ast_debug(3, "Retrieved endpoint %s\n", ast_sorcery_object_get_id(endpoint));
-       } else {
-               ast_debug(3, "Could not identify endpoint by username '%s'\n", endpoint_name);
+static struct ast_sip_endpoint *username_identify(pjsip_rx_data *rdata)
+{
+       char username[64], domain[64];
+       struct ast_sip_endpoint *endpoint;
+
+       if (get_from_header(rdata, username, sizeof(username), domain, sizeof(domain))) {
+               return NULL;
+       }
+       ast_debug(3, "Attempting identify by From username '%s' domain '%s'\n", username, domain);
+
+       endpoint = find_endpoint(rdata, username, domain);
+       if (!endpoint) {
+               ast_debug(3, "Endpoint not found for From username '%s' domain '%s'\n", username, domain);
+               ao2_cleanup(endpoint);
+               return NULL;
+       }
+       if (!(endpoint->ident_method & AST_SIP_ENDPOINT_IDENTIFY_BY_USERNAME)) {
+               ast_debug(3, "Endpoint found for '%s' but 'username' method not supported'\n", username);
+               ao2_cleanup(endpoint);
+               return NULL;
        }
+       ast_debug(3, "Identified by From username '%s' domain '%s'\n", username, domain);
+
        return endpoint;
 }
 
+static struct ast_sip_endpoint *auth_username_identify(pjsip_rx_data *rdata)
+{
+       char username[64], realm[64];
+       struct ast_sip_endpoint *endpoint;
+       pjsip_authorization_hdr *auth_header = NULL;
+
+       while ((auth_header = get_auth_header(rdata, username, sizeof(username), realm, sizeof(realm),
+               auth_header ? auth_header->next : NULL))) {
+               ast_debug(3, "Attempting identify by Authorization username '%s' realm '%s'\n", username,
+                       realm);
+
+               endpoint = find_endpoint(rdata, username, realm);
+               if (!endpoint) {
+                       ast_debug(3, "Endpoint not found for Authentication username '%s' realm '%s'\n",
+                               username, realm);
+                       ao2_cleanup(endpoint);
+                       continue;
+               }
+               if (!(endpoint->ident_method & AST_SIP_ENDPOINT_IDENTIFY_BY_AUTH_USERNAME)) {
+                       ast_debug(3, "Endpoint found for '%s' but 'auth_username' method not supported'\n",
+                               username);
+                       ao2_cleanup(endpoint);
+                       continue;
+               }
+               ast_debug(3, "Identified by Authorization username '%s' realm '%s'\n", username, realm);
+
+               return endpoint;
+       }
+
+       return NULL;
+}
+
+
 static struct ast_sip_endpoint_identifier username_identifier = {
        .identify_endpoint = username_identify,
 };
 
+static struct ast_sip_endpoint_identifier auth_username_identifier = {
+       .identify_endpoint = auth_username_identify,
+};
+
+
 static int load_module(void)
 {
        CHECK_PJSIP_MODULE_LOADED();
 
        ast_sip_register_endpoint_identifier_with_name(&username_identifier, "username");
+       ast_sip_register_endpoint_identifier_with_name(&auth_username_identifier, "auth_username");
        return AST_MODULE_LOAD_SUCCESS;
 }
 
 static int unload_module(void)
 {
+       ast_sip_unregister_endpoint_identifier(&auth_username_identifier);
        ast_sip_unregister_endpoint_identifier(&username_identifier);
        return 0;
 }
index 8edd6ee..ae8440a 100644 (file)
@@ -657,6 +657,65 @@ static int rx_task(void *data)
        return res;
 }
 
+static int match_aor(const char *aor_name, const char *id)
+{
+       if (ast_strlen_zero(aor_name)) {
+               return 0;
+       }
+
+       if (!strcmp(aor_name, id)) {
+               ast_debug(3, "Matched id '%s' to aor '%s'\n", id, aor_name);
+               return 1;
+       }
+
+       return 0;
+}
+
+static char *find_aor_name(const char *username, const char *domain, const char *aors)
+{
+       char *configured_aors;
+       char *aor_name;
+       char *id_domain;
+       struct ast_sip_domain_alias *alias;
+
+       id_domain = ast_alloca(strlen(username) + strlen(domain) + 2);
+       sprintf(id_domain, "%s@%s", username, domain);
+
+       /* Look for exact match on username@domain */
+       configured_aors = ast_strdupa(aors);
+       while ((aor_name = ast_strip(strsep(&configured_aors, ",")))) {
+               if (match_aor(aor_name, id_domain)) {
+                       return ast_strdup(aor_name);
+               }
+       }
+
+       /* If there's a domain alias, look for exact match on username@domain_alias */
+       alias = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "domain_alias", domain);
+       if (alias) {
+               char *id_domain_alias = ast_alloca(strlen(username) + strlen(alias->domain) + 2);
+
+               sprintf(id_domain, "%s@%s", username, alias->domain);
+               ao2_cleanup(alias);
+
+               configured_aors = ast_strdupa(aors);
+               while ((aor_name = ast_strip(strsep(&configured_aors, ",")))) {
+                       if (match_aor(aor_name, id_domain_alias)) {
+                               return ast_strdup(aor_name);
+                       }
+               }
+       }
+
+       /* Look for exact match on username only */
+       configured_aors = ast_strdupa(aors);
+       while ((aor_name = ast_strip(strsep(&configured_aors, ",")))) {
+               if (match_aor(aor_name, username)) {
+                       return ast_strdup(aor_name);
+               }
+       }
+
+       return NULL;
+}
+
 static pj_bool_t registrar_on_rx_request(struct pjsip_rx_data *rdata)
 {
        RAII_VAR(struct serializer *, ser, NULL, ao2_cleanup);
@@ -665,10 +724,10 @@ static pj_bool_t registrar_on_rx_request(struct pjsip_rx_data *rdata)
        RAII_VAR(struct ast_sip_endpoint *, endpoint,
                 ast_pjsip_rdata_get_endpoint(rdata), ao2_cleanup);
        RAII_VAR(struct ast_sip_aor *, aor, NULL, ao2_cleanup);
-       pjsip_sip_uri *uri;
-       char *domain_name;
-       char *configured_aors, *aor_name;
-       RAII_VAR(struct ast_str *, id, NULL, ast_free);
+       char *domain_name = NULL;
+       char *username = NULL;
+       RAII_VAR(char *, aor_name, NULL, ast_free);
+       int i;
 
        if (pjsip_method_cmp(&rdata->msg_info.msg->line.req.method, &pjsip_register_method) || !endpoint) {
                return PJ_FALSE;
@@ -689,38 +748,46 @@ static pj_bool_t registrar_on_rx_request(struct pjsip_rx_data *rdata)
                return PJ_TRUE;
        }
 
-       uri = pjsip_uri_get_uri(rdata->msg_info.to->uri);
-       domain_name = ast_alloca(uri->host.slen + 1);
-       ast_copy_pj_str(domain_name, &uri->host, uri->host.slen + 1);
+       for (i = 0; i < AST_VECTOR_SIZE(&endpoint->ident_method_order); i++) {
+               pjsip_sip_uri *uri;
+               pjsip_authorization_hdr *header = NULL;
 
-       configured_aors = ast_strdupa(endpoint->aors);
+               switch (AST_VECTOR_GET(&endpoint->ident_method_order, i)) {
+               case AST_SIP_ENDPOINT_IDENTIFY_BY_USERNAME :
+                       uri = pjsip_uri_get_uri(rdata->msg_info.to->uri);
 
-       /* Iterate the configured AORs to see if the user or the user+domain match */
-       while ((aor_name = ast_strip(strsep(&configured_aors, ",")))) {
-               struct ast_sip_domain_alias *alias = NULL;
+                       domain_name = ast_alloca(uri->host.slen + 1);
+                       ast_copy_pj_str(domain_name, &uri->host, uri->host.slen + 1);
+                       username = ast_alloca(uri->user.slen + 1);
+                       ast_copy_pj_str(username, &uri->user, uri->user.slen + 1);
 
-               if (ast_strlen_zero(aor_name)) {
-                       continue;
-               }
-
-               if (!pj_strcmp2(&uri->user, aor_name)) {
+                       aor_name = find_aor_name(username, domain_name, endpoint->aors);
+                       if (aor_name) {
+                               ast_debug(3, "Matched aor '%s' by To username\n", aor_name);
+                       }
                        break;
+               case AST_SIP_ENDPOINT_IDENTIFY_BY_AUTH_USERNAME :
+                       while ((header = pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_AUTHORIZATION,
+                               header ? header->next : NULL))) {
+                               if (header && !pj_stricmp2(&header->scheme, "digest")) {
+                                       username = ast_alloca(header->credential.digest.username.slen + 1);
+                                       ast_copy_pj_str(username, &header->credential.digest.username, header->credential.digest.username.slen + 1);
+                                       domain_name = ast_alloca(header->credential.digest.realm.slen + 1);
+                                       ast_copy_pj_str(domain_name, &header->credential.digest.realm, header->credential.digest.realm.slen + 1);
+
+                                       aor_name = find_aor_name(username, domain_name, endpoint->aors);
+                                       if (aor_name) {
+                                               ast_debug(3, "Matched aor '%s' by Authentication username\n", aor_name);
+                                               break;
+                                       }
+                               }
+                       }
+                       break;
+               default:
+                       continue;
                }
 
-               if (!id && !(id = ast_str_create(uri->user.slen + uri->host.slen + 2))) {
-                       return PJ_TRUE;
-               }
-
-               ast_str_set(&id, 0, "%.*s@", (int)uri->user.slen, uri->user.ptr);
-               if ((alias = ast_sorcery_retrieve_by_id(ast_sip_get_sorcery(), "domain_alias", domain_name))) {
-                       ast_str_append(&id, 0, "%s", alias->domain);
-                       ao2_cleanup(alias);
-               } else {
-                       ast_str_append(&id, 0, "%s", domain_name);
-               }
-
-               if (!strcmp(aor_name, ast_str_buffer(id))) {
-                       ast_free(id);
+               if (aor_name) {
                        break;
                }
        }
@@ -729,7 +796,7 @@ static pj_bool_t registrar_on_rx_request(struct pjsip_rx_data *rdata)
                /* The provided AOR name was not found (be it within the configuration or sorcery itself) */
                pjsip_endpt_respond_stateless(ast_sip_get_pjsip_endpoint(), rdata, 404, NULL, NULL, NULL);
                ast_sip_report_req_no_support(endpoint, rdata, "registrar_requested_aor_not_found");
-               ast_log(LOG_WARNING, "AOR '%.*s' not found for endpoint '%s'\n", (int)uri->user.slen, uri->user.ptr, ast_sorcery_object_get_id(endpoint));
+               ast_log(LOG_WARNING, "AOR '%s' not found for endpoint '%s'\n", username, ast_sorcery_object_get_id(endpoint));
                return PJ_TRUE;
        }