]> git.ipfire.org Git - thirdparty/asterisk.git/commitdiff
manager: Enhance event filtering for performance
authorGeorge Joseph <gjoseph@sangoma.com>
Wed, 31 Jul 2024 22:05:45 +0000 (16:05 -0600)
committerGeorge Joseph <gjoseph@sangoma.com>
Mon, 23 Sep 2024 14:45:38 +0000 (14:45 +0000)
UserNote: You can now perform more granular filtering on events
in manager.conf using expressions like
`eventfilter(name(Newchannel),header(Channel),method(starts_with)) = PJSIP/`
This is much more efficient than
`eventfilter = Event: Newchannel.*Channel: PJSIP/`
Full syntax guide is in configs/samples/manager.conf.sample.

configs/samples/manager.conf.sample
main/manager.c
main/manager_doc.xml

index 342f290ea7c25fb9f5a57c15b8c5c41e28741d26..a8e3e7ed5f4268b61649b5d993008c346e7c2072 100644 (file)
@@ -104,22 +104,184 @@ bindaddr = 0.0.0.0
 ; originates a call. You can define multiple setvar= commands for one manager
 ; user.
 ;
+
+;--
+-- eventfilter --------------------------------------------------------
+Include and/or exclude events for this user.
+
+There are two ways to use this feature... Legacy and Advanced.
+
+Legacy Event Filtering:
+
+This is the original method of filtering events.  It's no longer
+recommended but still supported for backwards compatibility.  The filter
+is a regular expression, optionally prefixed with an exclamation point (!).
+The regular expression is applied to the entire payload of every event.
+If any part of the event payload matches, the event is included.  If the
+first character of the filter is an exclamation point (!), the event is
+excluded. On a busy system, this is a resource intensive process and the
+reason it's no longer recommended.
+
+Another issue with legacy filtering is that regexes are very sensitive to
+whitespace and separators.  "Event:Newchannel" will NOT work because of
+the missing space after the ':'.  Neither will "Event:  Newchannel" or
+"Event Newchannel" because of the extra space in the first expression
+and the missing ':' in the second.
+
+Advanced Event Filtering:
+
+Advanced filtering still allows you to use regular expressions but adds
+the ability to pre-select certain events and constrain the regular
+expression to matching the contents of a specific event header.
+The syntax is:
+
+eventfilter(<match_criteria>) = [ <match_expression> ]
+
+<match_criteria> : [ action(include|exclude) | name(<event_name>) |
+    header(<header_name>) | method(<match_method>) ][, ...]
+
+You can specify at most one of each of the following in any order,
+separated by commas.
+
+    action(include|exclude): Default: 'include'. Instead of using '!' to
+    exclude matching events, specify 'action(exclude)'.  Although the
+    default is "include" if "action" isn't specified, adding
+    "action(include)" will help with readability.
+
+    name(<event_name>): Include only events with a name exactly matching
+    <event_name>.  This is actually implemented using the "hash" of the
+    event names and is therefore much more efficient than using a regular
+    expression.
+
+    header(<header_name>): Include only events that have a header exactly
+    matching <header_name>.  Additionally, the data to be searched will
+    be constrained to the value of this header instead of the entire
+    event payload.
+
+    method(regex | exact | starts_with | ends_with | contains | none ):
+    How should <match_expression> be applied to the event data?  The data may
+    be the entire event payload or, if header(<header_name>) was used, the
+    value of that specific header.  If 'action(exclude)' was specified, a
+    "match" here will cause the event to be excluded instead of included.
+
+        regex:  As a regular expression that, if matched anywhere in the
+        data, constitutes a match.
+
+        exact: As a simple string that must match all of the data.
+        Probably only useful when the data is constrained to a specific header
+        and the data itself is a simple value.
+
+        starts_with: As a simple string that, if found at the beginning of the
+        data, constitutes a match.
+
+        ends_with: As a simple string that, if found at the end of the data,
+        constitutes a match.
+
+        contains: As a simple string that, if found anywhere in the data,
+        constitutes a match.
+
+        none: Ignore <match_expression> altogether.  This is the default
+        because the majority of use cases for event filtering involve
+        including or excluding events by event name without regard to the
+        event payload.  In this case, you can just leave <match_expression>
+        empty.
+
+  TIP:  Although match criteria order doesn't matter to Asterisk, using the
+  order shown can help you read them.  For instance...
+  eventfilter(action(exclude),name(Newchannel),header(Channel),method(starts_with)) = Local/
+  ...means "Exclude Newchannel events with a Channel header that starts with Local/"
+
+Event Filter Processing Ordering:
+
+Both Legacy and Advanced filter entries are processed as follows:
+ - If no filters are configured, all events are reported as normal.
+
+ - If there are 'include' filters only, an event that matches ANY filter
+   will be reported.
+
+ - If there are 'exclude' filters only, an event that matched ANY filter
+   will be excluded.
+
+ - If there are both 'include' and 'exclude' filters, all 'include' filters
+   are matched first, then the 'exclude' filters will be applied to the
+   resulting set.
+--;
+
+; ----- Legacy Filter Examples:
+; Every legacy filter expression results in regular expression matching
+; on the entire payload of every event even if no regular expression
+; meta-characters were used.
+
+; Only include Newchannel events
 ;eventfilter=Event: Newchannel
+
+; Only include events of any type with a "Channel" header that matches
+; the regular expression.
 ;eventfilter=Channel: (PJ)?SIP/(james|jim|john)-
+
+; Only include Newchannel events which contain a "Channel" header
+; for PJSIP channels.
+;eventfilter = Event: Newchannel.*Channel: PJSIP/
+
+; Only include Newchannel or Hangup events whose "Channel" header doesn't start
+; with Local/.  All other events are filtered out.
+;eventfilter = Event: Newchannel
+;eventfilter = Event: Hangup
+;eventfilter = !Channel: Local/
+; This causes three regexes to be searched for on every event!
+
+; Include ALL events EXCEPT Newchannel and Hangup events whose "Channel" header
+; starts with Local/.
+; Other Newchannel and Hangup events ARE reported.
+;eventfilter = !Event: (Newchannel|Hangup).*Channel: Local/
+; This causes one regex to be searched for but it's a fairly expensive
+; one.
+
+; Exclude any event that has a "Channel" header whose value starts with "DADHI/"
 ;eventfilter=!Channel: DAHDI/
-; The eventfilter option is used to whitelist or blacklist events per user.
-; A filter consists of an (unanchored) regular expression that is run on the
-; entire event data. If the first character of the filter is an exclamation
-; mark (!), the filter is appended to the blacklist instead of the whitelist.
-; After first checking the read access below, the regular expression filters
-; are processed as follows:
-; - If no filters are configured all events are reported as normal.
-; - If there are white filters only: implied black all filter processed first,
-; then white filters.
-; - If there are black filters only: implied white all filter processed first,
-; then black filters.
-; - If there are both white and black filters: implied black all filter processed
-; first, then white filters, and lastly black filters.
+
+; ----- Advanced Filter Examples:
+; All of these examples are WAY more efficient than their legacy
+; equivalents.
+
+; Include only "Newchannel" events.
+; eventfilter(name(Newchannel)) =
+; Note that there's nothing to the right of the '=' because you don't care
+; what's in the payload.  You still need the '=' though or the config file
+; parser will complain.  'action(include)' and 'method(none)' are implied.
+
+; Only include events of any type with a "Channel" header that matches
+; the regular expression.
+;eventfilter(action(include),header(Channel),method(regex)) = (PJ)?SIP/(james|jim|john)-
+; We're still testing every event but because we only apply the regex to the
+; value of the Channel header this is still more efficient than using the
+; legacy method.
+
+; Only include Newchannel and Hangup events whose "Channel" header doesn't
+; start with Local/.
+;eventfilter(action(include),name(Newchannel)) =
+;eventfilter(action(include),name(Hangup)) =
+;eventfilter(header(Channel),action(exclude),method(starts_with)) = Local/
+; No regexes at all.  We do the hash match against the event names first and
+; only mathcing events are passed to the next filter.
+; Then, in only those events, we look for a Channel header by exact match, then
+; look for 'Local/' at the beginning of its value.
+
+; Include ALL events EXCEPT Newchannel and Hangup events whose "Channel" header
+; starts with Local/.
+; Other Newchannel and Hangup events ARE reported.
+;eventfilter(action(exclude),name(Newchannel),header(Channel),method(starts_with)) = Local/
+;eventfilter(action(exclude),name(Hangup),header(Channel),method(starts_with)) = Local/
+; Again, no regexes. Very efficient because the filters start by looking for
+; a hash match on the event name.
+
+; Exclude any event that has a "Channel" header whose value starts with "DADHI/"
+;eventfilter(action(exclude),header(Channel),method(starts_with)) = DAHDI/
+; We're still testing every event but there are no regexes involved at all.
+
+;--
+-- eventfilter end ----------------------------------------------------
+--;
 
 ;
 ; If the device connected via this user accepts input slowly,
index 8d9d4ffbaa7b36994973b8254cbd2a43d8aac94a..225b7e3a677346093041d337c4b04006a377ea06 100644 (file)
@@ -124,9 +124,10 @@ enum error_type {
 };
 
 enum add_filter_result {
-       FILTER_SUCCESS,
+       FILTER_SUCCESS = 0,
        FILTER_ALLOC_FAILED,
        FILTER_COMPILE_FAIL,
+       FILTER_FORMAT_ERROR,
 };
 
 /*!
@@ -153,6 +154,7 @@ struct eventqent {
        int category;
        unsigned int seq;       /*!< sequence number */
        struct timeval tv;  /*!< When event was allocated */
+       int event_name_hash;
        AST_RWLIST_ENTRY(eventqent) eq_next;
        char eventdata[1];      /*!< really variable size, allocated by append_event() */
 };
@@ -294,8 +296,8 @@ struct mansession_session {
        int writeperm;          /*!< Authorization for writing */
        char inbuf[1025];       /*!< Buffer -  we use the extra byte to add a '\\0' and simplify parsing */
        int inlen;              /*!< number of buffered bytes */
-       struct ao2_container *whitefilters;     /*!< Manager event filters - white list */
-       struct ao2_container *blackfilters;     /*!< Manager event filters - black list */
+       struct ao2_container *includefilters;   /*!< Manager event filters - include list */
+       struct ao2_container *excludefilters;   /*!< Manager event filters - exclude list */
        struct ast_variable *chanvars;  /*!< Channel variables to set for originate */
        int send_events;        /*!<  XXX what ? */
        struct eventqent *last_ev;      /*!< last event processed. */
@@ -349,8 +351,8 @@ struct ast_manager_user {
        int displayconnects;            /*!< XXX unused */
        int allowmultiplelogin; /*!< Per user option*/
        int keep;                       /*!< mark entries created on a reload */
-       struct ao2_container *whitefilters; /*!< Manager event filters - white list */
-       struct ao2_container *blackfilters; /*!< Manager event filters - black list */
+       struct ao2_container *includefilters; /*!< Manager event filters - include list */
+       struct ao2_container *excludefilters; /*!< Manager event filters - exclude list */
        struct ast_acl_list *acl;       /*!< ACL setting */
        char *a1_hash;                  /*!< precalculated A1 for Digest auth */
        struct ast_variable *chanvars;  /*!< Channel variables to set for originate */
@@ -382,9 +384,41 @@ static int __attribute__((format(printf, 9, 0))) __manager_event_sessions(
        const char *func,
        const char *fmt,
        ...);
-static enum add_filter_result manager_add_filter(const char *filter_pattern, struct ao2_container *whitefilters, struct ao2_container *blackfilters);
 
-static int match_filter(struct mansession *s, char *eventdata);
+enum event_filter_match_type {
+       FILTER_MATCH_REGEX = 0,
+       FILTER_MATCH_EXACT,
+       FILTER_MATCH_STARTS_WITH,
+       FILTER_MATCH_ENDS_WITH,
+       FILTER_MATCH_CONTAINS,
+       FILTER_MATCH_NONE,
+};
+
+static char *match_type_names[] = {
+       [FILTER_MATCH_REGEX] = "regex",
+       [FILTER_MATCH_EXACT] = "exact",
+       [FILTER_MATCH_STARTS_WITH] = "starts_with",
+       [FILTER_MATCH_ENDS_WITH] = "ends_with",
+       [FILTER_MATCH_CONTAINS] = "contains",
+       [FILTER_MATCH_NONE] = "none",
+};
+
+struct event_filter_entry {
+       enum event_filter_match_type match_type;
+       regex_t *regex_filter;
+       char *string_filter;
+       char *event_name;
+       unsigned int event_name_hash;
+       char *header_name;
+       int is_excludefilter;
+};
+
+static enum add_filter_result manager_add_filter(const char *criteria,
+       const char *filter_pattern, struct ao2_container *includefilters,
+       struct ao2_container *excludefilters);
+
+static int should_send_event(struct ao2_container *includefilters,
+       struct ao2_container *excludefilters, struct eventqent *eqe);
 
 /*!
  * @{ \brief Define AMI message types.
@@ -593,6 +627,7 @@ static void manager_generic_msg_cb(void *data, struct stasis_subscription *sub,
                ao2_cleanup(sessions);
                return;
        }
+
        manager_event_sessions(sessions, class_type, type,
                "%s", ast_str_buffer(event_buffer));
        ast_free(event_buffer);
@@ -890,8 +925,14 @@ static struct mansession_session *unref_mansession(struct mansession_session *s)
 
 static void event_filter_destructor(void *obj)
 {
-       regex_t *regex_filter = obj;
-       regfree(regex_filter);
+       struct event_filter_entry *entry = obj;
+       if (entry->regex_filter) {
+               regfree(entry->regex_filter);
+               ast_free(entry->regex_filter);
+       }
+       ast_free(entry->event_name);
+       ast_free(entry->header_name);
+       ast_free(entry->string_filter);
 }
 
 static void session_destructor(void *obj)
@@ -913,12 +954,12 @@ static void session_destructor(void *obj)
                ast_variables_destroy(session->chanvars);
        }
 
-       if (session->whitefilters) {
-               ao2_t_ref(session->whitefilters, -1, "decrement ref for white container, should be last one");
+       if (session->includefilters) {
+               ao2_t_ref(session->includefilters, -1, "decrement ref for include container, should be last one");
        }
 
-       if (session->blackfilters) {
-               ao2_t_ref(session->blackfilters, -1, "decrement ref for black container, should be last one");
+       if (session->excludefilters) {
+               ao2_t_ref(session->excludefilters, -1, "decrement ref for exclude container, should be last one");
        }
 
        ast_mutex_destroy(&session->notify_lock);
@@ -935,9 +976,9 @@ static struct mansession_session *build_mansession(const struct ast_sockaddr *ad
                return NULL;
        }
 
-       newsession->whitefilters = ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_MUTEX, 0, NULL, NULL);
-       newsession->blackfilters = ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_MUTEX, 0, NULL, NULL);
-       if (!newsession->whitefilters || !newsession->blackfilters) {
+       newsession->includefilters = ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_MUTEX, 0, NULL, NULL);
+       newsession->excludefilters = ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_MUTEX, 0, NULL, NULL);
+       if (!newsession->includefilters || !newsession->excludefilters) {
                ao2_ref(newsession, -1);
                return NULL;
        }
@@ -2344,16 +2385,16 @@ static int authenticate(struct mansession *s, const struct message *m)
                s->session->chanvars = ast_variables_dup(user->chanvars);
        }
 
-       filter_iter = ao2_iterator_init(user->whitefilters, 0);
+       filter_iter = ao2_iterator_init(user->includefilters, 0);
        while ((regex_filter = ao2_iterator_next(&filter_iter))) {
-               ao2_t_link(s->session->whitefilters, regex_filter, "add white user filter to session");
+               ao2_t_link(s->session->includefilters, regex_filter, "add include user filter to session");
                ao2_t_ref(regex_filter, -1, "remove iterator ref");
        }
        ao2_iterator_destroy(&filter_iter);
 
-       filter_iter = ao2_iterator_init(user->blackfilters, 0);
+       filter_iter = ao2_iterator_init(user->excludefilters, 0);
        while ((regex_filter = ao2_iterator_next(&filter_iter))) {
-               ao2_t_link(s->session->blackfilters, regex_filter, "add black user filter to session");
+               ao2_t_link(s->session->excludefilters, regex_filter, "add exclude user filter to session");
                ao2_t_ref(regex_filter, -1, "remove iterator ref");
        }
        ao2_iterator_destroy(&filter_iter);
@@ -3144,7 +3185,7 @@ static int action_waitevent(struct mansession *s, const struct message *m)
                while ((eqe = advance_event(eqe))) {
                        if (((s->session->readperm & eqe->category) == eqe->category)
                                && ((s->session->send_events & eqe->category) == eqe->category)
-                               && match_filter(s, eqe->eventdata)) {
+                               && should_send_event(s->session->includefilters, s->session->excludefilters, eqe)) {
                                astman_append(s, "%s", eqe->eventdata);
                        }
                        s->session->last_ev = eqe;
@@ -5340,33 +5381,118 @@ static int action_timeout(struct mansession *s, const struct message *m)
        return 0;
 }
 
-static int whitefilter_cmp_fn(void *obj, void *arg, void *data, int flags)
+/*!
+ * \brief Test eventdata against a filter entry
+ *
+ * \param entry The event_filter entry to match with
+ * \param eventdata  The data to match against
+ * \retval 0 if no match
+ * \retval 1 if match
+ */
+static int match_eventdata(struct event_filter_entry *entry, const char *eventdata)
+{
+       switch(entry->match_type) {
+       case FILTER_MATCH_REGEX:
+               return regexec(entry->regex_filter, eventdata, 0, NULL, 0) == 0;
+       case FILTER_MATCH_STARTS_WITH:
+               return ast_begins_with(eventdata, entry->string_filter);
+       case FILTER_MATCH_ENDS_WITH:
+               return ast_ends_with(eventdata, entry->string_filter);
+       case FILTER_MATCH_CONTAINS:
+               return strstr(eventdata, entry->string_filter) != NULL;
+       case FILTER_MATCH_EXACT:
+               return strcmp(eventdata, entry->string_filter) == 0;
+       case FILTER_MATCH_NONE:
+               return 1;
+       }
+
+       return 0;
+}
+
+static int filter_cmp_fn(void *obj, void *arg, void *data, int flags)
 {
-       regex_t *regex_filter = obj;
-       const char *eventdata = arg;
+       struct eventqent *eqe = arg;
+       struct event_filter_entry *filter_entry = obj;
+       char *line_buffer_start = NULL;
+       char *line_buffer = NULL;
+       char *line = NULL;
+       int match = 0;
        int *result = data;
 
-       if (!regexec(regex_filter, eventdata, 0, NULL, 0)) {
-               *result = 1;
-               return (CMP_MATCH | CMP_STOP);
+       if (filter_entry->event_name_hash) {
+               if (eqe->event_name_hash != filter_entry->event_name_hash) {
+                       goto done;
+               }
        }
 
-       return 0;
+       /* We're looking at the entire event data */
+       if (!filter_entry->header_name) {
+               match = match_eventdata(filter_entry, eqe->eventdata);
+               goto done;
+       }
+
+       /* We're looking at a specific header */
+       line_buffer_start = ast_strdup(eqe->eventdata);
+       line_buffer = line_buffer_start;
+       if (!line_buffer_start) {
+               goto done;
+       }
+
+       while ((line = ast_read_line_from_buffer(&line_buffer))) {
+               if (ast_begins_with(line, filter_entry->header_name)) {
+                       line += strlen(filter_entry->header_name);
+                       line = ast_skip_blanks(line);
+                       if (ast_strlen_zero(line)) {
+                               continue;
+                       }
+                       match = match_eventdata(filter_entry, line);
+                       if (match) {
+                               ast_free(line_buffer_start);
+                               line_buffer_start = NULL;
+                               break;
+                       }
+               }
+       }
+
+       ast_free(line_buffer_start);
+
+done:
+
+       *result = match;
+       return match ? CMP_MATCH | CMP_STOP : 0;
 }
 
-static int blackfilter_cmp_fn(void *obj, void *arg, void *data, int flags)
+static int should_send_event(struct ao2_container *includefilters,
+       struct ao2_container *excludefilters, struct eventqent *eqe)
 {
-       regex_t *regex_filter = obj;
-       const char *eventdata = arg;
-       int *result = data;
+       int result = 0;
 
-       if (!regexec(regex_filter, eventdata, 0, NULL, 0)) {
-               *result = 0;
-               return (CMP_MATCH | CMP_STOP);
+       if (manager_debug) {
+               ast_verbose("<-- Examining AMI event (%u): -->\n%s\n", eqe->event_name_hash, eqe->eventdata);
+       } else {
+               ast_debug(4, "Examining AMI event (%u):\n%s\n", eqe->event_name_hash, eqe->eventdata);
+       }
+       if (!ao2_container_count(includefilters) && !ao2_container_count(excludefilters)) {
+               return 1; /* no filtering means match all */
+       } else if (ao2_container_count(includefilters) && !ao2_container_count(excludefilters)) {
+               /* include filters only: implied exclude all filter processed first, then include filters */
+               ao2_t_callback_data(includefilters, OBJ_NODATA, filter_cmp_fn, eqe, &result, "find filter in includefilters container");
+               return result;
+       } else if (!ao2_container_count(includefilters) && ao2_container_count(excludefilters)) {
+               /* exclude filters only: implied include all filter processed first, then exclude filters */
+               ao2_t_callback_data(excludefilters, OBJ_NODATA, filter_cmp_fn, eqe, &result, "find filter in excludefilters container");
+               return !result;
+       } else {
+               /* include and exclude filters: implied exclude all filter processed first, then include filters, and lastly exclude filters */
+               ao2_t_callback_data(includefilters, OBJ_NODATA, filter_cmp_fn, eqe, &result, "find filter in session filter container");
+               if (result) {
+                       result = 0;
+                       ao2_t_callback_data(excludefilters, OBJ_NODATA, filter_cmp_fn, eqe, &result, "find filter in session filter container");
+                       return !result;
+               }
        }
 
-       *result = 1;
-       return 0;
+       return result;
 }
 
 /*!
@@ -5375,24 +5501,44 @@ static int blackfilter_cmp_fn(void *obj, void *arg, void *data, int flags)
  */
 static int action_filter(struct mansession *s, const struct message *m)
 {
+       const char *match_criteria = astman_get_header(m, "MatchCriteria");
        const char *filter = astman_get_header(m, "Filter");
        const char *operation = astman_get_header(m, "Operation");
        int res;
 
        if (!strcasecmp(operation, "Add")) {
-               res = manager_add_filter(filter, s->session->whitefilters, s->session->blackfilters);
+               char *criteria;
+               int have_match = !ast_strlen_zero(match_criteria);
 
-               if (res != FILTER_SUCCESS) {
-                       if (res == FILTER_ALLOC_FAILED) {
+               /* Create an eventfilter expression.
+                * eventfilter[(match_criteria)]
+                */
+               res = ast_asprintf(&criteria, "eventfilter%s%s%s",
+                       S_COR(have_match, "(", ""), S_OR(match_criteria, ""),
+                       S_COR(have_match, ")", ""));
+               if (res <= 0) {
+                       astman_send_error(s, m, "Internal Error. Failed to allocate storage for filter type");
+                       return 0;
+               }
+
+               res = manager_add_filter(criteria, filter, s->session->includefilters, s->session->excludefilters);
+               ast_std_free(criteria);
+               if (res != FILTER_SUCCESS) {
+                       if (res == FILTER_ALLOC_FAILED) {
                                astman_send_error(s, m, "Internal Error. Failed to allocate regex for filter");
-                               return 0;
-                       } else if (res == FILTER_COMPILE_FAIL) {
-                               astman_send_error(s, m, "Filter did not compile.  Check the syntax of the filter given.");
-                               return 0;
-                       } else {
+                               return 0;
+                       } else if (res == FILTER_COMPILE_FAIL) {
+                               astman_send_error(s, m,
+                                       "Filter did not compile.  Check the syntax of the filter given.");
+                               return 0;
+                       } else if (res == FILTER_FORMAT_ERROR) {
+                               astman_send_error(s, m,
+                                       "Filter was formatted incorrectly.  Check the syntax of the filter given.");
+                               return 0;
+                       } else {
                                astman_send_error(s, m, "Internal Error. Failed adding filter.");
-                               return 0;
-                       }
+                               return 0;
+                       }
                }
 
                astman_send_ack(s, m, "Success");
@@ -5406,84 +5552,675 @@ static int action_filter(struct mansession *s, const struct message *m)
 /*!
  * \brief Add an event filter to a manager session
  *
- * \param filter_pattern  Filter syntax to add, see below for syntax
- * \param whitefilters, blackfilters
+ * \param criteria See examples in manager.conf.sample
+ * \param filter_pattern  Filter pattern
+ * \param includefilters, excludefilters
  *
  * \return FILTER_ALLOC_FAILED   Memory allocation failure
  * \return FILTER_COMPILE_FAIL   If the filter did not compile
+ * \return FILTER_FORMAT_ERROR   If the criteria weren't formatted correctly
  * \return FILTER_SUCCESS        Success
  *
- * Filter will be used to match against each line of a manager event
- * Filter can be any valid regular expression
- * Filter can be a valid regular expression prefixed with !, which will add the filter as a black filter
  *
  * Examples:
- * \code
- *   filter_pattern = "Event: Newchannel"
- *   filter_pattern = "Event: New.*"
- *   filter_pattern = "!Channel: DAHDI.*"
- * \endcode
+ * See examples in manager.conf.sample
  *
  */
-static enum add_filter_result manager_add_filter(const char *filter_pattern, struct ao2_container *whitefilters, struct ao2_container *blackfilters) {
-       regex_t *new_filter = ao2_t_alloc(sizeof(*new_filter), event_filter_destructor, "event_filter allocation");
-       int is_blackfilter;
+static enum add_filter_result manager_add_filter(
+       const char *criteria, const char *filter_pattern,
+       struct ao2_container *includefilters, struct ao2_container *excludefilters)
+{
+       RAII_VAR(struct event_filter_entry *, filter_entry,
+               ao2_t_alloc(sizeof(*filter_entry), event_filter_destructor, "event_filter allocation"),
+               ao2_cleanup);
+       char *options_start = NULL;
+       SCOPE_ENTER(3, "manager_add_filter(%s, %s, %p, %p)", criteria, filter_pattern, includefilters, excludefilters);
+
+       if (!filter_entry) {
+               SCOPE_EXIT_LOG_RTN_VALUE(FILTER_ALLOC_FAILED, LOG_WARNING, "Unable to allocate filter_entry");
+       }
 
-       if (!new_filter) {
-               return FILTER_ALLOC_FAILED;
+       /*
+        * At a minimum, criteria must be "eventfilter" but may contain additional
+        * constraints.
+        */
+       if (ast_strlen_zero(criteria)) {
+               SCOPE_EXIT_LOG_RTN_VALUE(FILTER_FORMAT_ERROR, LOG_WARNING, "Missing criteria");
+       }
+
+       /*
+        * filter_pattern could be empty but it should never be NULL.
+        */
+       if (!filter_pattern) {
+               SCOPE_EXIT_LOG_RTN_VALUE(FILTER_FORMAT_ERROR, LOG_WARNING, "Filter pattern was NULL");
        }
 
+       /*
+        * For a legacy filter, if the first character of filter_pattern is
+        * '!' then it's an exclude filter.  It's also accepted as an alternative
+        * to specifying "action(exclude)" for an advanced filter.  If
+        * "action" is specified however, it will take precedence.
+        */
        if (filter_pattern[0] == '!') {
-               is_blackfilter = 1;
+               filter_entry->is_excludefilter = 1;
                filter_pattern++;
-       } else {
-               is_blackfilter = 0;
        }
 
-       if (regcomp(new_filter, filter_pattern, REG_EXTENDED | REG_NOSUB)) {
-               ao2_t_ref(new_filter, -1, "failed to make regex");
-               return FILTER_COMPILE_FAIL;
+       /*
+        * This is the default
+        */
+       filter_entry->match_type = FILTER_MATCH_REGEX;
+
+       /*
+        * If the criteria has a '(' in it, then it's an advanced filter.
+        */
+       options_start = strstr(criteria, "(");
+
+       /*
+        * If it's a legacy filter, there MUST be a filter pattern.
+        */
+       if (!options_start && ast_strlen_zero(filter_pattern)) {
+               SCOPE_EXIT_LOG_RTN_VALUE(FILTER_FORMAT_ERROR, LOG_WARNING,
+                       "'%s = %s': Legacy filter with no filter pattern specified\n",
+                       criteria, filter_pattern);
        }
 
-       if (is_blackfilter) {
-               ao2_t_link(blackfilters, new_filter, "link new filter into black user container");
+       if (options_start) {
+               /*
+                * This is an advanced filter
+                */
+               char *temp = ast_strdupa(options_start + 1); /* skip over the leading '(' */
+               char *saveptr = NULL;
+               char *option = NULL;
+               enum found_options {
+                       action_found = (1 << 0),
+                       name_found = (1 << 1),
+                       header_found = (1 << 2),
+                       method_found = (1 << 3),
+               };
+               enum found_options options_found = 0;
+
+               filter_entry->match_type = FILTER_MATCH_NONE;
+
+               ast_strip(temp);
+               if (ast_strlen_zero(temp) || !ast_ends_with(temp, ")")) {
+                       SCOPE_EXIT_LOG_RTN_VALUE(FILTER_FORMAT_ERROR, LOG_WARNING,
+                               "'%s = %s': Filter options not formatted correctly\n",
+                               criteria, filter_pattern);
+               }
+
+               /*
+                * These can actually be in any order...
+                * action(include|exclude),name(<event_name>),header(<header_name>),method(<match_method>)
+                * At least one of action, name, or header is required.
+                */
+               while ((option = strtok_r(temp, " ,)", &saveptr))) {
+                       if (!strncmp(option, "action", 6)) {
+                               char *method = strstr(option, "(");
+                               if (ast_strlen_zero(method)) {
+                                       SCOPE_EXIT_LOG_RTN_VALUE(FILTER_FORMAT_ERROR, LOG_WARNING, "'%s = %s': 'action' parameter not formatted correctly\n",
+                                               criteria, filter_pattern);
+                               }
+                               method++;
+                               ast_strip(method);
+                               if (!strcmp(method, "include")) {
+                                       filter_entry->is_excludefilter = 0;
+                               } else if (!strcmp(method, "exclude")) {
+                                       filter_entry->is_excludefilter = 1;
+                               } else {
+                                       SCOPE_EXIT_LOG_RTN_VALUE(FILTER_FORMAT_ERROR, LOG_WARNING, "'%s = %s': 'action' option '%s' is unknown\n",
+                                               criteria, filter_pattern, method);
+                               }
+                               options_found |= action_found;
+                       } else if (!strncmp(option, "name", 4)) {
+                               char *event_name = strstr(option, "(");
+                               event_name++;
+                               ast_strip(event_name);
+                               if (ast_strlen_zero(event_name)) {
+                                       SCOPE_EXIT_LOG_RTN_VALUE(FILTER_FORMAT_ERROR, LOG_WARNING, "'%s = %s': 'name' parameter not formatted correctly\n",
+                                               criteria, filter_pattern);
+                               }
+                               filter_entry->event_name = ast_strdup(event_name);
+                               filter_entry->event_name_hash = ast_str_hash(event_name);
+                               options_found |= name_found;
+                       } else if (!strncmp(option, "header", 6)) {
+                               char *header_name = strstr(option, "(");
+                               header_name++;
+                               ast_strip(header_name);
+                               if (ast_strlen_zero(header_name)) {
+                                       SCOPE_EXIT_LOG_RTN_VALUE(FILTER_FORMAT_ERROR, LOG_WARNING, "'%s = %s': 'header' parameter not formatted correctly\n",
+                                               criteria, filter_pattern);
+                               }
+                               if (!ast_ends_with(header_name, ":")) {
+                                       filter_entry->header_name = ast_malloc(strlen(header_name) + 2);
+                                       if (!filter_entry->header_name) {
+                                               SCOPE_EXIT_LOG_RTN_VALUE(FILTER_ALLOC_FAILED, LOG_ERROR, "Unable to allocate memory for header_name");
+                                       }
+                                       sprintf(filter_entry->header_name, "%s:", header_name); /* Safe */
+                               } else {
+                                       filter_entry->header_name = ast_strdup(header_name);
+                               }
+                               options_found |= header_found;
+                       } else if (!strncmp(option, "method", 6)) {
+                               char *method = strstr(option, "(");
+                               method++;
+                               ast_strip(method);
+                               if (ast_strlen_zero(method)) {
+                                       SCOPE_EXIT_LOG_RTN_VALUE(FILTER_FORMAT_ERROR, LOG_WARNING, "'%s = %s': 'method' parameter not formatted correctly\n",
+                                               criteria, filter_pattern);
+                               }
+                               if (!strcmp(method, "regex")) {
+                                       filter_entry->match_type = FILTER_MATCH_REGEX;
+                               } else if (!strcmp(method, "exact")) {
+                                       filter_entry->match_type = FILTER_MATCH_EXACT;
+                               } else if (!strcmp(method, "starts_with")) {
+                                       filter_entry->match_type = FILTER_MATCH_STARTS_WITH;
+                               } else if (!strcmp(method, "ends_with")) {
+                                       filter_entry->match_type = FILTER_MATCH_ENDS_WITH;
+                               } else if (!strcmp(method, "contains")) {
+                                       filter_entry->match_type = FILTER_MATCH_CONTAINS;
+                               } else if (!strcmp(method, "none")) {
+                                       filter_entry->match_type = FILTER_MATCH_NONE;
+                               } else {
+                                       SCOPE_EXIT_LOG_RTN_VALUE(FILTER_FORMAT_ERROR, LOG_WARNING, "'%s = %s': 'method' option '%s' is unknown\n",
+                                               criteria, filter_pattern, method);
+                               }
+                               options_found |= method_found;
+                       } else {
+                               SCOPE_EXIT_LOG_RTN_VALUE(FILTER_FORMAT_ERROR, LOG_WARNING, "'%s = %s': Filter option '%s' is unknown\n",
+                                       criteria, filter_pattern, option);
+                       }
+                       temp = NULL;
+               }
+               if (!options_found) {
+                       SCOPE_EXIT_LOG_RTN_VALUE(FILTER_FORMAT_ERROR, LOG_WARNING,
+                               "'%s = %s': No action, name, header, or method option found\n",
+                               criteria, filter_pattern);
+               }
+               if (ast_strlen_zero(filter_pattern) && filter_entry->match_type != FILTER_MATCH_NONE) {
+                       SCOPE_EXIT_LOG_RTN_VALUE(FILTER_FORMAT_ERROR, LOG_WARNING,
+                               "'%s = %s': method can't be '%s' with no filter pattern\n",
+                               criteria, filter_pattern, match_type_names[filter_entry->match_type]);
+               }
+               if (!ast_strlen_zero(filter_pattern) && filter_entry->match_type == FILTER_MATCH_NONE) {
+                       SCOPE_EXIT_LOG_RTN_VALUE(FILTER_FORMAT_ERROR, LOG_WARNING,
+                               "'%s = %s': method can't be 'none' with a filter pattern\n",
+                               criteria, filter_pattern);
+               }
+               if (!(options_found & name_found) && !(options_found & header_found) &&
+                       filter_entry->match_type == FILTER_MATCH_NONE) {
+                       SCOPE_EXIT_LOG_RTN_VALUE(FILTER_FORMAT_ERROR, LOG_WARNING,
+                               "'%s = %s': No name or header option found and no filter pattern\n",
+                               criteria, filter_pattern);
+               }
+       }
+
+       if (!ast_strlen_zero(filter_pattern)) {
+               if (filter_entry->match_type == FILTER_MATCH_REGEX) {
+                       filter_entry->regex_filter = ast_calloc(1, sizeof(regex_t));
+                       if (!filter_entry->regex_filter) {
+                               SCOPE_EXIT_LOG_RTN_VALUE(FILTER_ALLOC_FAILED, LOG_ERROR, "Unable to allocate memory for regex_filter");
+                       }
+                       if (regcomp(filter_entry->regex_filter, filter_pattern, REG_EXTENDED | REG_NOSUB)) {
+                               SCOPE_EXIT_LOG_RTN_VALUE(FILTER_COMPILE_FAIL, LOG_WARNING, "Unable to compile regex filter for '%s'", filter_pattern);
+                       }
+               } else {
+                       filter_entry->string_filter = ast_strdup(filter_pattern);
+               }
+       }
+
+       ast_debug(2, "Event filter:\n"
+               "conf entry: %s = %s\n"
+               "event_name: %s (hash: %d)\n"
+               "test_header:  %s\n"
+               "match_type: %s\n"
+               "regex_filter: %p\n"
+               "string filter: %s\n"
+               "is excludefilter: %d\n",
+               criteria, filter_pattern,
+               S_OR(filter_entry->event_name, "<not used>"),
+               filter_entry->event_name_hash,
+               S_OR(filter_entry->header_name, "<not used>"),
+               match_type_names[filter_entry->match_type],
+               filter_entry->regex_filter,
+               filter_entry->string_filter,
+               filter_entry->is_excludefilter);
+
+       if (filter_entry->is_excludefilter) {
+               ao2_t_link(excludefilters, filter_entry, "link new filter into exclude user container");
        } else {
-               ao2_t_link(whitefilters, new_filter, "link new filter into white user container");
+               ao2_t_link(includefilters, filter_entry, "link new filter into include user container");
+       }
+
+       SCOPE_EXIT_RTN_VALUE(FILTER_SUCCESS, "Filter added successfully");
+}
+
+#ifdef TEST_FRAMEWORK
+
+struct test_filter_data {
+       const char *criteria;
+       const char *filter;
+       enum add_filter_result expected_add_filter_result;
+       struct event_filter_entry expected_filter_entry;
+       const char *test_event_name;
+       const char *test_event_payload;
+       int expected_should_send_event;
+};
+
+static char *add_filter_result_enums[] = {
+       [FILTER_SUCCESS] = "FILTER_SUCCESS",
+       [FILTER_ALLOC_FAILED] = "FILTER_ALLOC_FAILED",
+       [FILTER_COMPILE_FAIL] = "FILTER_COMPILE_FAIL",
+       [FILTER_FORMAT_ERROR] = "FILTER_FORMAT_ERROR",
+};
+
+#define TEST_EVENT_NEWCHANNEL "Newchannel", "Event: Newchannel\r\nChannel: XXX\r\nSomeheader: YYY\r\n"
+#define TEST_EVENT_VARSET "VarSet", "Event: VarSet\r\nChannel: ABC\r\nSomeheader: XXX\r\n"
+#define TEST_EVENT_NONE "", ""
+
+static struct test_filter_data parsing_filter_tests[] = {
+       /* Valid filters */
+       { "eventfilter", "XXX", FILTER_SUCCESS,
+               { FILTER_MATCH_REGEX, NULL, NULL, NULL, 0, NULL, 0}, TEST_EVENT_NEWCHANNEL, 1},
+       { "eventfilter", "!XXX", FILTER_SUCCESS,
+               { FILTER_MATCH_REGEX, NULL, NULL, NULL, 0, NULL, 1},  TEST_EVENT_VARSET, 0},
+       { "eventfilter(name(VarSet),method(none))", "", FILTER_SUCCESS,
+               { FILTER_MATCH_NONE, NULL, NULL, "VarSet", 0, NULL, 0}, TEST_EVENT_VARSET, 1},
+       { "eventfilter(name(Newchannel),method(regex))", "X[XYZ]X", FILTER_SUCCESS,
+               { FILTER_MATCH_REGEX, NULL, NULL, "Newchannel", 0, NULL, 0}, TEST_EVENT_NEWCHANNEL, 1},
+       { "eventfilter(name(Newchannel),method(regex))", "X[abc]X", FILTER_SUCCESS,
+               { FILTER_MATCH_REGEX, NULL, NULL, "Newchannel", 0, NULL, 0}, TEST_EVENT_NEWCHANNEL, 0},
+       { "eventfilter(action(exclude),name(Newchannel),method(regex))", "X[XYZ]X", FILTER_SUCCESS,
+               { FILTER_MATCH_REGEX, NULL, NULL, "Newchannel", 0, NULL, 1}, TEST_EVENT_NEWCHANNEL, 0},
+       { "eventfilter(action(exclude),name(Newchannel),method(regex))", "X[abc]X", FILTER_SUCCESS,
+               { FILTER_MATCH_REGEX, NULL, NULL, "Newchannel", 0, NULL, 1}, TEST_EVENT_NEWCHANNEL, 1},
+       { "eventfilter(action(include),name(VarSet),header(Channel),method(starts_with))", "AB", FILTER_SUCCESS,
+               { FILTER_MATCH_STARTS_WITH, NULL, NULL, "VarSet", 0, "Channel:", 0}, TEST_EVENT_VARSET, 1},
+       { "eventfilter(action(include),name(VarSet),header(Channel),method(ends_with))", "BC", FILTER_SUCCESS,
+               { FILTER_MATCH_ENDS_WITH, NULL, NULL, "VarSet", 0, "Channel:", 0}, TEST_EVENT_VARSET, 1},
+       { "eventfilter(action(include),name(VarSet),header(Channel),method(exact))", "ABC", FILTER_SUCCESS,
+               { FILTER_MATCH_EXACT, NULL, NULL, "VarSet", 0, "Channel:", 0}, TEST_EVENT_VARSET, 1},
+       { "eventfilter(action(include),name(VarSet),header(Channel),method(exact))", "XXX", FILTER_SUCCESS,
+               { FILTER_MATCH_EXACT, NULL, NULL, "VarSet", 0, "Channel:", 0}, TEST_EVENT_VARSET, 0},
+       { "eventfilter(name(VarSet),header(Channel),method(exact))", "!ZZZ", FILTER_SUCCESS,
+               { FILTER_MATCH_EXACT, NULL, NULL, "VarSet", 0, "Channel:", 1}, TEST_EVENT_VARSET, 1},
+       { "eventfilter(action(exclude),name(VarSet),header(Channel),method(exact))", "ZZZ", FILTER_SUCCESS,
+               { FILTER_MATCH_EXACT, NULL, NULL, "VarSet", 0, "Channel:", 1}, TEST_EVENT_VARSET, 1},
+       { "eventfilter(action(include),name(VarSet),header(Someheader),method(exact))", "!XXX", FILTER_SUCCESS,
+               { FILTER_MATCH_EXACT, NULL, NULL, "VarSet", 0, "Someheader:", 0}, TEST_EVENT_VARSET, 1},
+
+       /* Invalid filters */
+       { "eventfilter(action(include)", "", FILTER_FORMAT_ERROR, { 0, }, TEST_EVENT_NONE, 0},
+       { "eventfilter(action(inlude)", "", FILTER_FORMAT_ERROR, { 0, }, TEST_EVENT_NONE, 0},
+       { "eventfilter(nnnn(yyy)", "XXX", FILTER_FORMAT_ERROR, { 0, }, TEST_EVENT_NONE, 0},
+       { "eventfilter(eader(VarSet)", "XXX", FILTER_FORMAT_ERROR, { 0, }, TEST_EVENT_NONE, 0},
+       { "eventfilter(ethod(contains)", "XXX", FILTER_FORMAT_ERROR, { 0, }, TEST_EVENT_NONE, 0},
+       { "eventfilter(nnnn(yyy),header(VarSet),method(contains)", "XXX", FILTER_FORMAT_ERROR, { 0, }, TEST_EVENT_NONE, 0},
+       { "eventfilter(name(yyy),heder(VarSet),method(contains)", "XXX", FILTER_FORMAT_ERROR, { 0, }, TEST_EVENT_NONE, 0},
+       { "eventfilter(name(yyy),header(VarSet),mehod(contains)", "XXX", FILTER_FORMAT_ERROR, { 0, }, TEST_EVENT_NONE, 0},
+       { "eventfilter(name(yyy),header(VarSet),method(coains)", "XXX", FILTER_FORMAT_ERROR, { 0, }, TEST_EVENT_NONE, 0},
+       { "eventfilter(method(yyy))", "XXX", FILTER_FORMAT_ERROR, { 0, }, TEST_EVENT_NONE, 0},
+       { "eventfilter", "", FILTER_FORMAT_ERROR, { 0, }, TEST_EVENT_NONE, 0},
+       { "eventfilter", "!", FILTER_FORMAT_ERROR, { 0, }, TEST_EVENT_NONE, 0},
+       { "eventfilter()", "XXX", FILTER_FORMAT_ERROR, { 0, }, TEST_EVENT_NONE, 0},
+       { "eventfilter", "XX[X", FILTER_COMPILE_FAIL, { 0, }, TEST_EVENT_NONE, 0},
+       { "eventfilter(method(regex))", "XX[X", FILTER_COMPILE_FAIL, { 0, }, TEST_EVENT_NONE, 0},
+};
+
+/*
+ * This is a bit different than ast_strings_equal in that
+ * it will return 1 if both strings are NULL.
+ */
+static int strings_equal(const char *str1, const char *str2)
+{
+       if ((!str1 && str2) || (str1 && !str2)) {
+               return 0;
+       }
+
+       return str1 == str2 || !strcmp(str1, str2);
+}
+
+AST_TEST_DEFINE(eventfilter_test_creation)
+{
+       enum ast_test_result_state res = AST_TEST_PASS;
+       RAII_VAR(struct ao2_container *, includefilters, NULL, ao2_cleanup);
+       RAII_VAR(struct ao2_container *, excludefilters, NULL, ao2_cleanup);
+       int i = 0;
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "eventfilter_test_creation";
+               info->category = "/main/manager/";
+               info->summary = "Test eventfilter creation";
+               info->description =
+                       "This creates various eventfilters and tests to make sure they were created successfully.";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       includefilters = ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_MUTEX, 0, NULL, NULL);
+       excludefilters = ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_MUTEX, 0, NULL, NULL);
+       if (!includefilters || !excludefilters) {
+               ast_test_status_update(test, "Failed to allocate filter containers.\n");
+               return AST_TEST_FAIL;
        }
 
-       ao2_ref(new_filter, -1);
+       for (i = 0; i < ARRAY_LEN(parsing_filter_tests); i++) {
+               struct event_filter_entry *filter_entry;
+               enum add_filter_result add_filter_res;
+               int send_event = 0;
+               struct eventqent *eqe = NULL;
+               int include_container_count = 0;
+               int exclude_container_count = 0;
+
+               /* We need to clear the containers before each test */
+               ao2_callback(includefilters, OBJ_UNLINK | OBJ_NODATA, NULL, NULL);
+               ao2_callback(excludefilters, OBJ_UNLINK | OBJ_NODATA, NULL, NULL);
+
+               add_filter_res = manager_add_filter(parsing_filter_tests[i].criteria, parsing_filter_tests[i].filter,
+                       includefilters, excludefilters);
+
+               /* If you're adding a new test, enable this to see the full results */
+#if 0
+               ast_test_debug(test, "Add filter result '%s = %s': Expected: %s  Actual: %s  %s\n",
+                       parsing_filter_tests[i].criteria, parsing_filter_tests[i].filter,
+                       add_filter_result_enums[parsing_filter_tests[i].expected_add_filter_result],
+                       add_filter_result_enums[add_filter_res],
+                       add_filter_res != parsing_filter_tests[i].expected_add_filter_result ? "FAIL" : "PASS");
+#endif
+
+               if (add_filter_res != parsing_filter_tests[i].expected_add_filter_result) {
+                       ast_test_status_update(test,
+                               "Unexpected add filter result '%s = %s'. Expected result: %s Actual result: %s\n",
+                               parsing_filter_tests[i].criteria, parsing_filter_tests[i].filter,
+                               add_filter_result_enums[parsing_filter_tests[i].expected_add_filter_result],
+                               add_filter_result_enums[add_filter_res]);
+                       res = AST_TEST_FAIL;
+                       continue;
+               }
+
+               if (parsing_filter_tests[i].expected_add_filter_result != FILTER_SUCCESS) {
+                       /*
+                        * We don't need to test filters that we know aren't going
+                        * to be parsed successfully.
+                        */
+                       continue;
+               }
+
+               /* We need to set the event name hash on the test data */
+               if (parsing_filter_tests[i].expected_filter_entry.event_name) {
+                       parsing_filter_tests[i].expected_filter_entry.event_name_hash =
+                               ast_str_hash(parsing_filter_tests[i].expected_filter_entry.event_name);
+               }
+
+               include_container_count = ao2_container_count(includefilters);
+               exclude_container_count = ao2_container_count(excludefilters);
+
+               if (parsing_filter_tests[i].expected_filter_entry.is_excludefilter) {
+                       if (exclude_container_count != 1 || include_container_count != 0) {
+                               ast_test_status_update(test,
+                                       "Invalid container counts for exclude filter '%s = %s'. Exclude: %d Include: %d.  Should be 1 and 0\n",
+                                       parsing_filter_tests[i].criteria, parsing_filter_tests[i].filter,
+                                       exclude_container_count, include_container_count);
+                               res = AST_TEST_FAIL;
+                               continue;
+                       }
+                       /* There can only be one entry in the container so ao2_find is fine */
+                       filter_entry = ao2_find(excludefilters, NULL, OBJ_SEARCH_OBJECT);
+               } else {
+                       if (include_container_count != 1 || exclude_container_count != 0) {
+                               ast_test_status_update(test,
+                                       "Invalid container counts for include filter '%s = %s'. Include: %d Exclude: %d.  Should be 1 and 0\n",
+                                       parsing_filter_tests[i].criteria, parsing_filter_tests[i].filter,
+                                       include_container_count, exclude_container_count);
+                               res = AST_TEST_FAIL;
+                               continue;
+                       }
+                       /* There can only be one entry in the container so ao2_find is fine */
+                       filter_entry = ao2_find(includefilters, NULL, OBJ_SEARCH_OBJECT);
+               }
+
+               if (!filter_entry) {
+                       ast_test_status_update(test,
+                               "Failed to find filter entry for '%s = %s' in %s filter container\n",
+                               parsing_filter_tests[i].criteria, parsing_filter_tests[i].filter,
+                               parsing_filter_tests[i].expected_filter_entry.is_excludefilter ? "exclude" : "include");
+                       res = AST_TEST_FAIL;
+                       goto loop_cleanup;
+               }
+
+               if (filter_entry->match_type != parsing_filter_tests[i].expected_filter_entry.match_type) {
+                       ast_test_status_update(test,
+                               "Failed to match filter type for '%s = %s'. Expected: %s Actual: %s\n",
+                               parsing_filter_tests[i].criteria, parsing_filter_tests[i].filter,
+                               match_type_names[parsing_filter_tests[i].expected_filter_entry.match_type],
+                               match_type_names[filter_entry->match_type]);
+                       res = AST_TEST_FAIL;
+                       goto loop_cleanup;
+               }
+
+               if (!strings_equal(filter_entry->event_name, parsing_filter_tests[i].expected_filter_entry.event_name)) {
+                       ast_test_status_update(test,
+                               "Failed to match event name for '%s = %s'. Expected: '%s' Actual: '%s'\n",
+                               parsing_filter_tests[i].criteria, parsing_filter_tests[i].filter,
+                               parsing_filter_tests[i].expected_filter_entry.event_name, filter_entry->event_name);
+                       res = AST_TEST_FAIL;
+                       goto loop_cleanup;
+               }
+
+               if (filter_entry->event_name_hash != parsing_filter_tests[i].expected_filter_entry.event_name_hash) {
+                       ast_test_status_update(test,
+                               "Event name hashes failed to match for '%s = %s'. Expected: %u Actual: %u\n",
+                               parsing_filter_tests[i].criteria, parsing_filter_tests[i].filter,
+                               parsing_filter_tests[i].expected_filter_entry.event_name_hash, filter_entry->event_name_hash);
+                       res = AST_TEST_FAIL;
+                       goto loop_cleanup;
+               }
+
+               if (!strings_equal(filter_entry->header_name, parsing_filter_tests[i].expected_filter_entry.header_name)) {
+                       ast_test_status_update(test,
+                               "Failed to match header name for '%s = %s'. Expected: '%s' Actual: '%s'\n",
+                               parsing_filter_tests[i].criteria, parsing_filter_tests[i].filter,
+                               parsing_filter_tests[i].expected_filter_entry.header_name, filter_entry->header_name);
+                       res = AST_TEST_FAIL;
+                       goto loop_cleanup;
+               }
+
+               switch (parsing_filter_tests[i].expected_filter_entry.match_type) {
+               case FILTER_MATCH_REGEX:
+                       if (!filter_entry->regex_filter) {
+                               ast_test_status_update(test,
+                                       "Failed to compile regex filter for '%s = %s'\n",
+                                       parsing_filter_tests[i].criteria, parsing_filter_tests[i].filter);
+                               res = AST_TEST_FAIL;
+                               goto loop_cleanup;
+                       }
+                       break;
+               case FILTER_MATCH_NONE:
+                       if (filter_entry->regex_filter || !ast_strlen_zero(filter_entry->string_filter)) {
+                               ast_test_status_update(test,
+                                       "Unexpected regex filter or string for '%s = %s' with match_type 'none'\n",
+                                       parsing_filter_tests[i].criteria, parsing_filter_tests[i].filter);
+                               res = AST_TEST_FAIL;
+                               goto loop_cleanup;
+                       }
+                       break;
+               case FILTER_MATCH_STARTS_WITH:
+               case FILTER_MATCH_ENDS_WITH:
+               case FILTER_MATCH_CONTAINS:
+               case FILTER_MATCH_EXACT:
+                       if (filter_entry->regex_filter || ast_strlen_zero(filter_entry->string_filter)) {
+                               ast_test_status_update(test,
+                                       "Unexpected regex filter or empty string for '%s = %s' with match_type '%s'\n",
+                                       parsing_filter_tests[i].criteria, parsing_filter_tests[i].filter,
+                                       match_type_names[parsing_filter_tests[i].expected_filter_entry.match_type]);
+                               res = AST_TEST_FAIL;
+                               goto loop_cleanup;
+                       }
+                       break;
+               default:
+                       res = AST_TEST_FAIL;
+                       goto loop_cleanup;
+               }
 
-       return FILTER_SUCCESS;
+               /*
+                * This is a basic test of whether a single event matches a single filter.
+                */
+               eqe = ast_calloc(1, sizeof(*eqe) + strlen(parsing_filter_tests[i].test_event_payload) + 1);
+               if (!eqe) {
+                       ast_test_status_update(test, "Failed to allocate eventqent\n");
+                       res = AST_TEST_FAIL;
+                       ao2_ref(filter_entry, -1);
+                       break;
+               }
+               strcpy(eqe->eventdata, parsing_filter_tests[i].test_event_payload); /* Safe */
+               eqe->event_name_hash = ast_str_hash(parsing_filter_tests[i].test_event_name);
+               send_event = should_send_event(includefilters, excludefilters, eqe);
+               if (send_event != parsing_filter_tests[i].expected_should_send_event) {
+                       char *escaped = ast_escape_c_alloc(parsing_filter_tests[i].test_event_payload);
+                       ast_test_status_update(test,
+                               "Should send event failed to match for '%s = %s' payload '%s'. Expected: %s Actual: %s\n",
+                               parsing_filter_tests[i].criteria, parsing_filter_tests[i].filter, escaped,
+                               AST_YESNO(parsing_filter_tests[i].expected_should_send_event), AST_YESNO(send_event));
+                       ast_free(escaped);
+                       res = AST_TEST_FAIL;
+               }
+loop_cleanup:
+               ast_free(eqe);
+               ao2_cleanup(filter_entry);
+
+       }
+       ast_test_status_update(test, "Tested %d filters\n", i);
+
+       return res;
 }
 
-static int match_filter(struct mansession *s, char *eventdata)
+struct test_filter_matching {
+       const char *criteria;
+       const char *pattern;
+};
+
+/*
+ * These filters are used to test the precedence of include and exclude
+ * filters.  When there are both include and exclude filters, the include
+ * filters are matched first.  If the event doesn't match an include filter,
+ * it's discarded.  If it does match, the exclude filter list is searched and
+ * if a match is found, the event is discarded.
+ */
+
+/*
+ * The order of the filters in the array doesn't really matter.  The
+ * include and exclude filters are in separate containers and in each
+ * container, traversal stops when a match is found.
+ */
+static struct test_filter_matching filters_for_matching[] = {
+       { "eventfilter(name(VarSet),method(none))", ""},
+       { "eventfilter(name(Newchannel),method(regex))", "X[XYZ]X"},
+       { "eventfilter(name(Newchannel),method(regex))", "X[abc]X"},
+       { "eventfilter(name(Newchannel),header(Someheader),method(regex))", "ZZZ"},
+       { "eventfilter(action(exclude),name(Newchannel),method(regex))", "X[a]X"},
+       { "eventfilter(action(exclude),name(Newchannel),method(regex))", "X[Z]X"},
+       { "eventfilter(action(exclude),name(VarSet),header(Channel),method(regex))", "YYY"},
+};
+
+struct test_event_matching{
+       const char *event_name;
+       const char *payload;
+       int expected_should_send_event;
+};
+
+static struct test_event_matching events_for_matching[] = {
+       { "Newchannel", "Event: Newchannel\r\nChannel: XXX\r\nSomeheader: YYY\r\n", 1 },
+       { "Newchannel", "Event: Newchannel\r\nChannel: XZX\r\nSomeheader: YYY\r\n", 0 },
+       { "Newchannel", "Event: Newchannel\r\nChannel: XaX\r\nSomeheader: YYY\r\n", 0 },
+       { "Newchannel", "Event: Newchannel\r\nChannel: XbX\r\nSomeheader: YYY\r\n", 1 },
+       { "Newchannel", "Event: Newchannel\r\nChannel: XcX\r\nSomeheader: YYY\r\n", 1 },
+       { "Newchannel", "Event: Newchannel\r\nChannel: YYY\r\nSomeheader: YYY\r\n", 0 },
+       { "Newchannel", "Event: Newchannel\r\nChannel: YYY\r\nSomeheader: ZZZ\r\n", 1 },
+       { "VarSet", "Event: VarSet\r\nChannel: XXX\r\nSomeheader: YYY\r\n", 1 },
+       { "VarSet", "Event: VarSet\r\nChannel: YYY\r\nSomeheader: YYY\r\n", 0 },
+};
+
+AST_TEST_DEFINE(eventfilter_test_matching)
 {
-       int result = 0;
+       enum ast_test_result_state res = AST_TEST_PASS;
+       RAII_VAR(struct ao2_container *, includefilters, NULL, ao2_cleanup);
+       RAII_VAR(struct ao2_container *, excludefilters, NULL, ao2_cleanup);
+       int i = 0;
 
-       if (manager_debug) {
-               ast_verbose("<-- Examining AMI event: -->\n%s\n", eventdata);
-       } else {
-               ast_debug(4, "Examining AMI event:\n%s\n", eventdata);
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "eventfilter_test_matching";
+               info->category = "/main/manager/";
+               info->summary = "Test eventfilter matching";
+               info->description =
+                       "This creates various eventfilters and tests to make sure they were matched successfully.";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
        }
-       if (!ao2_container_count(s->session->whitefilters) && !ao2_container_count(s->session->blackfilters)) {
-               return 1; /* no filtering means match all */
-       } else if (ao2_container_count(s->session->whitefilters) && !ao2_container_count(s->session->blackfilters)) {
-               /* white filters only: implied black all filter processed first, then white filters */
-               ao2_t_callback_data(s->session->whitefilters, OBJ_NODATA, whitefilter_cmp_fn, eventdata, &result, "find filter in session filter container");
-       } else if (!ao2_container_count(s->session->whitefilters) && ao2_container_count(s->session->blackfilters)) {
-               /* black filters only: implied white all filter processed first, then black filters */
-               ao2_t_callback_data(s->session->blackfilters, OBJ_NODATA, blackfilter_cmp_fn, eventdata, &result, "find filter in session filter container");
-       } else {
-               /* white and black filters: implied black all filter processed first, then white filters, and lastly black filters */
-               ao2_t_callback_data(s->session->whitefilters, OBJ_NODATA, whitefilter_cmp_fn, eventdata, &result, "find filter in session filter container");
-               if (result) {
-                       result = 0;
-                       ao2_t_callback_data(s->session->blackfilters, OBJ_NODATA, blackfilter_cmp_fn, eventdata, &result, "find filter in session filter container");
+
+       includefilters = ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_MUTEX, 0, NULL, NULL);
+       excludefilters = ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_MUTEX, 0, NULL, NULL);
+       if (!includefilters || !excludefilters) {
+               ast_test_status_update(test, "Failed to allocate filter containers.\n");
+               return AST_TEST_FAIL;
+       }
+
+       /* Load all the expected SUCCESS filters */
+       for (i = 0; i < ARRAY_LEN(filters_for_matching); i++) {
+               enum add_filter_result add_filter_res;
+
+               add_filter_res = manager_add_filter(filters_for_matching[i].criteria,
+                       filters_for_matching[i].pattern, includefilters, excludefilters);
+
+               if (add_filter_res != FILTER_SUCCESS) {
+                       ast_test_status_update(test,
+                               "Unexpected add filter result '%s = %s'. Expected result: %s Actual result: %s\n",
+                               parsing_filter_tests[i].criteria, parsing_filter_tests[i].filter,
+                               add_filter_result_enums[FILTER_SUCCESS],
+                               add_filter_result_enums[add_filter_res]);
+                       res = AST_TEST_FAIL;
+                       break;
                }
        }
+       ast_test_debug(test, "Loaded %d filters\n", i);
 
-       return result;
+       if (res != AST_TEST_PASS) {
+               return res;
+       }
+
+       /* Now test them */
+       for (i = 0; i < ARRAY_LEN(events_for_matching); i++) {
+               int send_event = 0;
+               struct eventqent *eqe = NULL;
+
+               eqe = ast_calloc(1, sizeof(*eqe) + strlen(events_for_matching[i].payload) + 1);
+               if (!eqe) {
+                       ast_test_status_update(test, "Failed to allocate eventqent\n");
+                       res = AST_TEST_FAIL;
+                       break;
+               }
+               strcpy(eqe->eventdata, events_for_matching[i].payload); /* Safe */
+               eqe->event_name_hash = ast_str_hash(events_for_matching[i].event_name);
+               send_event = should_send_event(includefilters, excludefilters, eqe);
+               if (send_event != events_for_matching[i].expected_should_send_event) {
+                       char *escaped = ast_escape_c_alloc(events_for_matching[i].payload);
+                       ast_test_status_update(test,
+                               "Should send event failed to match for '%s'. Expected: %s Actual: %s\n",
+                               escaped,
+                               AST_YESNO(events_for_matching[i].expected_should_send_event), AST_YESNO(send_event));
+                       ast_free(escaped);
+                       res = AST_TEST_FAIL;
+               }
+               ast_free(eqe);
+       }
+       ast_test_debug(test, "Tested %d events\n", i);
+
+       return res;
 }
+#endif
 
 /*!
  * Send any applicable events to the client listening on this socket.
@@ -5506,7 +6243,7 @@ static int process_events(struct mansession *s)
                        if (!ret && s->session->authenticated &&
                            (s->session->readperm & eqe->category) == eqe->category &&
                            (s->session->send_events & eqe->category) == eqe->category) {
-                                       if (match_filter(s, eqe->eventdata)) {
+                                       if (should_send_event(s->session->includefilters, s->session->excludefilters, eqe)) {
                                                if (send_string(s, eqe->eventdata) < 0 || s->write_error)
                                                        ret = -1;       /* don't send more */
                                        }
@@ -6523,7 +7260,7 @@ static int purge_sessions(int n_max)
  * events are appended to a queue from where they
  * can be dispatched to clients.
  */
-static int append_event(const char *str, int category)
+static int append_event(const char *str, int event_name_hash, int category)
 {
        struct eventqent *tmp = ast_malloc(sizeof(*tmp) + strlen(str));
        static int seq; /* sequence number */
@@ -6537,6 +7274,7 @@ static int append_event(const char *str, int category)
        tmp->category = category;
        tmp->seq = ast_atomic_fetchadd_int(&seq, 1);
        tmp->tv = ast_tvnow();
+       tmp->event_name_hash = event_name_hash;
        AST_RWLIST_NEXT(tmp, eq_next) = NULL;
        strcpy(tmp->eventdata, str);
 
@@ -6584,6 +7322,7 @@ static int __attribute__((format(printf, 9, 0))) __manager_event_sessions_va(
        struct timeval now;
        struct ast_str *buf;
        int i;
+       int event_name_hash;
 
        if (!ast_strlen_zero(manager_disabledevents)) {
                if (ast_in_delimited_string(event, manager_disabledevents, ',')) {
@@ -6635,7 +7374,9 @@ static int __attribute__((format(printf, 9, 0))) __manager_event_sessions_va(
 
        ast_str_append(&buf, 0, "\r\n");
 
-       append_event(ast_str_buffer(buf), category);
+       event_name_hash = ast_str_hash(event);
+
+       append_event(ast_str_buffer(buf), event_name_hash, category);
 
        /* Wake up any sleeping sessions */
        if (sessions) {
@@ -6690,8 +7431,8 @@ static int __attribute__((format(printf, 9, 0))) __manager_event_sessions(
        int res;
 
        va_start(ap, fmt);
-       res = __manager_event_sessions_va(sessions, category, event, chancount, chans,
-               file, line, func, fmt, ap);
+       res = __manager_event_sessions_va(sessions, category, event,
+               chancount, chans, file, line, func, fmt, ap);
        va_end(ap);
        return res;
 }
@@ -8437,11 +9178,11 @@ static void manager_free_user(struct ast_manager_user *user)
 {
        ast_free(user->a1_hash);
        ast_free(user->secret);
-       if (user->whitefilters) {
-               ao2_t_ref(user->whitefilters, -1, "decrement ref for white container, should be last one");
+       if (user->includefilters) {
+               ao2_t_ref(user->includefilters, -1, "decrement ref for include container, should be last one");
        }
-       if (user->blackfilters) {
-               ao2_t_ref(user->blackfilters, -1, "decrement ref for black container, should be last one");
+       if (user->excludefilters) {
+               ao2_t_ref(user->excludefilters, -1, "decrement ref for exclude container, should be last one");
        }
        user->acl = ast_free_acl_list(user->acl);
        ast_variables_destroy(user->chanvars);
@@ -8456,6 +9197,11 @@ static void manager_shutdown(void)
 {
        struct ast_manager_user *user;
 
+#ifdef TEST_FRAMEWORK
+       AST_TEST_UNREGISTER(eventfilter_test_creation);
+       AST_TEST_UNREGISTER(eventfilter_test_matching);
+#endif
+
        /* This event is not actually transmitted, but causes all TCP sessions to be closed */
        manager_event(EVENT_FLAG_SHUTDOWN, "CloseSession", "CloseSession: true\r\n");
 
@@ -8724,7 +9470,8 @@ static int __init_manager(int reload, int by_external_config)
                ast_extension_state_add(NULL, NULL, manager_state_cb, NULL);
 
                /* Append placeholder event so master_eventq never runs dry */
-               if (append_event("Event: Placeholder\r\n\r\n", 0)) {
+               if (append_event("Event: Placeholder\r\n\r\n",
+                       ast_str_hash("Placeholder"), 0)) {
                        return -1;
                }
 
@@ -8995,9 +9742,9 @@ static int __init_manager(int reload, int by_external_config)
                        /* Default allowmultiplelogin from [general] */
                        user->allowmultiplelogin = allowmultiplelogin;
                        user->writetimeout = 100;
-                       user->whitefilters = ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_MUTEX, 0, NULL, NULL);
-                       user->blackfilters = ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_MUTEX, 0, NULL, NULL);
-                       if (!user->whitefilters || !user->blackfilters) {
+                       user->includefilters = ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_MUTEX, 0, NULL, NULL);
+                       user->excludefilters = ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_MUTEX, 0, NULL, NULL);
+                       if (!user->includefilters || !user->excludefilters) {
                                manager_free_user(user);
                                break;
                        }
@@ -9005,8 +9752,8 @@ static int __init_manager(int reload, int by_external_config)
                        /* Insert into list */
                        AST_RWLIST_INSERT_TAIL(&users, user, list);
                } else {
-                       ao2_t_callback(user->whitefilters, OBJ_UNLINK | OBJ_NODATA | OBJ_MULTIPLE, NULL, NULL, "unlink all white filters");
-                       ao2_t_callback(user->blackfilters, OBJ_UNLINK | OBJ_NODATA | OBJ_MULTIPLE, NULL, NULL, "unlink all black filters");
+                       ao2_t_callback(user->includefilters, OBJ_UNLINK | OBJ_NODATA | OBJ_MULTIPLE, NULL, NULL, "unlink all include filters");
+                       ao2_t_callback(user->excludefilters, OBJ_UNLINK | OBJ_NODATA | OBJ_MULTIPLE, NULL, NULL, "unlink all exclude filters");
                }
 
                /* Make sure we keep this user and don't destroy it during cleanup */
@@ -9062,9 +9809,9 @@ static int __init_manager(int reload, int by_external_config)
                                                user->chanvars = tmpvar;
                                        }
                                }
-                       } else if (!strcasecmp(var->name, "eventfilter")) {
+                       } else if (ast_begins_with(var->name, "eventfilter")) {
                                const char *value = var->value;
-                               manager_add_filter(value, user->whitefilters, user->blackfilters);
+                               manager_add_filter(var->name, value, user->includefilters, user->excludefilters);
                        } else {
                                ast_debug(1, "%s is an unknown option.\n", var->name);
                        }
@@ -9157,14 +9904,19 @@ static int unload_module(void)
 
 static int load_module(void)
 {
+       int rc = 0;
        ast_register_cleanup(manager_shutdown);
-
-       return __init_manager(0, 0) ? AST_MODULE_LOAD_FAILURE : AST_MODULE_LOAD_SUCCESS;
+       rc = __init_manager(0, 0) ? AST_MODULE_LOAD_FAILURE : AST_MODULE_LOAD_SUCCESS;
+#ifdef TEST_FRAMEWORK
+       AST_TEST_REGISTER(eventfilter_test_creation);
+       AST_TEST_REGISTER(eventfilter_test_matching);
+#endif
+       return rc;
 }
 
 static int reload_module(void)
 {
-       return __init_manager(1, 0);
+       return __init_manager(1, 0) ? AST_MODULE_LOAD_FAILURE : AST_MODULE_LOAD_SUCCESS;
 }
 
 int astman_datastore_add(struct mansession *s, struct ast_datastore *datastore)
index 38e698d10eac1a4a32bfafca2a0b31abda29a071..07fd856d9ac5338b26219f014e617c2e81be3778 100644 (file)
                                        </enum>
                                </enumlist>
                        </parameter>
-                       <parameter name="FilterType">
-                               <para>FilterType can be one of the following:</para>
-                               <enumlist>
-                                       <enum name="regex">
-                                               <para>The Filter parameter contains a regular expression
-                                               which will be applied to the contents of the MatchAgainst
-                                               parameter.</para>
-                                       </enum>
-                                       <enum name="exact">
-                                               <para>The Filter parameter contains a string
-                                               which will be exactly matched to the contents of the MatchAgainst
-                                               parameter.</para>
-                                       </enum>
-                                       <enum name="partial">
-                                               <para>The Filter parameter contains a string
-                                               which will be searched for in the contents of the MatchAgainst
-                                               parameter.</para>
-                                       </enum>
-                               </enumlist>
-                               <para>The default is <literal>regex</literal></para>
-                       </parameter>
-                       <parameter name="MatchAgainst">
-                               <para>MatchAgainst can be one of the following:</para>
+                       <parameter name="MatchCriteria">
+                               <para>
+                               Advanced match criteria.  If not specified, the <literal>Filter</literal>
+                               parameter is assumed to be a regular expression and will be matched against
+                               the entire event payload.
+                               </para>
+                               <para>
+                               Syntax: [name(&lt;event_name&gt;)][,header(&lt;header_name&gt;)][,&lt;match_method&gt;]
+                               </para>
+                               <para>
+                               One of each of the following may be specified separated by commas.
+                               </para>
+                               <para>
+                               </para>
                                <enumlist>
-                                       <enum name="name">
-                                               <para>Match only against the event name.</para>
+                                       <enum name="action(include|exclude)">
+                                               <para>
+                                               Instead of prefixing the Filter with <literal>!</literal> to exclude matching events,
+                                               specify <literal>action(exclude)</literal>.  Although the default is <literal>include</literal>
+                                               if <literal>action</literal> isn't specified, adding <literal>action(include)</literal>
+                                               will help with readability.
+                                               </para>
+                                               <para>
+                                               </para>
                                        </enum>
-                                       <enum name="header(header_name)">
-                                               <para>Match only against the contents of this event header.</para>
+                                       <enum name="name(&lt;event_name&gt;)">
+                                               <para>
+                                               Only events with name <replaceable>event_name</replaceable> will be included.
+                                               </para>
+                                               <para>
+                                               </para>
                                        </enum>
-                                       <enum name="all">
-                                               <para>Match against the entire event payload.</para>
+                                       <enum name="header(&lt;header_name&gt;)">
+                                               <para>
+                                               Only events containing a header with a name of <replaceable>header_name</replaceable>
+                                               will be included and the <literal>Filter</literal> parameter (if supplied) will only be
+                                               matched against the value of the header.
+                                               </para>
+                                               <para>
+                                               </para>
                                        </enum>
+                                       <enum name="&lt;match_method&gt;">
+                                               <para>Specifies how the <literal>Filter</literal> parameter
+                                               is to be applied to the results of applying any 
+                                               <literal>name(&lt;event_name&gt;)</literal> and/or
+                                               <literal>header(&lt;header_name&gt;)</literal> parameters
+                                               above.
+                                               </para>
+                                               <para>
+                                               One of the following:
+                                               </para>
+                                               <enumlist>
+                                                       <enum name="regex">
+                                                               <para>The <literal>Filter</literal> parameter contains a regular expression
+                                                               which will be matched against the result. (default)
+                                                               </para>
+                                                               <para>
+                                                               </para>
+                                                       </enum>
+                                                       <enum name="exact">
+                                                               <para>The <literal>Filter</literal> parameter contains a string which must
+                                                               exactly match the entire result.
+                                                               </para>
+                                                               <para>
+                                                               </para>
+                                                       </enum>
+                                                       <enum name="startsWith">
+                                                               <para>The <literal>Filter</literal> parameter contains a string which must
+                                                               match the beginning of the result.
+                                                               </para>
+                                                               <para>
+                                                               </para>
+                                                       </enum>
+                                                       <enum name="endsWith">
+                                                               <para>The <literal>Filter</literal> parameter contains a string which must
+                                                               match the end of the result.
+                                                               </para>
+                                                               <para>
+                                                               </para>
+                                                       </enum>
+                                                       <enum name="contains">
+                                                               <para>The <literal>Filter</literal> parameter contains a string
+                                                               which will be searched for in the result.
+                                                               </para>
+                                                               <para>
+                                                               </para>
+                                                       </enum>
+                                                       <enum name="none">
+                                                               <para>The <literal>Filter</literal> parameter is ignored.
+                                                               </para>
+                                                       </enum>
+                                               </enumlist>
+                                       </enum> 
                                </enumlist>
-                               <para>The default is <literal>all</literal></para>
                        </parameter>
                        <parameter name="Filter">
-                               <para>Filters can be whitelist or blacklist</para>
-                               <para>Example whitelist filter: "Event: Newchannel"</para>
-                               <para>Example blacklist filter: "!Channel: DAHDI.*"</para>
-                               <para>This filter option is used to whitelist or blacklist events per user to be
-                               reported with regular expressions and are allowed if both the regex matches
-                               and the user has read access as defined in manager.conf. Filters are assumed to be for whitelisting
-                               unless preceeded by an exclamation point, which marks it as being black.
-                               Evaluation of the filters is as follows:</para>
-                               <para>- If no filters are configured all events are reported as normal.</para>
-                               <para>- If there are white filters only: implied black all filter processed first, then white filters.</para>
-                               <para>- If there are black filters only: implied white all filter processed first, then black filters.</para>
-                               <para>- If there are both white and black filters: implied black all filter processed first, then white
-                               filters, and lastly black filters.</para>
+                               <para>The match expression to be applied to the event.</para>
+                               <para>See the manager.conf.sample file in the configs/samples
+                               directory of the Asterisk source tree for more information.</para>
                        </parameter>
                </syntax>
                <description>
-                       <para>The filters added are only used for the current session.
-                       Once the connection is closed the filters are removed.</para>
-                       <para>This comand requires the system permission because
+                       <para>See the manager.conf.sample file in the configs/samples
+                       directory of the Asterisk source tree for a full description
+                       and examples.</para>
+                       <note>
+                       <para>
+                       The filters added are only used for the current session.
+                       Once the connection is closed the filters are removed.
+                       </para>
+                       </note>
+                       <note>
+                       <para>
+                       This comand requires the system permission because
                        this command can be used to create filters that may bypass
-                       filters defined in manager.conf</para>
+                       filters defined in manager.conf
+                       </para>
+                       </note>
                </description>
        </manager>
        <manager name="BlindTransfer" language="en_US">