diff --git a/configs/samples/manager.conf.sample b/configs/samples/manager.conf.sample index 342f290ea7..a8e3e7ed5f 100644 --- a/configs/samples/manager.conf.sample +++ b/configs/samples/manager.conf.sample @@ -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, diff --git a/main/manager.c b/main/manager.c index 8d9d4ffbaa..225b7e3a67 100644 --- a/main/manager.c +++ b/main/manager.c @@ -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) diff --git a/main/manager_doc.xml b/main/manager_doc.xml index 38e698d10e..07fd856d9a 100644 --- a/main/manager_doc.xml +++ b/main/manager_doc.xml @@ -1320,64 +1320,124 @@ </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(<event_name>)][,header(<header_name>)][,<match_method>] + </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(<event_name>)"> + <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(<header_name>)"> + <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="<match_method>"> + <para>Specifies how the <literal>Filter</literal> parameter + is to be applied to the results of applying any + <literal>name(<event_name>)</literal> and/or + <literal>header(<header_name>)</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">