From 6beeb1b708550be0d4a53b272283e17e5e35fe17 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 17:01:30 +0200 Subject: Adding upstream version 2.4.57. Signed-off-by: Daniel Baumann --- modules/filters/mod_filter.c | 767 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 767 insertions(+) create mode 100644 modules/filters/mod_filter.c (limited to 'modules/filters/mod_filter.c') diff --git a/modules/filters/mod_filter.c b/modules/filters/mod_filter.c new file mode 100644 index 0000000..5b5ecf6 --- /dev/null +++ b/modules/filters/mod_filter.c @@ -0,0 +1,767 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define APR_WANT_STRFUNC +#include "apr_want.h" +#include "apr_lib.h" +#include "apr_strings.h" +#include "apr_hash.h" +#include "httpd.h" +#include "http_config.h" +#include "http_request.h" +#include "http_log.h" +#include "util_filter.h" +#include "ap_expr.h" + +module AP_MODULE_DECLARE_DATA filter_module; + +/** + * @brief is a filter provider, as defined and implemented by mod_filter. + * + * The struct is a linked list, with dispatch criteria + * defined for each filter. The provider implementation itself is a + * (2.0-compatible) ap_filter_rec_t* frec. + */ +struct ap_filter_provider_t { + ap_expr_info_t *expr; + const char **types; + + /** The filter that implements this provider */ + ap_filter_rec_t *frec; + + /** The next provider in the list */ + ap_filter_provider_t *next; +}; + +/** we need provider_ctx to save ctx values set by providers in filter_init */ +typedef struct provider_ctx provider_ctx; +struct provider_ctx { + ap_filter_provider_t *provider; + void *ctx; + provider_ctx *next; +}; +typedef struct { + ap_out_filter_func func; + void *fctx; + provider_ctx *init_ctx; +} harness_ctx; + +typedef struct mod_filter_chain { + const char *fname; + struct mod_filter_chain *next; +} mod_filter_chain; + +typedef struct { + apr_hash_t *live_filters; + mod_filter_chain *chain; +} mod_filter_cfg; + +typedef struct { + const char* range ; +} mod_filter_ctx ; + + +static void filter_trace(conn_rec *c, int debug, const char *fname, + apr_bucket_brigade *bb) +{ + apr_bucket *b; + + switch (debug) { + case 0: /* normal, operational use */ + return; + case 1: /* mod_diagnostics level */ + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(01375) "%s", fname); + for (b = APR_BRIGADE_FIRST(bb); + b != APR_BRIGADE_SENTINEL(bb); + b = APR_BUCKET_NEXT(b)) { + + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(01376) + "%s: type: %s, length: %" APR_SIZE_T_FMT, + fname, b->type->name ? b->type->name : "(unknown)", + b->length); + } + break; + } +} + +static int filter_init(ap_filter_t *f) +{ + ap_filter_provider_t *p; + provider_ctx *pctx; + int err; + ap_filter_rec_t *filter = f->frec; + + harness_ctx *fctx = apr_pcalloc(f->r->pool, sizeof(harness_ctx)); + for (p = filter->providers; p; p = p->next) { + if (p->frec->filter_init_func == filter_init) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, f->c, APLOGNO(01377) + "Chaining of FilterProviders not supported"); + return HTTP_INTERNAL_SERVER_ERROR; + } + else if (p->frec->filter_init_func) { + f->ctx = NULL; + if ((err = p->frec->filter_init_func(f)) != OK) { + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, f->c, APLOGNO(01378) + "filter_init for %s failed", p->frec->name); + return err; /* if anyone errors out here, so do we */ + } + if (f->ctx != NULL) { + /* the filter init function set a ctx - we need to record it */ + pctx = apr_pcalloc(f->r->pool, sizeof(provider_ctx)); + pctx->provider = p; + pctx->ctx = f->ctx; + pctx->next = fctx->init_ctx; + fctx->init_ctx = pctx; + } + } + } + f->ctx = fctx; + return OK; +} + +static int filter_lookup(ap_filter_t *f, ap_filter_rec_t *filter) +{ + ap_filter_provider_t *provider; + int match = 0; + const char *err = NULL; + request_rec *r = f->r; + harness_ctx *ctx = f->ctx; + provider_ctx *pctx; +#ifndef NO_PROTOCOL + unsigned int proto_flags; + mod_filter_ctx *rctx = ap_get_module_config(r->request_config, + &filter_module); +#endif + + /* Check registered providers in order */ + for (provider = filter->providers; provider; provider = provider->next) { + if (provider->expr) { + match = ap_expr_exec(r, provider->expr, &err); + if (err) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01379) + "Error evaluating filter dispatch condition: %s", + err); + match = 0; + } + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, + "Expression condition for '%s' %s", + provider->frec->name, + match ? "matched" : "did not match"); + } + else if (r->content_type) { + const char **type = provider->types; + size_t len = strcspn(r->content_type, "; \t"); + AP_DEBUG_ASSERT(type != NULL); + ap_log_rerror(APLOG_MARK, APLOG_TRACE4, 0, r, + "Content-Type '%s' ...", r->content_type); + while (*type) { + /* Handle 'content-type;charset=...' correctly */ + if (strncmp(*type, r->content_type, len) == 0 + && (*type)[len] == '\0') { + ap_log_rerror(APLOG_MARK, APLOG_TRACE4, 0, r, + "... matched '%s'", *type); + match = 1; + break; + } + else { + ap_log_rerror(APLOG_MARK, APLOG_TRACE4, 0, r, + "... did not match '%s'", *type); + } + type++; + } + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, + "Content-Type condition for '%s' %s", + provider->frec->name, + match ? "matched" : "did not match"); + } + else { + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, + "Content-Type condition for '%s' did not match: " + "no Content-Type", provider->frec->name); + } + + if (match) { + /* condition matches this provider */ +#ifndef NO_PROTOCOL + /* check protocol + * + * FIXME: + * This is a quick hack and almost certainly buggy. + * The idea is that by putting this in mod_filter, we relieve + * filter implementations of the burden of fixing up HTTP headers + * for cases that are routinely affected by filters. + * + * Default is ALWAYS to do nothing, so as not to tread on the + * toes of filters which want to do it themselves. + * + */ + proto_flags = provider->frec->proto_flags; + + /* some specific things can't happen in a proxy */ + if (r->proxyreq) { + if (proto_flags & AP_FILTER_PROTO_NO_PROXY) { + /* can't use this provider; try next */ + continue; + } + + if (proto_flags & AP_FILTER_PROTO_TRANSFORM) { + const char *str = apr_table_get(r->headers_out, + "Cache-Control"); + if (str) { + if (ap_strcasestr(str, "no-transform")) { + /* can't use this provider; try next */ + continue; + } + } + apr_table_addn(r->headers_out, "Warning", + apr_psprintf(r->pool, + "214 %s Transformation applied", + r->hostname)); + } + } + + /* things that are invalidated if the filter transforms content */ + if (proto_flags & AP_FILTER_PROTO_CHANGE) { + apr_table_unset(r->headers_out, "Content-MD5"); + apr_table_unset(r->headers_out, "ETag"); + if (proto_flags & AP_FILTER_PROTO_CHANGE_LENGTH) { + apr_table_unset(r->headers_out, "Content-Length"); + } + } + + /* no-cache is for a filter that has different effect per-hit */ + if (proto_flags & AP_FILTER_PROTO_NO_CACHE) { + apr_table_unset(r->headers_out, "Last-Modified"); + apr_table_addn(r->headers_out, "Cache-Control", "no-cache"); + } + + if (proto_flags & AP_FILTER_PROTO_NO_BYTERANGE) { + apr_table_setn(r->headers_out, "Accept-Ranges", "none"); + } + else if (rctx && rctx->range) { + /* restore range header we saved earlier */ + apr_table_setn(r->headers_in, "Range", rctx->range); + rctx->range = NULL; + } +#endif + for (pctx = ctx->init_ctx; pctx; pctx = pctx->next) { + if (pctx->provider == provider) { + ctx->fctx = pctx->ctx ; + } + } + ctx->func = provider->frec->filter_func.out_func; + return 1; + } + } + + /* No provider matched */ + return 0; +} + +static apr_status_t filter_harness(ap_filter_t *f, apr_bucket_brigade *bb) +{ + apr_status_t ret; +#ifndef NO_PROTOCOL + const char *cachecontrol; +#endif + harness_ctx *ctx = f->ctx; + ap_filter_rec_t *filter = f->frec; + + if (f->r->status != 200 + && !apr_table_get(f->r->subprocess_env, "filter-errordocs")) { + ap_remove_output_filter(f); + return ap_pass_brigade(f->next, bb); + } + + filter_trace(f->c, filter->debug, f->frec->name, bb); + + /* look up a handler function if we haven't already set it */ + if (!ctx->func) { +#ifndef NO_PROTOCOL + if (f->r->proxyreq) { + if (filter->proto_flags & AP_FILTER_PROTO_NO_PROXY) { + ap_remove_output_filter(f); + return ap_pass_brigade(f->next, bb); + } + + if (filter->proto_flags & AP_FILTER_PROTO_TRANSFORM) { + cachecontrol = apr_table_get(f->r->headers_out, + "Cache-Control"); + if (cachecontrol) { + if (ap_strcasestr(cachecontrol, "no-transform")) { + ap_remove_output_filter(f); + return ap_pass_brigade(f->next, bb); + } + } + } + } +#endif + if (!filter_lookup(f, filter)) { + ap_remove_output_filter(f); + return ap_pass_brigade(f->next, bb); + } + AP_DEBUG_ASSERT(ctx->func != NULL); + } + + /* call the content filter with its own context, then restore our + * context + */ + f->ctx = ctx->fctx; + ret = ctx->func(f, bb); + ctx->fctx = f->ctx; + f->ctx = ctx; + + return ret; +} + +#ifndef NO_PROTOCOL +static const char *filter_protocol(cmd_parms *cmd, void *CFG, const char *fname, + const char *pname, const char *proto) +{ + static const char *sep = ";, \t"; + char *arg; + char *tok = 0; + unsigned int flags = 0; + mod_filter_cfg *cfg = CFG; + ap_filter_provider_t *provider = NULL; + ap_filter_rec_t *filter = apr_hash_get(cfg->live_filters, fname, + APR_HASH_KEY_STRING); + + if (!filter) { + return "FilterProtocol: No such filter"; + } + + /* Fixup the args: it's really pname that's optional */ + if (proto == NULL) { + proto = pname; + pname = NULL; + } + else { + /* Find provider */ + for (provider = filter->providers; provider; provider = provider->next) { + if (!strcasecmp(provider->frec->name, pname)) { + break; + } + } + if (!provider) { + return "FilterProtocol: No such provider for this filter"; + } + } + + /* Now set flags from our args */ + for (arg = apr_strtok(apr_pstrdup(cmd->temp_pool, proto), sep, &tok); + arg; arg = apr_strtok(NULL, sep, &tok)) { + + if (!strcasecmp(arg, "change=yes")) { + flags |= AP_FILTER_PROTO_CHANGE | AP_FILTER_PROTO_CHANGE_LENGTH; + } + if (!strcasecmp(arg, "change=no")) { + flags &= ~(AP_FILTER_PROTO_CHANGE | AP_FILTER_PROTO_CHANGE_LENGTH); + } + else if (!strcasecmp(arg, "change=1:1")) { + flags |= AP_FILTER_PROTO_CHANGE; + } + else if (!strcasecmp(arg, "byteranges=no")) { + flags |= AP_FILTER_PROTO_NO_BYTERANGE; + } + else if (!strcasecmp(arg, "proxy=no")) { + flags |= AP_FILTER_PROTO_NO_PROXY; + } + else if (!strcasecmp(arg, "proxy=transform")) { + flags |= AP_FILTER_PROTO_TRANSFORM; + } + else if (!strcasecmp(arg, "cache=no")) { + flags |= AP_FILTER_PROTO_NO_CACHE; + } + } + + if (pname) { + provider->frec->proto_flags = flags; + } + else { + filter->proto_flags = flags; + } + + return NULL; +} +#endif + +static const char *filter_declare(cmd_parms *cmd, void *CFG, const char *fname, + const char *place) +{ + mod_filter_cfg *cfg = (mod_filter_cfg *)CFG; + ap_filter_rec_t *filter; + + filter = apr_pcalloc(cmd->pool, sizeof(ap_filter_rec_t)); + apr_hash_set(cfg->live_filters, fname, APR_HASH_KEY_STRING, filter); + + filter->name = fname; + filter->filter_init_func = filter_init; + filter->filter_func.out_func = filter_harness; + filter->ftype = AP_FTYPE_RESOURCE; + filter->next = NULL; + + if (place) { + if (!strcasecmp(place, "CONTENT_SET")) { + filter->ftype = AP_FTYPE_CONTENT_SET; + } + else if (!strcasecmp(place, "PROTOCOL")) { + filter->ftype = AP_FTYPE_PROTOCOL; + } + else if (!strcasecmp(place, "CONNECTION")) { + filter->ftype = AP_FTYPE_CONNECTION; + } + else if (!strcasecmp(place, "NETWORK")) { + filter->ftype = AP_FTYPE_NETWORK; + } + } + + return NULL; +} + +static const char *add_filter(cmd_parms *cmd, void *CFG, + const char *fname, const char *pname, + const char *expr, const char **types) +{ + mod_filter_cfg *cfg = CFG; + ap_filter_provider_t *provider; + const char *c; + ap_filter_rec_t* frec; + ap_filter_rec_t* provider_frec; + ap_expr_info_t *node; + const char *err = NULL; + + /* if provider has been registered, we can look it up */ + provider_frec = ap_get_output_filter_handle(pname); + if (!provider_frec) { + return apr_psprintf(cmd->pool, "Unknown filter provider %s", pname); + } + + /* fname has been declared with DeclareFilter, so we can look it up */ + frec = apr_hash_get(cfg->live_filters, fname, APR_HASH_KEY_STRING); + + /* or if provider is mod_filter itself, we can also look it up */ + if (!frec) { + c = filter_declare(cmd, CFG, fname, NULL); + if ( c ) { + return c; + } + frec = apr_hash_get(cfg->live_filters, fname, APR_HASH_KEY_STRING); + frec->ftype = provider_frec->ftype; + } + + if (!frec) { + return apr_psprintf(cmd->pool, "Undeclared smart filter %s", fname); + } + + provider = apr_palloc(cmd->pool, sizeof(ap_filter_provider_t)); + if (expr) { + node = ap_expr_parse_cmd(cmd, expr, 0, &err, NULL); + if (err) { + return apr_pstrcat(cmd->pool, + "Error parsing FilterProvider expression:", err, + NULL); + } + provider->expr = node; + provider->types = NULL; + } + else { + provider->types = types; + provider->expr = NULL; + } + provider->frec = provider_frec; + provider->next = frec->providers; + frec->providers = provider; + return NULL; +} + +static const char *filter_provider(cmd_parms *cmd, void *CFG, + const char *fname, const char *pname, + const char *expr) +{ + return add_filter(cmd, CFG, fname, pname, expr, NULL); +} + +static const char *filter_chain(cmd_parms *cmd, void *CFG, const char *arg) +{ + mod_filter_chain *p; + mod_filter_chain *q; + mod_filter_cfg *cfg = CFG; + + switch (arg[0]) { + case '+': /* add to end of chain */ + p = apr_pcalloc(cmd->pool, sizeof(mod_filter_chain)); + p->fname = arg+1; + if (cfg->chain) { + for (q = cfg->chain; q->next; q = q->next); + q->next = p; + } + else { + cfg->chain = p; + } + break; + + case '@': /* add to start of chain */ + p = apr_palloc(cmd->pool, sizeof(mod_filter_chain)); + p->fname = arg+1; + p->next = cfg->chain; + cfg->chain = p; + break; + + case '-': /* remove from chain */ + if (cfg->chain) { + if (strcasecmp(cfg->chain->fname, arg+1)) { + for (p = cfg->chain; p->next; p = p->next) { + if (!strcasecmp(p->next->fname, arg+1)) { + p->next = p->next->next; + } + } + } + else { + cfg->chain = cfg->chain->next; + } + } + break; + + case '!': /* Empty the chain */ + /** IG: Add a NULL provider to the beginning so that + * we can ensure that we'll empty everything before + * this when doing config merges later */ + p = apr_pcalloc(cmd->pool, sizeof(mod_filter_chain)); + p->fname = NULL; + cfg->chain = p; + break; + + case '=': /* initialise chain with this arg */ + /** IG: Prepend a NULL provider to the beginning as above */ + p = apr_pcalloc(cmd->pool, sizeof(mod_filter_chain)); + p->fname = NULL; + p->next = apr_pcalloc(cmd->pool, sizeof(mod_filter_chain)); + p->next->fname = arg+1; + cfg->chain = p; + break; + + default: /* add to end */ + p = apr_pcalloc(cmd->pool, sizeof(mod_filter_chain)); + p->fname = arg; + if (cfg->chain) { + for (q = cfg->chain; q->next; q = q->next); + q->next = p; + } + else { + cfg->chain = p; + } + break; + } + + return NULL; +} + +static const char *filter_bytype1(cmd_parms *cmd, void *CFG, + const char *pname, const char **types) +{ + const char *rv; + const char *fname; + int seen_name = 0; + mod_filter_cfg *cfg = CFG; + + /* construct fname from name */ + fname = apr_pstrcat(cmd->pool, "BYTYPE:", pname, NULL); + + /* check whether this is already registered, in which case + * it's already in the filter chain + */ + if (apr_hash_get(cfg->live_filters, fname, APR_HASH_KEY_STRING)) { + seen_name = 1; + } + + rv = add_filter(cmd, CFG, fname, pname, NULL, types); + + /* If it's the first time through, add to filterchain */ + if (rv == NULL && !seen_name) { + rv = filter_chain(cmd, CFG, fname); + } + return rv; +} + +static const char *filter_bytype(cmd_parms *cmd, void *CFG, + int argc, char *const argv[]) +{ + /* back compatibility, need to parse multiple components in filter name */ + char *pname; + char *strtok_state = NULL; + char *name; + const char **types; + const char *rv = NULL; + if (argc < 2) + return "AddOutputFilterByType requires at least two arguments"; + name = apr_pstrdup(cmd->temp_pool, argv[0]); + types = apr_palloc(cmd->pool, argc * sizeof(char *)); + memcpy(types, &argv[1], (argc - 1) * sizeof(char *)); + types[argc-1] = NULL; + for (pname = apr_strtok(name, ";", &strtok_state); + pname != NULL && rv == NULL; + pname = apr_strtok(NULL, ";", &strtok_state)) { + rv = filter_bytype1(cmd, CFG, pname, types); + } + return rv; +} + +static const char *filter_debug(cmd_parms *cmd, void *CFG, const char *fname, + const char *level) +{ + mod_filter_cfg *cfg = CFG; + ap_filter_rec_t *frec = apr_hash_get(cfg->live_filters, fname, + APR_HASH_KEY_STRING); + if (!frec) { + return apr_psprintf(cmd->pool, "Undeclared smart filter %s", fname); + } + frec->debug = atoi(level); + + return NULL; +} + +static void filter_insert(request_rec *r) +{ + mod_filter_chain *p; + ap_filter_rec_t *filter; + mod_filter_cfg *cfg = ap_get_module_config(r->per_dir_config, + &filter_module); +#ifndef NO_PROTOCOL + int ranges = 1; + mod_filter_ctx *ctx = apr_pcalloc(r->pool, sizeof(mod_filter_ctx)); + ap_set_module_config(r->request_config, &filter_module, ctx); +#endif + + /** IG: Now that we've merged to the final config, go one last time + * through the chain, and prune out the NULL filters */ + + for (p = cfg->chain; p; p = p->next) { + if (p->fname == NULL) + cfg->chain = p->next; + } + + for (p = cfg->chain; p; p = p->next) { + filter = apr_hash_get(cfg->live_filters, p->fname, APR_HASH_KEY_STRING); + if (filter == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(01380) + "Unknown filter %s not added", p->fname); + continue; + } + ap_add_output_filter_handle(filter, NULL, r, r->connection); +#ifndef NO_PROTOCOL + if (ranges && (filter->proto_flags + & (AP_FILTER_PROTO_NO_BYTERANGE + | AP_FILTER_PROTO_CHANGE_LENGTH))) { + ctx->range = apr_table_get(r->headers_in, "Range"); + apr_table_unset(r->headers_in, "Range"); + ranges = 0; + } +#endif + } +} + +static void filter_hooks(apr_pool_t *pool) +{ + ap_hook_insert_filter(filter_insert, NULL, NULL, APR_HOOK_MIDDLE); +} + +static void *filter_config(apr_pool_t *pool, char *x) +{ + mod_filter_cfg *cfg = apr_palloc(pool, sizeof(mod_filter_cfg)); + cfg->live_filters = apr_hash_make(pool); + cfg->chain = NULL; + return cfg; +} + +static void *filter_merge(apr_pool_t *pool, void *BASE, void *ADD) +{ + mod_filter_cfg *base = BASE; + mod_filter_cfg *add = ADD; + mod_filter_chain *savelink = 0; + mod_filter_chain *newlink; + mod_filter_chain *p; + mod_filter_cfg *conf = apr_palloc(pool, sizeof(mod_filter_cfg)); + + conf->live_filters = apr_hash_overlay(pool, add->live_filters, + base->live_filters); + if (base->chain && add->chain) { + for (p = base->chain; p; p = p->next) { + newlink = apr_pmemdup(pool, p, sizeof(mod_filter_chain)); + if (newlink->fname == NULL) { + conf->chain = savelink = newlink; + } + else if (savelink) { + savelink->next = newlink; + savelink = newlink; + } + else { + conf->chain = savelink = newlink; + } + } + + for (p = add->chain; p; p = p->next) { + newlink = apr_pmemdup(pool, p, sizeof(mod_filter_chain)); + /** Filter out merged chain resets */ + if (newlink->fname == NULL) { + conf->chain = savelink = newlink; + } + else if (savelink) { + savelink->next = newlink; + savelink = newlink; + } + else { + conf->chain = savelink = newlink; + } + } + } + else if (add->chain) { + conf->chain = add->chain; + } + else { + conf->chain = base->chain; + } + + return conf; +} + +static const command_rec filter_cmds[] = { + AP_INIT_TAKE12("FilterDeclare", filter_declare, NULL, OR_OPTIONS, + "filter-name [filter-type]"), + AP_INIT_TAKE3("FilterProvider", filter_provider, NULL, OR_OPTIONS, + "filter-name provider-name match-expression"), + AP_INIT_ITERATE("FilterChain", filter_chain, NULL, OR_OPTIONS, + "list of filter names with optional [+-=!@]"), + AP_INIT_TAKE2("FilterTrace", filter_debug, NULL, RSRC_CONF | ACCESS_CONF, + "filter-name debug-level"), + AP_INIT_TAKE_ARGV("AddOutputFilterByType", filter_bytype, NULL, OR_FILEINFO, + "output filter name followed by one or more content-types"), +#ifndef NO_PROTOCOL + AP_INIT_TAKE23("FilterProtocol", filter_protocol, NULL, OR_OPTIONS, + "filter-name [provider-name] protocol-args"), +#endif + { NULL } +}; + +AP_DECLARE_MODULE(filter) = { + STANDARD20_MODULE_STUFF, + filter_config, + filter_merge, + NULL, + NULL, + filter_cmds, + filter_hooks +}; -- cgit v1.2.3