]> granicus.if.org Git - apache/commitdiff
mod_policy: Add a new testing module to help server administrators
authorGraham Leggett <minfrin@apache.org>
Wed, 21 Dec 2011 10:42:34 +0000 (10:42 +0000)
committerGraham Leggett <minfrin@apache.org>
Wed, 21 Dec 2011 10:42:34 +0000 (10:42 +0000)
enforce a configurable level of protocol compliance on their
servers and application servers behind theirs.

git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1221668 13f79535-47bb-0310-9956-ffa450edef68

CHANGES
NWGNUmakefile
build/installwinconf.awk
configure.in
docs/conf/extra/httpd-policy.conf.in [new file with mode: 0644]
docs/conf/httpd.conf.in
modules/test/config.m4
modules/test/mod_policy.c [new file with mode: 0644]

diff --git a/CHANGES b/CHANGES
index 12f0e895c7da3abd7b660716bfe836f799377682..457d9bdec6aad01d2abf07481027bdce76d04f3b 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -1,6 +1,10 @@
                                                          -*- coding: utf-8 -*-
 Changes with Apache 2.5.0
 
+  *) mod_policy: Add a new testing module to help server administrators
+     enforce a configurable level of protocol compliance on their
+     servers and application servers behind theirs. [Graham Leggett]
+
   *) mod_firehose: Add a new debugging module able to record traffic
      passing through the server in such a way that connections and/or
      requests be reconstructed and replayed. [Graham Leggett]
index b37ed825f828982a3bc591930658ed2a6fefd425..6724a89dcc357ac7a76f6a62bbbca19e28406b0b 100644 (file)
@@ -426,6 +426,7 @@ install :: nlms instscripts FORCE
        $(call MKCNF,docs/conf/extra/httpd-multilang-errordoc.conf.in, $(INSTALLBASE)/conf/extra/httpd-multilang-errordoc.conf)
        $(call MKCNF,docs/conf/extra/httpd-userdir.conf.in,            $(INSTALLBASE)/conf/extra/httpd-userdir.conf)
        $(call MKCNF,docs/conf/extra/httpd-vhosts.conf.in,             $(INSTALLBASE)/conf/extra/httpd-vhosts.conf)
+       $(call MKCNF,docs/conf/extra/httpd-policy.conf.in,             $(INSTALLBASE)/conf/extra/httpd-policy.conf)
        $(call MKCNF,docs/conf/extra/httpd-ssl.conf.in,                $(INSTALLBASE)/conf/extra/httpd-ssl.conf)
        $(call MKCNF,docs/conf/extra/proxy-html.conf.in,               $(INSTALLBASE)/conf/extra/proxy-html.conf)
        $(call COPYR,docs/docroot,                                     $(INSTALLBASE)/htdocs)
index 88cf46cf2d51aadf55ab33dacd34be79b8ea417a..f97aa6f9b866f98436ef3066e3e099612d4ac827 100644 (file)
@@ -63,6 +63,7 @@ BEGIN {
     filelist["httpd-manual.conf"] = "httpd-manual.conf.in";
     filelist["httpd-mpm.conf"] = "httpd-mpm.conf.in";
     filelist["httpd-multilang-errordoc.conf"] = "httpd-multilang-errordoc.conf.in";
+    filelist["httpd-policy.conf"] = "httpd-policy.conf.in";
     filelist["httpd-ssl.conf"] = "httpd-ssl.conf.in";
     filelist["httpd-userdir.conf"] = "httpd-userdir.conf.in";
     filelist["httpd-vhosts.conf"] = "httpd-vhosts.conf.in";
index 443711bdd0fd5a05b76b73b640c3c007b52becff..afd2fc91fb41c77ccee5c5a931b52b1aca096eed 100644 (file)
@@ -810,6 +810,6 @@ dnl Ensure that the httpd version is included
 HTTPD_VERSION=`$abs_srcdir/build/get-version.sh all $abs_srcdir/include/ap_release.h AP_SERVER`
 AC_SUBST(HTTPD_VERSION)
 
-AC_OUTPUT($APACHE_OUTPUT_FILES docs/conf/httpd.conf docs/conf/extra/httpd-autoindex.conf docs/conf/extra/httpd-dav.conf docs/conf/extra/httpd-default.conf docs/conf/extra/httpd-info.conf docs/conf/extra/httpd-languages.conf docs/conf/extra/httpd-manual.conf docs/conf/extra/httpd-mpm.conf docs/conf/extra/httpd-multilang-errordoc.conf docs/conf/extra/httpd-ssl.conf docs/conf/extra/httpd-userdir.conf docs/conf/extra/httpd-vhosts.conf docs/conf/extra/proxy-html.conf include/ap_config_layout.h support/apxs support/apachectl support/dbmmanage support/envvars-std support/log_server_status support/logresolve.pl support/phf_abuse_log.cgi support/split-logfile build/rules.mk build/pkg/pkginfo build/config_vars.sh,[true],[
+AC_OUTPUT($APACHE_OUTPUT_FILES docs/conf/httpd.conf docs/conf/extra/httpd-autoindex.conf docs/conf/extra/httpd-dav.conf docs/conf/extra/httpd-default.conf docs/conf/extra/httpd-info.conf docs/conf/extra/httpd-languages.conf docs/conf/extra/httpd-manual.conf docs/conf/extra/httpd-mpm.conf docs/conf/extra/httpd-multilang-errordoc.conf docs/conf/extra/httpd-policy.conf docs/conf/extra/httpd-ssl.conf docs/conf/extra/httpd-userdir.conf docs/conf/extra/httpd-vhosts.conf docs/conf/extra/proxy-html.conf include/ap_config_layout.h support/apxs support/apachectl support/dbmmanage support/envvars-std support/log_server_status support/logresolve.pl support/phf_abuse_log.cgi support/split-logfile build/rules.mk build/pkg/pkginfo build/config_vars.sh,[true],[
   APACHE_GEN_MAKEFILES
 ])
diff --git a/docs/conf/extra/httpd-policy.conf.in b/docs/conf/extra/httpd-policy.conf.in
new file mode 100644 (file)
index 0000000..966c82a
--- /dev/null
@@ -0,0 +1,61 @@
+
+#
+# Load the module if not already present
+<IfModule !mod_policy.c>
+  LoadModule policy_module modules/mod_policy.so
+</IfModule>
+
+#
+# Typical policy for static content.
+# Swap "enforce" for "log" to complain about violations rather
+# than failing.
+<Location />
+  SetOutputFilter POLICY_TYPE;POLICY_LENGTH;POLICY_KEEPALIVE;POLICY_VARY;POLICY_VALIDATION;POLICY_CONDITIONAL;POLICY_NOCACHE;POLICY_MAXAGE
+
+  # content type must be present and valid, but can be anything<br />
+  PolicyType enforce */*<br />
+
+  # reject if no explicitly declared content length<br />
+  PolicyLength enforce<br />
+
+  # covered by the policy length filter<br />
+  PolicyKeepalive ignore<br />
+
+  # reject if User-Agent appears within Vary headers<br />
+  PolicyVary enforce User-Agent<br />
+
+  # we want to enforce validation<br />
+  PolicyValidation enforce<br />
+
+  # non-functional conditional responses should be rejected<br />
+  PolicyConditional enforce<br />
+
+  # no-cache responses should be rejected<br />
+  PolicyNocache enforce<br />
+
+  # maxage must be at least a day<br />
+  PolicyMaxage enforce 86400<br />
+
+  # request version can be anything<br />
+  PolicyVersion ignore HTTP/1.1<br />
+
+  # define documentation links
+  PolicyConditionalURL http://httpd.apache.org/docs/trunk/compliance.html#policyconditional
+  PolicyLengthURL http://httpd.apache.org/docs/trunk/compliance.html#policylength
+  PolicyTypeURL http://httpd.apache.org/docs/trunk/compliance.html#policytype
+  PolicyKeepaliveURL http://httpd.apache.org/docs/trunk/compliance.html#policykeepalive
+  PolicyMaxageURL http://httpd.apache.org/docs/trunk/compliance.html#policymaxage
+  PolicyNocacheURL http://httpd.apache.org/docs/trunk/compliance.html#policynocache
+  PolicyValidationURL http://httpd.apache.org/docs/trunk/compliance.html#policyvalidation
+  PolicyVaryURL http://httpd.apache.org/docs/trunk/compliance.html#policyvary
+  PolicyVersionURL http://httpd.apache.org/docs/trunk/compliance.html#policyversion
+
+</Location>
+
+#
+# Server status can be bypassed
+<Location /server-status>
+  PolicyFilter off
+</Location>
+
+
index 6edb323cfd8bea50ba9ecf4685875d8e7394171f..643ee397061f254ef3cb643b68afe288eedfb63e 100644 (file)
@@ -389,6 +389,9 @@ LogLevel warn
 # Various default settings
 #Include @rel_sysconfdir@/extra/httpd-default.conf
 
+# Policy enforcement
+#Include @rel_sysconfdir@/extra/httpd-policy.conf
+
 # Configure mod_proxy_html to understand HTML4/XHTML1
 <IfModule proxy_html_module>
 Include @rel_sysconfdir@/extra/proxy-html.conf
index dfcd3218cdac5b3d95a03f758083a15f498dc87b..ef0b6d519ce402ff21639de40aa545df771c01fb 100644 (file)
@@ -8,6 +8,8 @@ APACHE_MODULE(optional_fn_export, example optional function exporter, , , no)
 
 APACHE_MODULE(dialup, rate limits static files to dialup modem speeds, , , )
 
+APACHE_MODULE(policy, HTTP protocol compliance filters, , , no)
+
 APR_ADDTO(INCLUDES, [-I\$(top_srcdir)/$modpath_current])
 
 APACHE_MODPATH_FINISH
diff --git a/modules/test/mod_policy.c b/modules/test/mod_policy.c
new file mode 100644 (file)
index 0000000..9018678
--- /dev/null
@@ -0,0 +1,1334 @@
+/* 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.
+ */
+
+/*
+ * Originally written @ BBC by Graham Leggett
+ * (C) 2011 British Broadcasting Corporation
+ */
+
+/*
+ * mod_policy.c --- Enforce specific policies on outgoing requests, logging
+ *                  or rejecting requests as appropriate.
+ *
+ * To enable, add the corresponding filters like so:
+ *
+ * SetOutputFilter POLICY_TYPE,POLICY_LENGTH
+ *
+ */
+
+#include "util_filter.h"
+#include "httpd.h"
+#include "http_config.h"
+#include "http_core.h"
+#include "http_log.h"
+#include "http_protocol.h"
+
+#include <apr_tables.h>
+#include <apr_strings.h>
+#include <apr_date.h>
+
+module AP_MODULE_DECLARE_DATA policy_module;
+
+#define DEFAULT_TYPE "*/*"
+
+typedef enum policy_result
+{
+    policy_ignore = 0, /* ignore this policy */
+    policy_log, /* log the violation as a warning, but let it through */
+    policy_enforce /* log the violation as an error, and decline */
+} policy_result;
+
+typedef struct policy_conf
+{
+    int policy; /* whether the filters should do anything at all */
+    int policy_set;
+    const char *environment; /* optional name of the subprocess environment variable that
+     * controls whether the policies are enforced.
+     */
+    const char *environment_log; /* value to trigger logging only */
+    const char *environment_ignore; /* value to suspend policy enforcement */
+    int environment_set;
+    policy_result type_action;
+    apr_array_header_t *type_matches; /* content type default patterns to match */
+    int type_set;
+    const char *type_url;
+    int type_url_set;
+    policy_result length_action;
+    int length_set;
+    const char *length_url;
+    int length_url_set;
+    policy_result keepalive_action;
+    int keepalive_set;
+    const char *keepalive_url;
+    int keepalive_url_set;
+    policy_result vary_action;
+    apr_array_header_t *vary_matches; /* Vary default patterns to match */
+    int vary_set;
+    const char *vary_url;
+    int vary_url_set;
+    policy_result validation_action;
+    int validation_set;
+    const char *validation_url;
+    int validation_url_set;
+    policy_result conditional_action;
+    int conditional_set;
+    const char *conditional_url;
+    int conditional_url_set;
+    policy_result nocache_action;
+    int nocache_set;
+    const char *nocache_url;
+    int nocache_url_set;
+    policy_result maxage_action;
+    apr_int64_t maxage;
+    int maxage_set;
+    const char *maxage_url;
+    int maxage_url_set;
+    policy_result version_action;
+    const char *version;
+    int version_num;
+    int version_set;
+    const char *version_url;
+    int version_url_set;
+} policy_conf;
+
+/**
+ * Does the value of a flagpole override the original value?
+ */
+static int check_enabled(request_rec *r, policy_conf *conf,
+        policy_result result)
+{
+    if (conf && !conf->policy) {
+        return policy_ignore;
+    }
+    if (conf && result != policy_ignore && conf->environment) {
+        const char *value = apr_table_get(r->subprocess_env, conf->environment);
+        if (value) {
+            /* downgrade enforce to log? */
+            if (conf->environment_log && !strcmp(value, conf->environment_log)) {
+                if (result == policy_enforce) {
+                    return policy_log;
+                }
+            }
+            /* downgrade enforce and log to ignore? */
+            else if (conf->environment_ignore && !strcmp(value,
+                    conf->environment_ignore)) {
+                return policy_ignore;
+            }
+        }
+    }
+    return result;
+}
+
+static void handle_policy(request_rec *r, policy_result result,
+        const char *message, const char *url, apr_bucket_brigade *bb,
+        int status)
+{
+    apr_bucket *e;
+
+    switch (result) {
+    case policy_log: {
+        ap_log_rerror(
+                APLOG_MARK,
+                APLOG_WARNING,
+                0,
+                r,
+                "mod_policy: violation: %s, uri: %s",
+                message, r->uri);
+        apr_table_addn(r->headers_out, "Warning", apr_psprintf(r->pool,
+                "299 %s \"%s\"", ap_get_server_name(r), message));
+        break;
+    }
+    case policy_enforce: {
+
+        ap_log_rerror(
+                APLOG_MARK,
+                APLOG_ERR,
+                0,
+                r,
+                "mod_policy: violation, rejecting request: %s, uri: %s",
+                message, r->uri);
+        apr_table_addn(r->err_headers_out, "Warning", apr_psprintf(r->pool,
+                "299 %s \"Rejected: %s\"", ap_get_server_name(r), message));
+        apr_table_setn(
+                r->notes, "error-notes",
+                    apr_pstrcat(r->pool, url ? apr_pstrcat(r->pool, "<a href=\"",
+                            url, "\">", NULL) : "", message, url ? "</a>" : "",
+                                    NULL));
+
+        apr_brigade_cleanup(bb);
+        e = ap_bucket_error_create(status, NULL, r->pool,
+                r->connection->bucket_alloc);
+        APR_BRIGADE_INSERT_TAIL(bb, e);
+        e = apr_bucket_eos_create(r->connection->bucket_alloc);
+        APR_BRIGADE_INSERT_TAIL(bb, e);
+
+    }
+    case policy_ignore: {
+    }
+    }
+
+}
+
+/**
+ * Policy for Content-Type.
+ *
+ * - It must be present.
+ * - It must match the optional regex (default .* / .*)
+ */
+static apr_status_t policy_type_out_filter(ap_filter_t *f,
+        apr_bucket_brigade *bb)
+{
+
+    policy_conf *conf = ap_get_module_config(f->r->per_dir_config,
+            &policy_module);
+    policy_result result = check_enabled(f->r, conf, conf->type_action);
+
+    if (result != policy_ignore) {
+        int fail = 1;
+
+        /* content type present and valid? */
+        if (f->r->content_type) {
+            const char *type = f->r->content_type;
+            const char *end = strchr(type, ';');
+            if (end) {
+                type = apr_pstrmemdup(f->r->pool, type, end - type);
+            }
+            if (!conf->type_matches) {
+                if (!ap_strcmp_match(type, DEFAULT_TYPE)) {
+                    fail = 0;
+                }
+            }
+            else {
+                int i;
+                for (i = 0; i < conf->type_matches->nelts; i++) {
+                    if (!ap_strcmp_match(type,
+                            ((char **) conf->type_matches->elts)[i])) {
+                        fail = 0;
+                        break;
+                    }
+                }
+            }
+        }
+
+        if (fail) {
+            const char *types = NULL;
+            if (conf->type_matches) {
+                int i;
+                for (i = 0; i < conf->type_matches->nelts; i++) {
+                    types = apr_pstrcat(f->r->pool, types ? ", " : "",
+                            ((char **) conf->type_matches->elts)[i], NULL);
+                }
+            }
+            else {
+                types = DEFAULT_TYPE;
+            }
+
+            handle_policy(
+                    f->r,
+                    result,
+                    apr_psprintf(
+                            f->r->pool,
+                            "Content-Type of '%s' should be RFC compliant and match one of: %s",
+                            f->r->content_type, types), conf->type_url, bb, HTTP_BAD_GATEWAY);
+
+        }
+
+    }
+
+    ap_remove_output_filter(f);
+    return ap_pass_brigade(f->next, bb);
+}
+
+/**
+ * Policy for Content-Length.
+ *
+ * - It must be present (missing, or Transfer-Encoding: chunked would be rejected)
+ * - Only applies to 2xx result codes
+ */
+static apr_status_t policy_length_out_filter(ap_filter_t *f,
+        apr_bucket_brigade *bb)
+{
+    request_rec *r = f->r;
+
+    policy_conf *conf = ap_get_module_config(r->per_dir_config,
+            &policy_module);
+    policy_result result = check_enabled(r, conf, conf->length_action);
+
+    if (result != policy_ignore && r->status >= 200 && r->status < 300
+            && r->status != HTTP_NO_CONTENT) {
+
+        if (!apr_table_get(r->headers_out, "Content-Length")) {
+
+            handle_policy(r, result, "Content-Length should be present",
+                    conf->length_url, bb, HTTP_BAD_GATEWAY);
+
+        }
+
+    }
+
+    ap_remove_output_filter(f);
+    return ap_pass_brigade(f->next, bb);
+}
+
+/**
+ * Policy for Content-Length / Chunked Encoding.
+ *
+ * We follow a subset of the algorithm httpd uses, which is:
+ *
+ *   IF  we have not marked this connection as errored;
+ *   and the client isn't expecting 100-continue (PR47087 - more
+ *       input here could be the client continuing when we're
+ *       closing the request).
+ *   and the response status does not require a close;
+ *   and the response body has a defined length due to the status code
+ *       being 304 or 204, the request method being HEAD, already
+ *       having defined Content-Length or Transfer-Encoding: chunked, or
+ *       the request version being HTTP/1.1 and thus capable of being set
+ *       as chunked
+ *   THEN  we support keepalive.
+ *
+ * Note: The server may choose to turn off keepalive for various reasons,
+ * such as an imminent shutdown, or a Connection: close from the client,
+ * but for our purposes we only care that keepalive was possible from
+ * the application, not that keepalive actually took place.
+ */
+static apr_status_t policy_keepalive_out_filter(ap_filter_t *f,
+        apr_bucket_brigade *bb)
+{
+    request_rec *r = f->r;
+
+    policy_conf *conf = ap_get_module_config(r->per_dir_config,
+            &policy_module);
+    policy_result result = check_enabled(r, conf, conf->keepalive_action);
+
+    if (result != policy_ignore && r->connection->keepalive != AP_CONN_CLOSE
+            && !r->expecting_100 && !ap_status_drops_connection(r->status)) {
+
+        if (!((r->status == HTTP_NOT_MODIFIED)
+                || (r->status == HTTP_NO_CONTENT)
+                || r->header_only
+                || apr_table_get(r->headers_out, "Content-Length")
+                || ap_find_last_token(r->pool, apr_table_get(r->headers_out,
+                        "Transfer-Encoding"), "chunked")
+                || r->proto_num >= HTTP_VERSION(1, 1))) {
+
+            handle_policy(r, result, "Keepalive should be possible (supply Content-Length or HTTP/1.1 Transfer-Encoding)",
+                    conf->keepalive_url, bb, HTTP_BAD_GATEWAY);
+
+        }
+
+    }
+
+    ap_remove_output_filter(f);
+    return ap_pass_brigade(f->next, bb);
+}
+
+static int vary_test(void *rec, const char *key, const char *value) {
+    request_rec *r = (request_rec *)rec;
+    char *token = apr_pstrdup(r->pool, value);
+    char *last;
+
+    policy_conf *conf = ap_get_module_config(r->per_dir_config,
+            &policy_module);
+
+    token = apr_strtok(token, ", \t", &last);
+    while (token) {
+        int i;
+        for (i = 0; i < conf->vary_matches->nelts; i++) {
+            if (!ap_strcasecmp_match(token,
+                    ((char **) conf->vary_matches->elts)[i])) {
+                return 0;
+            }
+        }
+
+        token = apr_strtok(NULL, ", \t", &last);
+    }
+    return 1;
+}
+
+/**
+ * Policy for Vary.
+ *
+ * - If an element matches the optional regex (no default), the request is rejected.
+ * Typically used to reject Varying on User-Agent.
+ */
+static apr_status_t policy_vary_out_filter(ap_filter_t *f,
+        apr_bucket_brigade *bb)
+{
+
+    policy_conf *conf = ap_get_module_config(f->r->per_dir_config,
+            &policy_module);
+    policy_result result = check_enabled(f->r, conf, conf->vary_action);
+
+    if (result != policy_ignore) {
+
+        /* Vary present and valid? */
+        if (!apr_table_do(vary_test, f->r, f->r->headers_out, "Vary", NULL)) {
+            const char *varys = NULL;
+            if (conf->vary_matches) {
+                int i;
+                for (i = 0; i < conf->vary_matches->nelts; i++) {
+                    varys = apr_pstrcat(f->r->pool, varys ? ", " : "",
+                            ((char **) conf->vary_matches->elts)[i], NULL);
+                }
+            }
+
+            handle_policy(f->r, result, apr_psprintf(f->r->pool,
+                    "Vary header(s) should NOT match any of: %s", varys),
+                    conf->vary_url, bb, HTTP_BAD_GATEWAY);
+
+        }
+
+    }
+
+    ap_remove_output_filter(f);
+    return ap_pass_brigade(f->next, bb);
+}
+
+/**
+ * Policy for Validation.
+ *
+ * Validation is possible through either the ETag or Last-Modified header, as described
+ * in http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.
+ *
+ * - Either must be present
+ * - Last-Modified, if present, must parse to a valid date
+ * - ETag, if present, must parse to a valid ETag.
+ */
+static apr_status_t policy_validation_out_filter(ap_filter_t *f,
+        apr_bucket_brigade *bb)
+{
+
+    policy_conf *conf = ap_get_module_config(f->r->per_dir_config,
+            &policy_module);
+    policy_result result = check_enabled(f->r, conf, conf->validation_action);
+
+    if (result != policy_ignore) {
+        int fail = 1, etagfail = 0, lmfail = 0;
+        const char *etag = apr_table_get(f->r->headers_out, "ETag");
+        const char *lastmodified = apr_table_get(f->r->headers_out,
+                "Last-Modified");
+
+        if (etag) {
+            int len = strlen(etag);
+            if (len > 1) {
+                if (etag[0] == '\"' && etag[len - 1] == '\"') {
+                    fail = 0;
+                }
+                else if (etag[0] == 'W' && etag[1] == '/' && etag[2] == '\"'
+                        && etag[len - 1] == '\"') {
+                    fail = 0;
+                }
+            }
+            if (fail) {
+                etagfail = 1;
+            }
+        }
+
+        if (lastmodified) {
+            apr_time_t lastmod = apr_date_parse_http(lastmodified);
+            if (lastmod != APR_DATE_BAD) {
+                fail = 0;
+            }
+            if (fail) {
+                lmfail = 1;
+            }
+        }
+
+        if (fail) {
+            const char *error = NULL;
+            if (!etag && !lastmodified) {
+                error = apr_psprintf(f->r->pool,
+                        "Etag and Last Modified missing");
+            }
+            else {
+                error = apr_pstrcat(f->r->pool,
+                        (etagfail ? "ETag syntax error (check quotes)" : ""),
+                        (etagfail && lmfail ? " / " : ""),
+                        (lmfail ? "Last-Modified could not be parsed" : ""),
+                        NULL);
+            }
+
+            handle_policy(f->r, result, error, conf->validation_url, bb,
+                    HTTP_BAD_GATEWAY);
+
+        }
+
+    }
+
+    ap_remove_output_filter(f);
+    return ap_pass_brigade(f->next, bb);
+}
+
+/**
+ * Policy for Revalidation through Conditional Requests.
+ *
+ * The If-None-Match header is described in
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26. Over and above the
+ * checks done in the validation filter, the conditional filter detects when an
+ * If-None-Match header is present in the request, an ETag is present in the response,
+ * and the response code is unexpected given the match. A result code is unexpected
+ * where a 304 Not Modified or 412 Precondition Failed was expected, but a 200 response
+ * was seen instead.
+ */
+static apr_status_t policy_conditional_out_filter(ap_filter_t *f,
+        apr_bucket_brigade *bb)
+{
+
+    policy_conf *conf = ap_get_module_config(f->r->per_dir_config,
+            &policy_module);
+    policy_result result = check_enabled(f->r, conf, conf->conditional_action);
+
+    if (result != policy_ignore) {
+        int code = ap_meets_conditions(f->r);
+
+        if (OK != code && code != f->r->status) {
+
+            handle_policy(
+                    f->r,
+                    result,
+                    apr_psprintf(
+                            f->r->pool,
+                            "Conditional request should have returned %d, instead returned %d",
+                            code, f->r->status), conf->conditional_url, bb,
+                    HTTP_BAD_GATEWAY);
+
+        }
+
+    }
+
+    ap_remove_output_filter(f);
+    return ap_pass_brigade(f->next, bb);
+}
+
+/**
+ * Policy for No-Cache Requests.
+ *
+ * If the Cache-Control and/or Pragma header specifies that the content is not
+ * cacheable, the request will be rejected.
+ *
+ * - If Cache-Control: no-cache
+ * - If Pragma: no-cache
+ * - If Cache-Control: no-store
+ * - If Cache-Control: private
+ */
+static apr_status_t policy_nocache_out_filter(ap_filter_t *f,
+        apr_bucket_brigade *bb)
+{
+
+    policy_conf *conf = ap_get_module_config(f->r->per_dir_config,
+            &policy_module);
+    policy_result result = check_enabled(f->r, conf, conf->nocache_action);
+
+    if (result != policy_ignore) {
+        request_rec *r = f->r;
+        const char *cc_header = apr_table_get(r->headers_out, "Cache-Control");
+        const char *pragma_header = apr_table_get(r->headers_out, "Pragma");
+        int fail = 0;
+        char *last;
+
+        if (pragma_header) {
+            char *header = apr_pstrdup(r->pool, pragma_header);
+            const char *token = apr_strtok(header, ", ", &last);
+            while (token) {
+                /* handle most common quickest case... */
+                if (!strcmp(token, "no-cache")) {
+                    fail = 1;
+                }
+                /* ...then try slowest case */
+                else if (!strcasecmp(token, "no-cache")) {
+                    fail = 1;
+                }
+                token = apr_strtok(NULL, ", ", &last);
+            }
+        }
+
+        if (cc_header) {
+            char *header = apr_pstrdup(r->pool, cc_header);
+            const char *token = apr_strtok(header, ", ", &last);
+            while (token) {
+                switch (token[0]) {
+                case 'n':
+                case 'N': {
+                    /* handle most common quickest cases... */
+                    if (!strcmp(token, "no-cache")) {
+                        fail = 1;
+                    }
+                    else if (!strcmp(token, "no-store")) {
+                        fail = 1;
+                    }
+                    /* ...then try slowest cases */
+                    else if (!strncasecmp(token, "no-cache", 8)) {
+                        if (token[8] == '=') {
+                        }
+                        else if (!token[8]) {
+                            fail = 1;
+                        }
+                        break;
+                    }
+                    else if (!strcasecmp(token, "no-store")) {
+                        fail = 1;
+                    }
+                    break;
+                }
+                case 'p':
+                case 'P': {
+                    /* handle most common quickest cases... */
+                    if (!strcmp(token, "private")) {
+                        fail = 1;
+                    }
+                    /* ...then try slowest cases */
+                    else if (!strncasecmp(token, "private", 7)) {
+                        if (token[7] == '=') {
+                        }
+                        else if (!token[7]) {
+                            fail = 1;
+                        }
+                        break;
+                    }
+                    break;
+                }
+                }
+                token = apr_strtok(NULL, ", ", &last);
+            }
+        }
+
+        if (fail) {
+
+            handle_policy(r, result, "Response is marked uncacheable",
+                    conf->nocache_url, bb, HTTP_BAD_GATEWAY);
+
+        }
+
+    }
+
+    ap_remove_output_filter(f);
+    return ap_pass_brigade(f->next, bb);
+}
+
+/**
+ * Policy for Maxage.
+ *
+ * If the effective maxage of the request is less than the parameter provided,
+ * the request will be rejected.
+ *
+ * - If Cache-Control: s-maxage is less than the limit
+ * - If Cache-Control: maxage is less than the limit
+ * - If Expires - Date is less than the limit
+ * - If none of the above, reject the request, as maxage is heuristic
+ *
+ * As soon as a test passes, we stop, as HTTP maxage handling follows a given
+ * set of priorities (s-maxage beats maxage, maxage beats Expires).
+ */
+static apr_status_t policy_maxage_out_filter(ap_filter_t *f,
+        apr_bucket_brigade *bb)
+{
+
+    policy_conf *conf = ap_get_module_config(f->r->per_dir_config,
+            &policy_module);
+    policy_result result = check_enabled(f->r, conf, conf->maxage_action);
+
+    if (result != policy_ignore) {
+        request_rec *r = f->r;
+        const char *cc_header;
+        const char *expires_header;
+        const char *date_header;
+        char *last;
+
+        int max_age = 0;
+        apr_int64_t max_age_value = 0;
+        int s_maxage = 0;
+        apr_int64_t s_maxage_value = 0;
+
+        /* parse Cache-Control */
+        cc_header = apr_table_get(r->headers_out, "Cache-Control");
+        if (cc_header) {
+            char *header = apr_pstrdup(r->pool, cc_header);
+            const char *token = apr_strtok(header, ", ", &last);
+            while (token) {
+                switch (token[0]) {
+                case 'm':
+                case 'M': {
+                    /* handle most common quickest cases... */
+                    if (!strncmp(token, "max-age", 7)) {
+                        max_age = 1;
+                        max_age_value = apr_atoi64(token + 8);
+                    }
+                    /* ...then try slowest cases */
+                    else if (!strncasecmp(token, "max-age", 7)) {
+                        if (token[7] == '=') {
+                            max_age = 1;
+                            max_age_value = apr_atoi64(token + 8);
+                        }
+                        break;
+                    }
+                    break;
+                }
+                case 's':
+                case 'S': {
+                    if (!strncmp(token, "s-maxage", 8)) {
+                        if (token[8] == '=') {
+                            s_maxage = 1;
+                            s_maxage_value = apr_atoi64(token + 9);
+                        }
+                        break;
+                    }
+                    else if (!strncasecmp(token, "s-maxage", 8)) {
+                        if (token[8] == '=') {
+                            s_maxage = 1;
+                            s_maxage_value = apr_atoi64(token + 9);
+                        }
+                        break;
+                    }
+                    break;
+                }
+                }
+                token = apr_strtok(NULL, ", ", &last);
+            }
+        }
+
+        /* test s-maxage, if present */
+        if (s_maxage) {
+            if (s_maxage_value < conf->maxage) {
+
+                handle_policy(
+                        f->r,
+                        result,
+                        apr_psprintf(
+                                f->r->pool,
+                                "Response s-maxage of %" APR_INT64_T_FMT " must be at least %" APR_INT64_T_FMT,
+                                s_maxage_value, conf->maxage), conf->maxage_url,
+                        bb, HTTP_BAD_GATEWAY);
+
+            }
+
+            /* decision is made, leave */
+            ap_remove_output_filter(f);
+            return ap_pass_brigade(f->next, bb);
+        }
+
+        /* test max-age, if present */
+        if (max_age) {
+            if (max_age_value < conf->maxage) {
+
+                handle_policy(
+                        f->r,
+                        result,
+                        apr_psprintf(
+                                f->r->pool,
+                                "Response max-age of %" APR_INT64_T_FMT " must be at least %" APR_INT64_T_FMT,
+                                max_age_value, conf->maxage), conf->maxage_url,
+                        bb, HTTP_BAD_GATEWAY);
+
+            }
+
+            /* decision is made, leave */
+            ap_remove_output_filter(f);
+            return ap_pass_brigade(f->next, bb);
+        }
+
+        /* test expires, if present */
+        expires_header = apr_table_get(r->headers_out, "Expires");
+        date_header = apr_table_get(r->headers_out, "Date");
+        if (expires_header && date_header) {
+            apr_time_t expires = apr_date_parse_http(expires_header);
+            apr_time_t date = apr_date_parse_http(date_header);
+            apr_int64_t fresh = apr_time_sec(expires - date);
+
+            if (expires == APR_DATE_BAD) {
+
+                handle_policy(
+                        f->r,
+                        result,
+                        apr_psprintf(
+                                f->r->pool,
+                                "Response Expires of '%s' is invalid, maxage %" APR_INT64_T_FMT " required",
+                                expires_header, conf->maxage),
+                        conf->maxage_url, bb, HTTP_BAD_GATEWAY);
+
+            }
+
+            else if (date == APR_DATE_BAD) {
+
+                handle_policy(
+                        f->r,
+                        result,
+                        apr_psprintf(
+                                f->r->pool,
+                                "Response Date of '%s' is invalid, maxage %" APR_INT64_T_FMT " required",
+                                date_header, conf->maxage),
+                        conf->maxage_url, bb, HTTP_BAD_GATEWAY);
+
+            }
+
+            else if (conf->maxage > 0 && fresh < conf->maxage) {
+
+                handle_policy(
+                        f->r,
+                        result,
+                        apr_psprintf(
+                                f->r->pool,
+                                "Response expires in %" APR_INT64_T_FMT " seconds, must be at least %" APR_INT64_T_FMT,
+                                fresh, conf->maxage),
+                        conf->maxage_url, bb, HTTP_BAD_GATEWAY);
+
+            }
+
+            /* decision is made, leave */
+            ap_remove_output_filter(f);
+            return ap_pass_brigade(f->next, bb);
+        }
+
+        /* no explicit maxage defined, so fail */
+        handle_policy(r, result, "Response has no explicit freshness lifetime (s-maxage, max-age or Expires/Date)",
+                conf->maxage_url, bb, HTTP_BAD_GATEWAY);
+
+    }
+
+    ap_remove_output_filter(f);
+    return ap_pass_brigade(f->next, bb);
+}
+
+static const char *version_string(int proto_num)
+{
+    switch (proto_num) {
+    case HTTP_VERSION(0, 9): {
+        return "HTTP/0.9";
+    }
+    case HTTP_VERSION(1, 0): {
+        return "HTTP/1.0";
+    }
+    case HTTP_VERSION(1, 1): {
+        return "HTTP/1.1";
+    }
+    default: {
+        return "(unknown)";
+    }
+    }
+}
+
+/**
+ * Policy for HTTP Version.
+ *
+ * - The HTTP version of the response must be at least the level specified.
+ */
+static apr_status_t policy_version_out_filter(ap_filter_t *f,
+        apr_bucket_brigade *bb)
+{
+    request_rec *r = f->r;
+
+    policy_conf *conf = ap_get_module_config(r->per_dir_config,
+            &policy_module);
+    policy_result result = check_enabled(r, conf, conf->version_action);
+
+    if (result != policy_ignore) {
+
+        if (r->proto_num > 0 && r->proto_num < conf->version_num) {
+
+            handle_policy(f->r, result, apr_psprintf(f->r->pool,
+                    "Request HTTP version '%s' should be at least '%s'",
+                    version_string(r->proto_num), conf->version),
+                    conf->version_url, bb, HTTP_VERSION_NOT_SUPPORTED);
+
+        }
+
+    }
+
+    ap_remove_output_filter(f);
+    return ap_pass_brigade(f->next, bb);
+}
+
+static void *create_policy_config(apr_pool_t *p, char *dummy)
+{
+    policy_conf *new = (policy_conf *) apr_pcalloc(p, sizeof(policy_conf));
+
+    new->policy = 1;
+    new->type_action = policy_log;
+    new->length_action = policy_log;
+    new->vary_action = policy_log;
+    new->validation_action = policy_log;
+    new->conditional_action = policy_log;
+    new->nocache_action = policy_log;
+    new->maxage_action = policy_log;
+    new->version_action = policy_log;
+    new->version_num = HTTP_VERSION(0, 9);
+    new->version = "HTTP/0.9";
+
+    return (void *) new;
+}
+
+static void *merge_policy_config(apr_pool_t *p, void *basev, void *addv)
+{
+    policy_conf *new = (policy_conf *) apr_pcalloc(p, sizeof(policy_conf));
+    policy_conf *add = (policy_conf *) addv;
+    policy_conf *base = (policy_conf *) basev;
+
+    new->policy = (add->policy_set == 0) ? base->policy : add->policy;
+    new->policy_set = add->policy_set || base->policy_set;
+    new->environment = (add->environment_set == 0) ? base->environment : add->environment;
+    new->environment_log = (add->environment_set == 0) ? base->environment_log
+            : add->environment_log;
+    new->environment_ignore = (add->environment_set == 0) ? base->environment_ignore
+            : add->environment_ignore;
+    new->environment_set = add->environment_set || base->environment_set;
+    new->type_action = (add->type_set == 0) ? base->type_action
+            : add->type_action;
+    new->type_matches = (add->type_set == 0) ? base->type_matches
+            : add->type_matches;
+    new->type_set = add->type_set || base->type_set;
+    new->type_url = (add->type_url_set == 0) ? base->type_url : add->type_url;
+    new->type_url_set = add->type_url_set || base->type_url_set;
+    new->length_action = (add->length_set == 0) ? base->length_action
+            : add->length_action;
+    new->length_set = add->length_set || base->length_set;
+    new->length_url = (add->length_url_set == 0) ? base->length_url
+            : add->length_url;
+    new->length_url_set = add->length_url_set || base->length_url_set;
+    new->vary_action = (add->vary_set == 0) ? base->vary_action
+            : add->vary_action;
+    new->vary_matches = (add->vary_set == 0) ? base->vary_matches
+            : add->vary_matches;
+    new->vary_set = add->vary_set || base->vary_set;
+    new->vary_url = (add->vary_url_set == 0) ? base->vary_url : add->vary_url;
+    new->vary_url_set = add->vary_url_set || base->vary_url_set;
+    new->validation_action
+            = (add->validation_set == 0) ? base->validation_action
+                    : add->validation_action;
+    new->validation_set = add->validation_set || base->validation_set;
+    new->validation_url = (add->validation_url_set == 0) ? base->validation_url
+            : add->validation_url;
+    new->validation_url_set = add->validation_url_set
+            || base->validation_url_set;
+    new->conditional_action
+            = (add->conditional_set == 0) ? base->conditional_action
+                    : add->conditional_action;
+    new->conditional_set = add->conditional_set || base->conditional_set;
+    new->conditional_url
+            = (add->conditional_url_set == 0) ? base->conditional_url
+                    : add->conditional_url;
+    new->conditional_url_set = add->conditional_url_set
+            || base->conditional_url_set;
+    new->nocache_action = (add->nocache_set == 0) ? base->nocache_action
+            : add->nocache_action;
+    new->nocache_set = add->nocache_set || base->nocache_set;
+    new->nocache_url = (add->nocache_url_set == 0) ? base->nocache_url
+            : add->nocache_url;
+    new->nocache_url_set = add->nocache_url_set || base->nocache_url_set;
+    new->maxage_action = (add->maxage_set == 0) ? base->maxage_action
+            : add->maxage_action;
+    new->maxage = (add->maxage_set == 0) ? base->maxage : add->maxage;
+    new->maxage_set = add->maxage_set || base->maxage_set;
+    new->maxage_url = (add->maxage_url_set == 0) ? base->maxage_url
+            : add->maxage_url;
+    new->maxage_url_set = add->maxage_url_set || base->maxage_url_set;
+    new->version_action = (add->version_set == 0) ? base->version_action
+            : add->version_action;
+    new->version = (add->version_set == 0) ? base->version : add->version;
+    new->version_num = (add->version_set == 0) ? base->version_num : add->version_num;
+    new->version_set = add->version_set || base->version_set;
+    new->version_url = (add->version_url_set == 0) ? base->version_url
+            : add->version_url;
+    new->version_url_set = add->version_url_set || base->version_url_set;
+
+
+    return new;
+}
+
+static const char *parse_action(apr_pool_t *pool, const char *action,
+        policy_result *result)
+{
+    if (!strcmp(action, "enforce")) {
+        *result = policy_enforce;
+    }
+    else if (!strcmp(action, "log")) {
+        *result = policy_log;
+    }
+    else if (!strcmp(action, "ignore")) {
+        *result = policy_ignore;
+    }
+    else {
+        return apr_psprintf(pool,
+                "'%s' must be one of 'enforce, 'log' or 'ignore'.", action);
+    }
+    return NULL;
+}
+
+static const char *set_policy(cmd_parms *cmd, void *dconf, int flag)
+{
+    policy_conf *conf = dconf;
+
+    conf->policy = flag;
+    conf->policy_set = 1;
+
+    return NULL;
+}
+
+static const char *set_environment(cmd_parms *cmd, void *dconf,
+        const char *environment, const char *log, const char *ignore)
+{
+    policy_conf *conf = dconf;
+
+    conf->environment = environment;
+    conf->environment_log = log;
+    conf->environment_ignore = ignore;
+    conf->environment_set = 1;
+
+    return NULL;
+}
+
+static const char *set_type(cmd_parms *cmd, void *dconf, const char *action,
+        const char *type)
+{
+    policy_conf *conf = dconf;
+
+    if (type) {
+        const char **match_ptr;
+        if (!conf->type_matches) {
+            conf->type_matches = apr_array_make(cmd->pool, 2,
+                    sizeof(const char *));
+        }
+        match_ptr = apr_array_push(conf->type_matches);
+        *match_ptr = type;
+    }
+    conf->type_set = 1;
+
+    return parse_action(cmd->pool, action, &conf->type_action);
+}
+
+static const char *set_type_url(cmd_parms *cmd, void *dconf, const char *url)
+{
+    policy_conf *conf = dconf;
+
+    conf->type_url = url;
+    conf->type_url_set = 1;
+
+    return NULL;
+}
+
+static const char *set_length(cmd_parms *cmd, void *dconf, const char *action)
+{
+    policy_conf *conf = dconf;
+
+    conf->length_set = 1;
+
+    return parse_action(cmd->pool, action, &conf->length_action);
+}
+
+static const char *set_length_url(cmd_parms *cmd, void *dconf, const char *url)
+{
+    policy_conf *conf = dconf;
+
+    conf->length_url = url;
+    conf->length_url_set = 1;
+
+    return NULL;
+}
+
+static const char *set_keepalive(cmd_parms *cmd, void *dconf, const char *action)
+{
+    policy_conf *conf = dconf;
+
+    conf->keepalive_set = 1;
+
+    return parse_action(cmd->pool, action, &conf->keepalive_action);
+}
+
+static const char *set_keepalive_url(cmd_parms *cmd, void *dconf, const char *url)
+{
+    policy_conf *conf = dconf;
+
+    conf->keepalive_url = url;
+    conf->keepalive_url_set = 1;
+
+    return NULL;
+}
+
+static const char *set_vary(cmd_parms *cmd, void *dconf, const char *action,
+        const char *vary)
+{
+    policy_conf *conf = dconf;
+
+    if (vary) {
+        const char **match_ptr;
+        if (!conf->vary_matches) {
+            conf->vary_matches = apr_array_make(cmd->pool, 2,
+                    sizeof(const char *));
+        }
+        match_ptr = apr_array_push(conf->vary_matches);
+        *match_ptr = vary;
+    }
+    conf->vary_set = 1;
+
+    return parse_action(cmd->pool, action, &conf->vary_action);
+}
+
+static const char *set_vary_url(cmd_parms *cmd, void *dconf, const char *url)
+{
+    policy_conf *conf = dconf;
+
+    conf->vary_url = url;
+    conf->vary_url_set = 1;
+
+    return NULL;
+}
+
+static const char *set_validation(cmd_parms *cmd, void *dconf,
+        const char *action)
+{
+    policy_conf *conf = dconf;
+
+    conf->validation_set = 1;
+
+    return parse_action(cmd->pool, action, &conf->validation_action);
+}
+
+static const char *set_validation_url(cmd_parms *cmd, void *dconf,
+        const char *url)
+{
+    policy_conf *conf = dconf;
+
+    conf->validation_url = url;
+    conf->validation_url_set = 1;
+
+    return NULL;
+}
+
+static const char *set_conditional(cmd_parms *cmd, void *dconf,
+        const char *action)
+{
+    policy_conf *conf = dconf;
+
+    conf->conditional_set = 1;
+
+    return parse_action(cmd->pool, action, &conf->conditional_action);
+}
+
+static const char *set_conditional_url(cmd_parms *cmd, void *dconf,
+        const char *url)
+{
+    policy_conf *conf = dconf;
+
+    conf->conditional_url = url;
+    conf->conditional_url_set = 1;
+
+    return NULL;
+}
+
+static const char *set_nocache(cmd_parms *cmd, void *dconf, const char *action)
+{
+    policy_conf *conf = dconf;
+
+    conf->nocache_set = 1;
+
+    return parse_action(cmd->pool, action, &conf->nocache_action);
+}
+
+static const char *set_nocache_url(cmd_parms *cmd, void *dconf, const char *url)
+{
+    policy_conf *conf = dconf;
+
+    conf->nocache_url = url;
+    conf->nocache_url_set = 1;
+
+    return NULL;
+}
+
+static const char *set_maxage(cmd_parms *cmd, void *dconf, const char *action, const char *maxage)
+{
+    policy_conf *conf = dconf;
+
+    conf->maxage_set = 1;
+    conf->maxage = apr_atoi64(maxage);
+    if (conf->maxage < 0) {
+        return apr_psprintf(cmd->pool,
+                "'%s' must be a positive integer.", maxage);
+    }
+
+    return parse_action(cmd->pool, action, &conf->maxage_action);
+}
+
+static const char *set_maxage_url(cmd_parms *cmd, void *dconf, const char *url)
+{
+    policy_conf *conf = dconf;
+
+    conf->maxage_url = url;
+    conf->maxage_url_set = 1;
+
+    return NULL;
+}
+
+static const char *set_version(cmd_parms *cmd, void *dconf, const char *action, const char *version)
+{
+    policy_conf *conf = dconf;
+
+    conf->version_set = 1;
+
+    if (!strcmp(version, "HTTP/1.1")) {
+        conf->version = "HTTP/1.1";
+        conf->version_num = HTTP_VERSION(1, 1);
+    }
+    else if (!strcmp(version, "HTTP/1.0")) {
+        conf->version = "HTTP/1.0";
+        conf->version_num = HTTP_VERSION(1, 0);
+    }
+    else if (!strcmp(version, "HTTP/0.9")) {
+        conf->version = "HTTP/0.9";
+        conf->version_num = HTTP_VERSION(0, 9);
+    }
+    else {
+        return apr_psprintf(cmd->pool,
+                "'%s' must be one of 'HTTP/1.1', 'HTTP/1.0' or 'HTTP/0.9'.", version);
+    }
+
+    return parse_action(cmd->pool, action, &conf->version_action);
+}
+
+static const char *set_version_url(cmd_parms *cmd, void *dconf, const char *url)
+{
+    policy_conf *conf = dconf;
+
+    conf->version_url = url;
+    conf->version_url_set = 1;
+
+    return NULL;
+}
+
+static const command_rec
+        policy_cmds[] =
+        {
+                AP_INIT_FLAG("PolicyFilter", set_policy, NULL, RSRC_CONF
+                        | ACCESS_CONF,
+                        "Whether policies should be applied. Defaults to 'on'."),
+                AP_INIT_TAKE1(
+                        "PolicyConditional",
+                        set_conditional,
+                        NULL,
+                        RSRC_CONF | ACCESS_CONF,
+                        "Action to take (enforce, ignore, log) if a conditional request was not honoured. Defaults to 'log'."),
+                AP_INIT_TAKE1("PolicyConditionalURL", set_conditional_url,
+                        NULL, RSRC_CONF | ACCESS_CONF,
+                        "URL describing the conditional request policy."),
+                AP_INIT_TAKE3(
+                        "PolicyEnvironment",
+                        set_environment,
+                        NULL,
+                        RSRC_CONF | ACCESS_CONF,
+                        "Environment variable to control policy enforcement, followed by the variable value for logging only, and the value for policy suspension."),
+                AP_INIT_TAKE1(
+                        "PolicyLength",
+                        set_length,
+                        NULL,
+                        RSRC_CONF | ACCESS_CONF,
+                        "Action to take (enforce, ignore, log) if Content-Length missing. Defaults to 'log'."),
+                AP_INIT_TAKE1("PolicyLengthURL", set_length_url, NULL,
+                        RSRC_CONF | ACCESS_CONF,
+                        "URL describing the content length policy."),
+                AP_INIT_TAKE1(
+                        "PolicyKeepalive",
+                        set_keepalive,
+                        NULL,
+                        RSRC_CONF | ACCESS_CONF,
+                        "Action to take (enforce, ignore, log) if keepalive is not possible. Defaults to 'log'."),
+                AP_INIT_TAKE1("PolicyKeepaliveURL", set_keepalive_url, NULL,
+                        RSRC_CONF | ACCESS_CONF,
+                        "URL describing the keepalive policy."),
+                AP_INIT_ITERATE2(
+                        "PolicyType",
+                        set_type,
+                        NULL,
+                        RSRC_CONF | ACCESS_CONF,
+                        "Action to take (enforce, ignore, log), followed by one or more valid content types containing optional wildcards ? and *"),
+                AP_INIT_TAKE1("PolicyTypeURL", set_type_url, NULL, RSRC_CONF
+                        | ACCESS_CONF,
+                        "URL describing the content type policy."),
+                AP_INIT_ITERATE2(
+                        "PolicyVary",
+                        set_vary,
+                        NULL,
+                        RSRC_CONF | ACCESS_CONF,
+                        "Action to take (enforce, ignore, log), followed by one or more headers containing optional wildcards ? and * that are NOT to appear in a Vary header"),
+                AP_INIT_TAKE1("PolicyVaryURL", set_vary_url, NULL, RSRC_CONF
+                        | ACCESS_CONF,
+                        "URL describing the vary header policy."),
+                AP_INIT_TAKE1(
+                        "PolicyValidation",
+                        set_validation,
+                        NULL,
+                        RSRC_CONF | ACCESS_CONF,
+                        "Action to take (enforce, ignore, log) if Last-Modified or Etag is missing or invalid. Defaults to 'log'."),
+                AP_INIT_TAKE1("PolicyValidationURL", set_validation_url, NULL,
+                        RSRC_CONF | ACCESS_CONF,
+                        "URL describing the content validation policy."),
+                AP_INIT_TAKE1(
+                        "PolicyNocache",
+                        set_nocache,
+                        NULL,
+                        RSRC_CONF | ACCESS_CONF,
+                        "Action to take (enforce, ignore, log) if a response is not cacheable. Defaults to 'log'."),
+                AP_INIT_TAKE1("PolicyNocacheURL", set_nocache_url, NULL,
+                        RSRC_CONF | ACCESS_CONF,
+                        "URL describing the no cache policy."),
+                AP_INIT_TAKE2(
+                        "PolicyMaxage",
+                        set_maxage,
+                        NULL,
+                        RSRC_CONF | ACCESS_CONF,
+                        "Action to take (enforce, ignore, log) if a response has an effective maxage less than the age provided. Defaults to 'log'."),
+                AP_INIT_TAKE1("PolicyMaxageURL", set_maxage_url, NULL,
+                        RSRC_CONF | ACCESS_CONF,
+                        "URL describing the maxage policy."),
+                AP_INIT_TAKE2(
+                        "PolicyVersion",
+                        set_version,
+                        NULL,
+                        RSRC_CONF | ACCESS_CONF,
+                        "Action to take (enforce, ignore, log) if a response has an HTTP version less than the version provided. Defaults to 'log HTTP/0.9'."),
+                AP_INIT_TAKE1("PolicyVersionURL", set_version_url, NULL,
+                        RSRC_CONF | ACCESS_CONF,
+                        "URL describing the version policy."),
+                { NULL } };
+
+static void register_hooks(apr_pool_t *p)
+{
+    ap_register_output_filter("POLICY_TYPE", policy_type_out_filter, NULL,
+            AP_FTYPE_CONTENT_SET + 5);
+    ap_register_output_filter("POLICY_LENGTH", policy_length_out_filter, NULL,
+            AP_FTYPE_CONTENT_SET + 5);
+    ap_register_output_filter("POLICY_KEEPALIVE", policy_keepalive_out_filter, NULL,
+            AP_FTYPE_CONTENT_SET + 5);
+    ap_register_output_filter("POLICY_VARY", policy_vary_out_filter, NULL,
+            AP_FTYPE_CONTENT_SET + 5);
+    ap_register_output_filter("POLICY_VALIDATION",
+            policy_validation_out_filter, NULL, AP_FTYPE_CONTENT_SET + 5);
+    ap_register_output_filter("POLICY_CONDITIONAL",
+            policy_conditional_out_filter, NULL, AP_FTYPE_CONTENT_SET + 5);
+    ap_register_output_filter("POLICY_NOCACHE", policy_nocache_out_filter,
+            NULL, AP_FTYPE_CONTENT_SET + 5);
+    ap_register_output_filter("POLICY_MAXAGE", policy_maxage_out_filter,
+            NULL, AP_FTYPE_CONTENT_SET + 5);
+    ap_register_output_filter("POLICY_VERSION", policy_version_out_filter,
+            NULL, AP_FTYPE_CONTENT_SET + 5);
+}
+
+AP_DECLARE_MODULE(policy) =
+{
+    STANDARD20_MODULE_STUFF, create_policy_config, /* create per-directory config structure */
+    merge_policy_config, /* merge per-directory config structures */
+    NULL, /* create per-server config structure */
+    NULL, /* merge per-server config structures */
+    policy_cmds, /* command apr_table_t */
+    register_hooks /* register hooks */
+};