]> granicus.if.org Git - apache/blob - modules/experimental/mod_filter.c
use standard copyright notice
[apache] / modules / experimental / mod_filter.c
1 /* Copyright 2004 The Apache Software Foundation
2  *
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15
16 /* Originally contributed by Nick Kew <nick webthing.com>
17  *
18  * At the time of writing, this is designed primarily for use with
19  * httpd 2.2, but is also back-compatible with 2.0.  It is likely
20  * that the 2.0 and 2.2 versions may diverge in future, as additional
21  * capabilities for 2.2 are added, including updates to util_filter.
22  *
23  * 21/9/04: Unifying data structures with util_filter.
24  * From now on, until and unless we backport, mod_filter requires
25  * util_filter.h from CVS or httpd-2.1+ to compile.
26  * There's a minimal patch for httpd-2.0 users maintained by Nick
27  * to compile mod_filter at http://www.apache.org/~niq/
28  */
29
30 #define APR_WANT_STRFUNC
31 #include "apr_want.h"
32 #include "apr_lib.h"
33 #include "apr_strings.h"
34 #include "apr_hash.h"
35 #include "httpd.h"
36 #include "http_config.h"
37 #include "http_request.h"
38 #include "http_log.h"
39 #include "util_filter.h"
40
41 module AP_MODULE_DECLARE_DATA filter_module;
42
43 typedef struct {
44     ap_out_filter_func func;
45     void *fctx;
46 } harness_ctx;
47
48 typedef struct mod_filter_chain {
49     const char *fname;
50     struct mod_filter_chain *next;
51 } mod_filter_chain;
52
53 typedef struct {
54     apr_hash_t *live_filters;
55     mod_filter_chain *chain;
56 } mod_filter_cfg;
57
58 static const char *filter_bucket_type(apr_bucket *b)
59 {
60     static struct {
61         const apr_bucket_type_t *fn;
62         const char *desc;
63     } types[] = {
64         { &apr_bucket_type_heap,      "HEAP"      },
65         { &apr_bucket_type_transient, "TRANSIENT" },
66         { &apr_bucket_type_immortal,  "IMMORTAL"  },
67         { &apr_bucket_type_pool,      "POOL"      },
68         { &apr_bucket_type_eos,       "EOS"       },
69         { &apr_bucket_type_flush,     "FLUSH"     },
70         { &apr_bucket_type_file,      "FILE"      },
71 #if APR_HAS_MMAP
72         { &apr_bucket_type_mmap,      "MMAP"      },
73 #endif
74         { &apr_bucket_type_pipe,      "PIPE"      },
75         { &apr_bucket_type_socket,    "SOCKET"    },
76         { NULL, NULL }
77     };
78     int i = 0;
79
80     do {
81         if (b->type == types[i].fn) {
82             return types[i].desc;
83         }
84     } while (types[++i].fn != NULL);
85
86     return "(error)";
87 }
88
89 static void filter_trace(apr_pool_t *pool, int debug, const char *fname,
90                          apr_bucket_brigade *bb)
91 {
92     apr_bucket *b;
93     switch (debug) {
94     case 0:        /* normal, operational use */
95         return;
96     case 1:        /* mod_diagnostics level */
97         ap_log_perror(APLOG_MARK, APLOG_NOTICE, 0, pool, fname);
98         for (b = APR_BRIGADE_FIRST(bb);
99              b != APR_BRIGADE_SENTINEL(bb);
100              b = APR_BUCKET_NEXT(b)) {
101             ap_log_perror(APLOG_MARK, APLOG_NOTICE, 0, pool, "   %s: %s %d",
102                           fname, filter_bucket_type(b), b->length);
103         }
104         break;
105     }
106 }
107
108 static int filter_init(ap_filter_t *f)
109 {
110     ap_filter_provider_t *p;
111     int err = OK;
112     ap_filter_rec_t *filter = f->frec;
113
114     f->ctx = apr_pcalloc(f->r->pool, sizeof(harness_ctx));
115     for (p = filter->providers; p; p = p->next) {
116         if (p->frec->filter_init_func) {
117             if (err = p->frec->filter_init_func(f), err != OK) {
118                 ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, f->r,
119                               "filter_init for %s failed", p->frec->name);
120                 break;        /* if anyone errors out here, so do we */
121             }
122         }
123     }
124
125     return err;
126 }
127
128 static ap_out_filter_func filter_lookup(request_rec *r, ap_filter_rec_t *filter)
129 {
130     ap_filter_provider_t *provider;
131     const char *str = NULL;
132     char *str1;
133     int match;
134     unsigned int proto_flags;
135
136     /* Check registered providers in order */
137     for (provider = filter->providers; provider; provider = provider->next) {
138         match = 1;
139         switch (filter->dispatch) {
140         case REQUEST_HEADERS:
141             str = apr_table_get(r->headers_in, filter->value);
142             break;
143         case RESPONSE_HEADERS:
144             str = apr_table_get(r->headers_out, filter->value);
145             break;
146         case SUBPROCESS_ENV:
147             str = apr_table_get(r->subprocess_env, filter->value);
148             break;
149         case CONTENT_TYPE:
150             str = r->content_type;
151             break;
152         case HANDLER:
153             str = r->handler;
154             break;
155         }
156
157         /* treat nulls so we don't have to check every strcmp individually
158          * Not sure if there's anything better to do with them
159          */
160         if (!str) {
161             if (provider->match_type == DEFINED && provider->match.c) {
162                 match = 0;
163             }
164         }
165         else if (!provider->match.c) {
166             match = 0;
167         }
168         else {
169             /* Now we have no nulls, so we can do string and regexp matching */
170             switch (provider->match_type) {
171             case STRING_MATCH:
172                 if (strcasecmp(str, provider->match.c)) {
173                     match = 0;
174                 }
175                 break;
176             case STRING_CONTAINS:
177                 str1 = apr_pstrdup(r->pool, str);
178                 ap_str_tolower(str1);
179                 if (!strstr(str1, provider->match.c)) {
180                     match = 0;
181                 }
182                 break;
183             case REGEX_MATCH:
184                 if (ap_regexec(provider->match.r, str, 0, NULL, 0)
185                     == REG_NOMATCH) {
186                 match = 0;
187                 }
188                 break;
189             case INT_EQ:
190                 if (atoi(str) != provider->match.i) {
191                     match = 0;
192                 }
193                 break;
194             case INT_LT:
195                 if (atoi(str) < provider->match.i) {
196                     match = 0;
197                 }
198                 break;
199             case INT_GT:
200                 if (atoi(str) > provider->match.i) {
201                     match = 0;
202                 }
203                 break;
204             case DEFINED:        /* we already handled this:-) */
205                 break;
206             }
207         }
208
209         if (match != provider->not) {
210             /* condition matches this provider */
211 #ifndef NO_PROTOCOL
212             /* check protocol
213              *
214              * FIXME:
215              * This is a quick hack and almost certainly buggy.
216              * The idea is that by putting this in mod_filter, we relieve
217              * filter implementations of the burden of fixing up HTTP headers
218              * for cases that are routinely affected by filters.
219              * 
220              * Default is ALWAYS to do nothing, so as not to tread on the
221              * toes of filters which want to do it themselves.
222              * 
223              */
224             proto_flags = filter->proto_flags | provider->frec->proto_flags;
225
226             /* some specific things can't happen in a proxy */
227             if (r->proxyreq) {
228                 if (proto_flags & AP_FILTER_PROTO_NO_PROXY) {
229                     /* can't use this provider; try next */
230                     continue;
231                 }
232
233                 if (proto_flags & AP_FILTER_PROTO_TRANSFORM) {
234                     str = apr_table_get(r->headers_out, "Cache-Control");
235                     if (str) {
236                         str1 = apr_pstrdup(r->pool, str);
237                         ap_str_tolower(str1);
238                         if (strstr(str1, "no-transform")) {
239                             /* can't use this provider; try next */
240                             continue;
241                         }
242                     }
243                     apr_table_addn(r->headers_out, "Warning",
244                                    apr_psprintf(r->pool,
245                                                 "214 %s Transformation applied",
246                                                 r->hostname));
247                 }
248             }
249
250             /* things that are invalidated if the filter transforms content */
251             if (proto_flags & AP_FILTER_PROTO_CHANGE) {
252                 apr_table_unset(r->headers_out, "Content-MD5");
253                 apr_table_unset(r->headers_out, "ETag");
254                 if (proto_flags & AP_FILTER_PROTO_CHANGE_LENGTH) {
255                     apr_table_unset(r->headers_out, "Content-Length");
256                 }
257             }
258
259             /* no-cache is for a filter that has different effect per-hit */
260             if (proto_flags & AP_FILTER_PROTO_NO_CACHE) {
261                 apr_table_unset(r->headers_out, "Last-Modified");
262                 apr_table_addn(r->headers_out, "Cache-Control", "no-cache");
263             }
264
265             if (proto_flags & AP_FILTER_PROTO_NO_BYTERANGE) {
266                 apr_table_unset(r->headers_out, "Accept-Ranges");
267             }
268             else if (filter->range) {
269                 apr_table_setn(r->headers_in, "Range", filter->range);
270             }
271 #endif
272             return provider->frec->filter_func.out_func;
273         }
274     }
275
276     /* No provider matched */
277     return NULL;
278 }
279
280 static apr_status_t filter_harness(ap_filter_t *f, apr_bucket_brigade *bb)
281 {
282     apr_status_t ret;
283     const char *cachecontrol;
284     char *str;
285     harness_ctx *ctx = f->ctx;
286     ap_filter_rec_t *filter = f->frec;
287
288     if (f->r->status != 200) {
289         ap_remove_output_filter(f);
290         return ap_pass_brigade(f->next, bb);
291     }
292
293     filter_trace(f->c->pool, filter->debug, f->frec->name, bb);
294
295     /* look up a handler function if we haven't already set it */
296     if (!ctx->func) {
297 #ifndef NO_PROTOCOL
298         if (f->r->proxyreq) {
299             if (filter->proto_flags & AP_FILTER_PROTO_NO_PROXY) {
300                 ap_remove_output_filter(f);
301                 return ap_pass_brigade(f->next, bb);
302             }
303
304             if (filter->proto_flags & AP_FILTER_PROTO_TRANSFORM) {
305                 cachecontrol = apr_table_get(f->r->headers_out,
306                                              "Cache-Control");
307                 if (cachecontrol) {
308                     str = apr_pstrdup(f->r->pool,  cachecontrol);
309                     ap_str_tolower(str);
310                     if (strstr(str, "no-transform")) {
311                         ap_remove_output_filter(f);
312                         return ap_pass_brigade(f->next, bb);
313                     }
314                 }
315             }
316         }
317 #endif
318         ctx->func = filter_lookup(f->r, filter);
319         if (!ctx->func) {
320             ap_remove_output_filter(f);
321             return ap_pass_brigade(f->next, bb);
322         }
323     }
324
325     /* call the content filter with its own context, then restore our
326      * context
327      */
328     f->ctx = ctx->fctx;
329     ret = ctx->func(f, bb);
330     ctx->fctx = f->ctx;
331     f->ctx = ctx;
332
333     return ret;
334 }
335
336 #ifndef NO_PROTOCOL
337 static const char *filter_protocol(cmd_parms *cmd, void *CFG, const char *fname,
338                                    const char *pname, const char *proto)
339 {
340     static const char *sep = ";,\t";
341     char *arg;
342     char *tok = 0;
343     unsigned int flags = 0;
344     mod_filter_cfg *cfg = CFG;
345     ap_filter_provider_t *provider = NULL;
346     ap_filter_rec_t *filter = apr_hash_get(cfg->live_filters, fname,
347                                            APR_HASH_KEY_STRING);
348
349     if (!provider) {
350         return "FilterProtocol: No such filter";
351     }
352
353     /* Fixup the args: it's really pname that's optional */
354     if (proto == NULL) {
355         proto = pname;
356         pname = NULL;
357     }
358     else {
359         /* Find provider */
360         for (provider = filter->providers; provider; provider = provider->next) {
361             if (!strcasecmp(provider->frec->name, pname)) {
362                 break;
363             }
364         }
365         if (!provider) {
366             return "FilterProtocol: No such provider for this filter";
367         }
368     }
369
370     /* Now set flags from our args */
371     for (arg = apr_strtok(apr_pstrdup(cmd->pool, proto), sep, &tok);
372          arg; arg = apr_strtok(NULL, sep, &tok)) {
373
374         if (!strcasecmp(arg, "change=yes")) {
375             flags |= AP_FILTER_PROTO_CHANGE | AP_FILTER_PROTO_CHANGE_LENGTH;
376         }
377         else if (!strcasecmp(arg, "change=1:1")) {
378             flags |= AP_FILTER_PROTO_CHANGE;
379         }
380         else if (!strcasecmp(arg, "byteranges=no")) {
381             flags |= AP_FILTER_PROTO_NO_BYTERANGE;
382         }
383         else if (!strcasecmp(arg, "proxy=no")) {
384             flags |= AP_FILTER_PROTO_NO_PROXY;
385         }
386         else if (!strcasecmp(arg, "proxy=transform")) {
387             flags |= AP_FILTER_PROTO_TRANSFORM;
388         }
389         else if (!strcasecmp(arg, "cache=no")) {
390             flags |= AP_FILTER_PROTO_NO_CACHE;
391         }
392     }
393
394     if (pname) {
395         provider->frec->proto_flags = flags;
396     }
397     else {
398         filter->proto_flags = flags;
399     }
400
401     return NULL;
402 }
403 #endif
404
405 static const char *filter_declare(cmd_parms *cmd, void *CFG, const char *fname,
406                                   const char *condition, const char *place)
407 {
408     const char *eq;
409     char *tmpname = "";
410     mod_filter_cfg *cfg = (mod_filter_cfg *)CFG;
411     ap_filter_rec_t *filter;
412
413     filter = apr_pcalloc(cmd->pool, sizeof(ap_filter_rec_t));
414     apr_hash_set(cfg->live_filters, fname, APR_HASH_KEY_STRING, filter);
415
416     filter->name = fname;
417     filter->filter_init_func = filter_init;
418     filter->filter_func.out_func = filter_harness;
419     filter->ftype = AP_FTYPE_RESOURCE;
420     filter->next = NULL;
421
422     /* determine what this filter will dispatch on */
423     eq = ap_strchr_c(condition, '=');
424     if (eq) {
425         tmpname = apr_pstrdup(cmd->pool, eq+1);
426         if (!strncasecmp(condition, "env=", 4)) {
427             filter->dispatch = SUBPROCESS_ENV;
428         }
429         else if (!strncasecmp(condition, "req=", 4)) {
430             filter->dispatch = REQUEST_HEADERS;
431         }
432         else if (!strncasecmp(condition, "resp=", 5)) {
433             filter->dispatch = RESPONSE_HEADERS;
434         }
435         else {
436             return "FilterCondition: unrecognized dispatch table";
437         }
438     }
439     else {
440         if (!strcasecmp(condition, "handler")) {
441             filter->dispatch = HANDLER;
442         }
443         else {
444             filter->dispatch = RESPONSE_HEADERS;
445         }
446         tmpname = apr_pstrdup(cmd->pool, condition);
447         ap_str_tolower(tmpname);
448     }
449
450     if (   (filter->dispatch == RESPONSE_HEADERS)
451         && !strcmp(tmpname, "content-type")) {
452         filter->dispatch = CONTENT_TYPE;
453     }
454     filter->value = tmpname;
455
456     if (place) {
457         if (!strcasecmp(place, "CONTENT_SET")) {
458             filter->ftype = AP_FTYPE_CONTENT_SET;
459         }
460         else if (!strcasecmp(place, "PROTOCOL")) {
461             filter->ftype = AP_FTYPE_PROTOCOL;
462         }
463         else if (!strcasecmp(place, "CONNECTION")) {
464             filter->ftype = AP_FTYPE_CONNECTION;
465         }
466         else if (!strcasecmp(place, "NETWORK")) {
467             filter->ftype = AP_FTYPE_NETWORK;
468         }
469     }
470
471     return NULL;
472 }
473
474 static const char *filter_provider(cmd_parms *cmd, void *CFG,
475         const char *fname, const char *pname, const char *match)
476 {
477     int flags;
478     ap_filter_provider_t *provider;
479     const char *rxend;
480     const char *c;
481     char *str;
482
483     /* fname has been declared with DeclareFilter, so we can look it up */
484     mod_filter_cfg *cfg = CFG;
485     ap_filter_rec_t *frec = apr_hash_get(cfg->live_filters, fname, APR_HASH_KEY_STRING);
486     /* if provider has been registered, we can look it up */
487     ap_filter_rec_t *provider_frec = ap_get_output_filter_handle(pname);
488     /* or if provider is mod_filter itself, we can also look it up */
489
490     if (!provider_frec) {
491         provider_frec = apr_hash_get(cfg->live_filters, pname, APR_HASH_KEY_STRING);
492     }
493
494     if (!frec) {
495         return apr_psprintf(cmd->pool, "Undeclared smart filter %s", fname);
496     }
497     else if (!provider_frec) {
498         return apr_psprintf(cmd->pool, "Unknown filter provider %s", pname);
499     }
500     else {
501         provider = apr_palloc(cmd->pool, sizeof(ap_filter_provider_t));
502
503         if (match[0] == '!') {
504             provider->not = 1;
505             ++match;
506         }
507         else {
508             provider->not = 0;
509         }
510
511         switch (match[0]) {
512         case '<':
513             provider->match_type = INT_LT;
514             provider->match.i = atoi(match+1);
515             break;
516         case '>':
517             provider->match_type = INT_GT;
518             provider->match.i = atoi(match+1);
519             break;
520         case '=':
521             provider->match_type = INT_EQ;
522             provider->match.i = atoi(match+1);
523             break;
524         case '/':
525             provider->match_type = REGEX_MATCH;
526             rxend = ap_strchr_c(match+1, '/');
527             if (!rxend) {
528                   return "Bad regexp syntax";
529             }
530             flags = REG_NOSUB;        /* we're not mod_rewrite:-) */
531             for (c = rxend+1; *c; ++c) {
532                 switch (*c) {
533                     case 'i': flags |= REG_ICASE; break;
534                     case 'x': flags |= REG_EXTENDED; break;
535                 }
536             }
537             provider->match.r = ap_pregcomp(cmd->pool,
538                                             apr_pstrndup(cmd->pool, match+1,
539                                                          rxend-match-1),
540                                             flags);
541             break;
542         case '*':
543             provider->match_type = DEFINED;
544             provider->match.i = -1;
545             break;
546         case '$':
547             provider->match_type = STRING_CONTAINS;
548             str = apr_pstrdup(cmd->pool, match+1);
549             ap_str_tolower(str);
550             provider->match.c = str;
551             break;
552         default:
553             provider->match_type = STRING_MATCH;
554             provider->match.c = apr_pstrdup(cmd->pool, match);
555             break;
556         }
557         provider->frec = provider_frec;
558         provider->next = frec->providers;
559         frec->providers = provider;
560     }
561
562     return NULL;
563 }
564
565 static const char *filter_chain(cmd_parms *cmd, void *CFG, const char *arg)
566 {
567     mod_filter_chain *p;
568     mod_filter_chain *q;
569     mod_filter_cfg *cfg = CFG;
570
571     switch (arg[0]) {
572     case '+':        /* add to end of chain */
573         p = apr_pcalloc(cmd->pool, sizeof(mod_filter_chain));
574         p->fname = arg+1;
575         if (cfg->chain) {
576             for (q = cfg->chain; q->next; q = q->next);
577             q->next = p;
578         }
579         else {
580             cfg->chain = p;
581         }
582         break;
583
584     case '@':        /* add to start of chain */
585         p = apr_palloc(cmd->pool, sizeof(mod_filter_chain));
586         p->fname = arg+1;
587         p->next = cfg->chain;
588         cfg->chain = p;
589         break;
590
591     case '-':        /* remove from chain */
592         if (cfg->chain) {
593             if (strcasecmp(cfg->chain->fname, arg+1)) {
594                 for (p = cfg->chain; p->next; p = p->next) {
595                     if (!strcasecmp(p->next->fname, arg+1)) {
596                         p->next = p->next->next;
597                     }
598                 }
599             }
600             else {
601                 cfg->chain = cfg->chain->next;
602             }
603         }
604         break;
605
606     case '!':        /* Empty the chain */
607         cfg->chain = NULL;
608         break;
609
610     case '=':        /* initialise chain with this arg */
611         p = apr_pcalloc(cmd->pool, sizeof(mod_filter_chain));
612         p->fname = arg+1;
613         cfg->chain = p;
614         break;
615
616     default:        /* add to end */
617         p = apr_pcalloc(cmd->pool, sizeof(mod_filter_chain));
618         p->fname = arg;
619         if (cfg->chain) {
620             for (q = cfg->chain; q->next; q = q->next);
621             q->next = p;
622         }
623         else {
624             cfg->chain = p;
625         }
626         break;
627     }
628
629     return NULL;
630 }
631
632 static const char *filter_debug(cmd_parms *cmd, void *CFG, const char *fname,
633                                 const char *level)
634 {
635     mod_filter_cfg *cfg = CFG;
636     ap_filter_rec_t *frec = apr_hash_get(cfg->live_filters, fname,
637                                          APR_HASH_KEY_STRING);
638     frec->debug = atoi(level);
639
640     return NULL;
641 }
642
643 static void filter_insert(request_rec *r)
644 {
645     mod_filter_chain *p;
646     ap_filter_rec_t *filter;
647     mod_filter_cfg *cfg = ap_get_module_config(r->per_dir_config,
648                                                &filter_module);
649 #ifndef NO_PROTOCOL
650     int ranges = 1;
651 #endif
652
653     for (p = cfg->chain; p; p = p->next) {
654         filter = apr_hash_get(cfg->live_filters, p->fname, APR_HASH_KEY_STRING);
655         ap_add_output_filter_handle(filter, NULL, r, r->connection);
656 #ifndef NO_PROTOCOL
657         if (ranges && (filter->proto_flags
658                        & (AP_FILTER_PROTO_NO_BYTERANGE
659                           | AP_FILTER_PROTO_CHANGE_LENGTH))) {
660             filter->range = apr_table_get(r->headers_in, "Range");
661             apr_table_unset(r->headers_in, "Range");
662             ranges = 0;
663         }
664 #endif
665     }
666
667     return;
668 }
669
670 static void filter_hooks(apr_pool_t *pool)
671 {
672     ap_hook_insert_filter(filter_insert, NULL, NULL, APR_HOOK_MIDDLE);
673 }
674
675 static void *filter_config(apr_pool_t *pool, char *x)
676 {
677     mod_filter_cfg *cfg = apr_palloc(pool, sizeof(mod_filter_cfg));
678     cfg->live_filters = apr_hash_make(pool);
679     cfg->chain = NULL;
680     return cfg;
681 }
682
683 static void *filter_merge(apr_pool_t *pool, void *BASE, void *ADD)
684 {
685     mod_filter_cfg *base = BASE;
686     mod_filter_cfg *add = ADD;
687     mod_filter_chain *savelink = 0;
688     mod_filter_chain *newlink;
689     mod_filter_chain *p;
690     mod_filter_cfg *conf = apr_palloc(pool, sizeof(mod_filter_cfg));
691
692     conf->live_filters = apr_hash_overlay(pool, add->live_filters,
693                                           base->live_filters);
694     if (base->chain && add->chain) {
695         for (p = base->chain; p; p = p->next) {
696             newlink = apr_pmemdup(pool, p, sizeof(mod_filter_chain));
697             if (savelink) {
698                 savelink->next = newlink;
699                 savelink = newlink;
700             }
701             else {
702                 conf->chain = savelink = newlink;
703             }
704         }
705
706         for (p = add->chain; p; p = p->next) {
707             newlink = apr_pmemdup(pool, p, sizeof(mod_filter_chain));
708             savelink->next = newlink;
709             savelink = newlink;
710         }
711     }
712     else if (add->chain) {
713         conf->chain = add->chain;
714     }
715     else {
716         conf->chain = base->chain;
717     }
718
719     return conf;
720 }
721
722 static const command_rec filter_cmds[] = {
723     AP_INIT_TAKE23("FilterDeclare", filter_declare, NULL, OR_OPTIONS,
724         "filter-name, dispatch-criterion [, filter-type]"),
725     AP_INIT_TAKE3("FilterProvider", filter_provider, NULL, OR_OPTIONS,
726         "filter-name, provider-name, dispatch-match"),
727     AP_INIT_ITERATE("FilterChain", filter_chain, NULL, OR_OPTIONS,
728         "list of filter names with optional [+-=!@]"),
729     AP_INIT_TAKE2("FilterTrace", filter_debug, NULL, RSRC_CONF | ACCESS_CONF,
730         "Debug level"),
731 #ifndef NO_PROTOCOL
732     AP_INIT_TAKE23("FilterProtocol", filter_protocol, NULL, OR_OPTIONS,
733         "filter-name [provider-name] protocol-args"),
734 #endif
735     { NULL }
736 };
737
738 module AP_MODULE_DECLARE_DATA filter_module = {
739     STANDARD20_MODULE_STUFF,
740     filter_config,
741     filter_merge,
742     NULL,
743     NULL,
744     filter_cmds,
745     filter_hooks
746 };