/* [<][>][^][v][top][bottom][index][help] */
DEFINITIONS
This source file includes following definitions.
- filter_trace
- filter_init
- filter_lookup
- filter_harness
- filter_protocol
- filter_declare
- filter_provider
- filter_chain
- filter_debug
- filter_insert
- filter_hooks
- filter_config
- filter_merge
/* 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"
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 {
/** How to match this provider to filter dispatch criterion */
enum {
STRING_MATCH,
STRING_CONTAINS,
REGEX_MATCH,
INT_EQ,
INT_LT,
INT_LE,
INT_GT,
INT_GE,
DEFINED
} match_type;
/** negation on match_type */
int not;
/** The dispatch match itself - union member depends on match_type */
union {
const char *string;
ap_regex_t *regex;
int number;
} match;
/** The filter that implements this provider */
ap_filter_rec_t *frec;
/** The next provider in the list */
ap_filter_provider_t *next;
/** Dispatch criteria for filter providers */
enum {
HANDLER,
REQUEST_HEADERS,
RESPONSE_HEADERS,
SUBPROCESS_ENV,
CONTENT_TYPE
} dispatch;
/** Match value for filter providers */
const char* value;
};
/** 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, "%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,
"%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,
"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,
"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;
const char *str = NULL;
char *str1;
int match;
unsigned int proto_flags;
request_rec *r = f->r;
harness_ctx *ctx = f->ctx;
provider_ctx *pctx;
mod_filter_ctx *rctx = ap_get_module_config(r->request_config,
&filter_module);
/* Check registered providers in order */
for (provider = filter->providers; provider; provider = provider->next) {
match = 1;
switch (provider->dispatch) {
case REQUEST_HEADERS:
str = apr_table_get(r->headers_in, provider->value);
break;
case RESPONSE_HEADERS:
str = apr_table_get(r->headers_out, provider->value);
break;
case SUBPROCESS_ENV:
str = apr_table_get(r->subprocess_env, provider->value);
break;
case CONTENT_TYPE:
str = r->content_type;
break;
case HANDLER:
str = r->handler;
break;
}
/* treat nulls so we don't have to check every strcmp individually
* Not sure if there's anything better to do with them
*/
if (!str) {
if (provider->match_type == DEFINED && provider->match.string) {
match = 0;
}
}
/* we can't check for NULL in provider as that kills integer 0
* so we have to test each string/regexp case in the switch
*/
else {
switch (provider->match_type) {
case STRING_MATCH:
if (strcasecmp(str, provider->match.string)) {
match = 0;
}
break;
case STRING_CONTAINS:
str1 = apr_pstrdup(r->pool, str);
ap_str_tolower(str1);
if (!strstr(str1, provider->match.string)) {
match = 0;
}
break;
case REGEX_MATCH:
if (ap_regexec(provider->match.regex, str, 0, NULL, 0)
== AP_REG_NOMATCH) {
match = 0;
}
break;
case INT_EQ:
if (atoi(str) != provider->match.number) {
match = 0;
}
break;
/* Integer comparisons should be [var] OP [match]
* We need to set match = 0 if the condition fails
*/
case INT_LT:
if (atoi(str) >= provider->match.number) {
match = 0;
}
break;
case INT_LE:
if (atoi(str) > provider->match.number) {
match = 0;
}
break;
case INT_GT:
if (atoi(str) <= provider->match.number) {
match = 0;
}
break;
case INT_GE:
if (atoi(str) < provider->match.number) {
match = 0;
}
break;
case DEFINED: /* we already handled this:-) */
break;
}
}
if (match != provider->not) {
/* 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) {
str = apr_table_get(r->headers_out, "Cache-Control");
if (str) {
str1 = apr_pstrdup(r->pool, str);
ap_str_tolower(str1);
if (strstr(str1, "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_unset(r->headers_out, "Accept-Ranges");
}
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;
const char *cachecontrol;
char *str;
harness_ctx *ctx = f->ctx;
ap_filter_rec_t *filter = f->frec;
if (f->r->status != 200) {
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) {
str = apr_pstrdup(f->r->pool, cachecontrol);
ap_str_tolower(str);
if (strstr(str, "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);
}
}
/* 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->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;
}
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 *filter_provider(cmd_parms *cmd, void *CFG, const char *args)
{
mod_filter_cfg *cfg = CFG;
int flags;
ap_filter_provider_t *provider;
const char *rxend;
const char *c;
char *str;
const char *eq;
ap_filter_rec_t* frec;
ap_filter_rec_t* provider_frec;
/* insist on exactly four arguments */
const char *fname = ap_getword_conf(cmd->pool, &args) ;
const char *pname = ap_getword_conf(cmd->pool, &args) ;
const char *condition = ap_getword_conf(cmd->pool, &args) ;
const char *match = ap_getword_conf(cmd->pool, &args) ;
eq = ap_getword_conf(cmd->pool, &args) ;
if ( !*fname || !*pname || !*match || !*condition || *eq ) {
return "usage: FilterProvider filter provider condition match" ;
}
/* 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);
}
if (!frec) {
return apr_psprintf(cmd->pool, "Undeclared smart filter %s", fname);
}
/* 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);
}
provider = apr_palloc(cmd->pool, sizeof(ap_filter_provider_t));
if (*match == '!') {
provider->not = 1;
++match;
}
else {
provider->not = 0;
}
switch (*match++) {
case '<':
if (*match == '=') {
provider->match_type = INT_LE;
++match;
}
else {
provider->match_type = INT_LT;
}
provider->match.number = atoi(match);
break;
case '>':
if (*match == '=') {
provider->match_type = INT_GE;
++match;
}
else {
provider->match_type = INT_GT;
}
provider->match.number = atoi(match);
break;
case '=':
provider->match_type = INT_EQ;
provider->match.number = atoi(match);
break;
case '/':
provider->match_type = REGEX_MATCH;
rxend = ap_strchr_c(match, '/');
if (!rxend) {
return "Bad regexp syntax";
}
flags = AP_REG_NOSUB; /* we're not mod_rewrite:-) */
for (c = rxend+1; *c; ++c) {
switch (*c) {
case 'i': flags |= AP_REG_ICASE; break;
}
}
provider->match.regex = ap_pregcomp(cmd->pool,
apr_pstrndup(cmd->pool,
match,
rxend-match),
flags);
if (provider->match.regex == NULL) {
return "Bad regexp";
}
break;
case '*':
provider->match_type = DEFINED;
provider->match.number = -1;
break;
case '$':
provider->match_type = STRING_CONTAINS;
str = apr_pstrdup(cmd->pool, match);
ap_str_tolower(str);
provider->match.string = str;
break;
default:
provider->match_type = STRING_MATCH;
provider->match.string = apr_pstrdup(cmd->pool, match-1);
break;
}
provider->frec = provider_frec;
provider->next = frec->providers;
frec->providers = provider;
/* determine what a filter will dispatch this provider on */
eq = ap_strchr_c(condition, '=');
if (eq) {
str = apr_pstrdup(cmd->pool, eq+1);
if (!strncasecmp(condition, "env=", 4)) {
provider->dispatch = SUBPROCESS_ENV;
}
else if (!strncasecmp(condition, "req=", 4)) {
provider->dispatch = REQUEST_HEADERS;
}
else if (!strncasecmp(condition, "resp=", 5)) {
provider->dispatch = RESPONSE_HEADERS;
}
else {
return "FilterProvider: unrecognized dispatch table";
}
}
else {
if (!strcasecmp(condition, "handler")) {
provider->dispatch = HANDLER;
}
else {
provider->dispatch = RESPONSE_HEADERS;
}
str = apr_pstrdup(cmd->pool, condition);
ap_str_tolower(str);
}
if ( (provider->dispatch == RESPONSE_HEADERS)
&& !strcasecmp(str, "content-type")) {
provider->dispatch = CONTENT_TYPE;
}
provider->value = str;
return 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_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,
"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
}
return;
}
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]"),
/** we don't have a TAKE4, so we have to use RAW_ARGS */
AP_INIT_RAW_ARGS("FilterProvider", filter_provider, NULL, OR_OPTIONS,
"filter-name, provider-name, dispatch--criterion, dispatch-match"),
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,
"Debug level"),
#ifndef NO_PROTOCOL
AP_INIT_TAKE23("FilterProtocol", filter_protocol, NULL, OR_OPTIONS,
"filter-name [provider-name] protocol-args"),
#endif
{ NULL }
};
module AP_MODULE_DECLARE_DATA filter_module = {
STANDARD20_MODULE_STUFF,
filter_config,
filter_merge,
NULL,
NULL,
filter_cmds,
filter_hooks
};