res_pjsip_endpoint_identifier_ip.c: Added regex support to match_header
authorRichard Mudgett <rmudgett@digium.com>
Tue, 24 Jul 2018 18:44:41 +0000 (13:44 -0500)
committerRichard Mudgett <rmudgett@digium.com>
Fri, 27 Jul 2018 15:58:38 +0000 (10:58 -0500)
This patch adds regular expression support to make the identify section's
match_header option more useful when attempting to match complex headers
like the 'To' or 'From' headers.  The 'From' header has variable
components such as the tag parameter that you cannot predict.  To specify
a regular expression put slashes around the regular expression in place of
the header value.

[identify-alice]
type=identify
endpoint=alice
match_header=From: /<sip:alice@127\\.0\\.0\\.1>/

* Added regex support to match_header so you could match a 'To' header
among other complex headers.

Fixed reported crashes when trying to match special headers like 'Contact'.
The identify section's match_header method used code that assumed you were
matching a generic header.  Any other type of header could cause a crash
if the header structure variant did not match the generic header enough.

* Made use code that will work for any header type instead of code
specific to generic headers.

Other fixes while in the area:

* Made check all headers of the requested name.
* Added some more sanity checks to the configured identify matching
options when applying the configuration.

ASTERISK-27548

Change-Id: I27dfd4ff5e2259b906640e3c330681b76b4ed1f1

CHANGES
res/res_pjsip_endpoint_identifier_ip.c

diff --git a/CHANGES b/CHANGES
index c1cd83f..38c87e6 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -143,6 +143,13 @@ res_pjsip
    when both 'SIP' and 'Q.850' Reason headers are received.  This option allows
    the 'Q.850' Reason header to be suppressed.  The default value is 'no'.
 
+res_pjsip_endpoint_identifier_ip
+------------------
+ * Added regex support to the identify section match_header option.  You
+   specify a regex instead of an explicit string by surrounding the header
+   value with slashes:
+   match_header = SIPHeader: /regex/
+
 ------------------------------------------------------------------------------
 --- Functionality changes from Asterisk 15.4.0 to Asterisk 15.5.0 ------------
 ------------------------------------------------------------------------------
index 2468786..30b2a2e 100644 (file)
@@ -42,7 +42,7 @@
                                <description>
                                        <para>This module provides alternatives to matching inbound requests to
                                        a configured endpoint. At least one of the matching mechanisms
-                                       must be provided, or the object configuration will be invalid.</para>
+                                       must be provided, or the object configuration is invalid.</para>
                                        <para>The matching mechanisms are provided by the following
                                        configuration options:</para>
                                        <enumlist>
                                                specified with a <literal>:</literal>, as in
                                                <literal>match_header = SIPHeader: value</literal>.
                                                </para>
+                                               <para>The specified SIP header value can be a regular
+                                               expression if the value is of the form
+                                               /<replaceable>regex</replaceable>/.
+                                               </para>
+                                               <note><para>Use of a regex is expensive so be sure you need
+                                               to use a regex to match your endpoint.
+                                               </para></note>
                                        </description>
                                </configOption>
                                <configOption name="type">
@@ -109,13 +116,21 @@ struct ip_identify_match {
                AST_STRING_FIELD(endpoint_name);
                /*! If matching by header, the header/value to match against */
                AST_STRING_FIELD(match_header);
+               /*! SIP header name of the match_header string */
+               AST_STRING_FIELD(match_header_name);
+               /*! SIP header value of the match_header string */
+               AST_STRING_FIELD(match_header_value);
        );
+       /*! Compiled match_header regular expression when is_regex is non-zero */
+       regex_t regex_buf;
        /*! \brief Networks or addresses that should match this */
        struct ast_ha *matches;
-       /*! \brief Perform SRV resolution of hostnames */
-       unsigned int srv_lookups;
        /*! \brief Hosts to be resolved when applying configuration */
        struct ao2_container *hosts;
+       /*! \brief Perform SRV resolution of hostnames */
+       unsigned int srv_lookups;
+       /*! Non-zero if match_header has a regular expression (i.e., regex_buf is valid) */
+       unsigned int is_regex:1;
 };
 
 /*! \brief Destructor function for a matching object */
@@ -126,6 +141,9 @@ static void ip_identify_destroy(void *obj)
        ast_string_field_free_memory(identify);
        ast_free_ha(identify->matches);
        ao2_cleanup(identify->hosts);
+       if (identify->is_regex) {
+               regfree(&identify->regex_buf);
+       }
 }
 
 /*! \brief Allocator function for a matching object */
@@ -146,47 +164,66 @@ static int header_identify_match_check(void *obj, void *arg, int flags)
 {
        struct ip_identify_match *identify = obj;
        struct pjsip_rx_data *rdata = arg;
-       pjsip_generic_string_hdr *header;
+       pjsip_hdr *header;
        pj_str_t pj_header_name;
-       pj_str_t pj_header_value;
-       char *c_header;
-       char *c_value;
+       int header_present;
 
        if (ast_strlen_zero(identify->match_header)) {
                return 0;
        }
 
-       c_header = ast_strdupa(identify->match_header);
-       c_value = strchr(c_header, ':');
-       if (!c_value) {
-               /* This should not be possible.  The object cannot be created if so. */
-               ast_assert(0);
-               return 0;
-       }
-       *c_value = '\0';
-       c_value++;
-       c_value = ast_strip(c_value);
+       pj_header_name = pj_str((void *) identify->match_header_name);
+
+       /* Check all headers of the given name for a match. */
+       header_present = 0;
+       for (header = NULL;
+               (header = pjsip_msg_find_hdr_by_name(rdata->msg_info.msg, &pj_header_name, header));
+               header = header->next) {
+               char *pos;
+               int len;
+               char buf[PATH_MAX];
 
-       pj_header_name = pj_str(c_header);
-       header = pjsip_msg_find_hdr_by_name(rdata->msg_info.msg, &pj_header_name, NULL);
-       if (!header) {
-               ast_debug(3, "Identify '%s': SIP message does not have header '%s'\n",
+               header_present = 1;
+
+               /* Print header line to buf */
+               len = pjsip_hdr_print_on(header, buf, sizeof(buf) - 1);
+               if (len < 0) {
+                       /* Buffer not large enough or no header vptr! */
+                       ast_assert(0);
+                       continue;
+               }
+               buf[len] = '\0';
+
+               /* Remove header name from pj_buf and trim blanks. */
+               pos = strchr(buf, ':');
+               if (!pos) {
+                       /* No header name?  Bug in PJPROJECT if so. */
+                       ast_assert(0);
+                       continue;
+               }
+               pos = ast_strip(pos + 1);
+
+               /* Does header value match what we are looking for? */
+               if (identify->is_regex) {
+                       if (!regexec(&identify->regex_buf, pos, 0, NULL, 0)) {
+                               return CMP_MATCH;
+                       }
+               } else if (!strcmp(identify->match_header_value, pos)) {
+                       return CMP_MATCH;
+               }
+
+               ast_debug(3, "Identify '%s': SIP message has '%s' header but value '%s' does not match '%s'.\n",
                        ast_sorcery_object_get_id(identify),
-                       c_header);
-               return 0;
+                       identify->match_header_name,
+                       pos,
+                       identify->match_header_value);
        }
-
-       pj_header_value = pj_str(c_value);
-       if (pj_strcmp(&pj_header_value, &header->hvalue)) {
-               ast_debug(3, "Identify '%s': SIP message has header '%s' but value '%.*s' does not match '%s'\n",
+       if (!header_present) {
+               ast_debug(3, "Identify '%s': SIP message does not have '%s' header.\n",
                        ast_sorcery_object_get_id(identify),
-                       c_header,
-                       (int) pj_strlen(&header->hvalue), pj_strbuf(&header->hvalue),
-                       c_value);
-               return 0;
+                       identify->match_header_name);
        }
-
-       return CMP_MATCH;
+       return 0;
 }
 
 /*! \brief Comparator function for matching an object by IP address */
@@ -404,14 +441,57 @@ static int ip_identify_apply(const struct ast_sorcery *sorcery, void *obj)
                        ast_sorcery_object_get_id(identify));
                return -1;
        }
-       if (!ast_strlen_zero(identify->match_header)
-               && !strchr(identify->match_header, ':')) {
-               ast_log(LOG_ERROR, "Identify '%s' missing ':' separator in match_header '%s'.\n",
-                       ast_sorcery_object_get_id(identify), identify->match_header);
-               return -1;
+
+       if (!ast_strlen_zero(identify->match_header)) {
+               char *c_header;
+               char *c_value;
+               int len;
+
+               /* Split the header name and value */
+               c_header = ast_strdupa(identify->match_header);
+               c_value = strchr(c_header, ':');
+               if (!c_value) {
+                       ast_log(LOG_ERROR, "Identify '%s' missing ':' separator in match_header '%s'.\n",
+                               ast_sorcery_object_get_id(identify), identify->match_header);
+                       return -1;
+               }
+               *c_value = '\0';
+               c_value = ast_strip(c_value + 1);
+               c_header = ast_strip(c_header);
+
+               if (ast_strlen_zero(c_header)) {
+                       ast_log(LOG_ERROR, "Identify '%s' has no SIP header to match in match_header '%s'.\n",
+                               ast_sorcery_object_get_id(identify), identify->match_header);
+                       return -1;
+               }
+
+               if (!strcmp(c_value, "//")) {
+                       /* An empty regex is the same as an empty literal string. */
+                       c_value = "";
+               }
+
+               if (ast_string_field_set(identify, match_header_name, c_header)
+                       || ast_string_field_set(identify, match_header_value, c_value)) {
+                       return -1;
+               }
+
+               len = strlen(c_value);
+               if (2 < len && c_value[0] == '/' && c_value[len - 1] == '/') {
+                       /* Make "/regex/" into "regex" */
+                       c_value[len - 1] = '\0';
+                       ++c_value;
+
+                       if (regcomp(&identify->regex_buf, c_value, REG_EXTENDED | REG_NOSUB)) {
+                               ast_log(LOG_ERROR, "Identify '%s' failed to compile match_header regex '%s'.\n",
+                                       ast_sorcery_object_get_id(identify), c_value);
+                               return -1;
+                       }
+                       identify->is_regex = 1;
+               }
        }
 
        if (!identify->hosts) {
+               /* No match addresses to resolve */
                return 0;
        }