mod_policy.c revision d65e488b299ce82e10802fe9d921c89b45eebfb6
/* 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
*
*
* 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"
#define POLICY_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 */
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;
int type_set;
const char *type_url;
int type_url_set;
int length_set;
const char *length_url;
int length_url_set;
int keepalive_set;
const char *keepalive_url;
int keepalive_url_set;
int vary_set;
const char *vary_url;
int vary_url_set;
int validation_set;
const char *validation_url;
int validation_url_set;
int conditional_set;
const char *conditional_url;
int conditional_url_set;
int nocache_set;
const char *nocache_url;
int nocache_url_set;
int maxage_set;
const char *maxage_url;
int maxage_url_set;
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?
*/
{
return policy_ignore;
}
if (value) {
/* downgrade enforce to log? */
if (result == policy_enforce) {
return policy_log;
}
}
/* downgrade enforce and log to ignore? */
conf->environment_ignore)) {
return policy_ignore;
}
}
}
return result;
}
int status)
{
apr_bucket *e;
switch (result) {
case policy_log: {
0,
r,
"mod_policy: violation: %s, uri: %s",
break;
}
case policy_enforce: {
0,
r,
"mod_policy: violation, rejecting request: %s, uri: %s",
: message);
r->connection->bucket_alloc);
}
case policy_ignore: {
}
}
}
/**
* Policy for Content-Type.
*
* - It must be present.
* - It must match the optional regex (default .* / .*)
*/
{
if (result != policy_ignore) {
int fail = 1;
/* content type present and valid? */
if (f->r->content_type) {
const char *type = f->r->content_type;
if (end) {
}
if (!conf->type_matches) {
fail = 0;
}
}
else {
int i;
if (!ap_strcmp_match(type,
fail = 0;
break;
}
}
}
}
if (fail) {
if (conf->type_matches) {
int i;
}
}
else {
}
f->r,
f->r->pool,
"Content-Type of '%s' should be RFC compliant and match one of: %s",
}
}
}
/**
* Policy for Content-Length.
*
* - It must be present (missing, or Transfer-Encoding: chunked would be rejected)
* - Only applies to 2xx result codes
*/
{
request_rec *r = f->r;
&& r->status != HTTP_NO_CONTENT) {
}
}
}
/**
* 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.
*/
{
request_rec *r = f->r;
if (!((r->status == HTTP_NOT_MODIFIED)
|| (r->status == HTTP_NO_CONTENT)
|| r->header_only
"Transfer-Encoding"), "chunked")
handle_policy(r, result, "Keepalive should be possible (supply Content-Length or HTTP/1.1 Transfer-Encoding)",
}
}
}
char *last;
while (token) {
int i;
if (!ap_strcasecmp_match(token,
return 0;
}
}
}
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.
*/
{
if (result != policy_ignore) {
/* Vary present and valid? */
if (conf->vary_matches) {
int i;
}
}
"Vary header(s) should NOT match any of: %s", varys),
}
}
}
/**
* 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.
*/
{
if (result != policy_ignore) {
"Last-Modified");
if (etag) {
if (len > 1) {
fail = 0;
}
fail = 0;
}
}
if (fail) {
etagfail = 1;
}
}
if (lastmodified) {
if (lastmod != APR_DATE_BAD) {
fail = 0;
}
if (fail) {
lmfail = 1;
}
}
if (fail) {
if (!etag && !lastmodified) {
"Etag and Last Modified missing");
}
else {
NULL);
}
}
}
}
/**
* 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.
*/
{
if (result != policy_ignore) {
int code = ap_meets_conditions(f->r);
f->r,
f->r->pool,
"Conditional request should have returned %d, instead returned %d",
}
}
}
/**
* Policy for No-Cache Requests.
*
* cacheable, the request will be rejected.
*
* - If Cache-Control: no-cache
* - If Pragma: no-cache
* - If Cache-Control: no-store
* - If Cache-Control: private
*/
{
if (result != policy_ignore) {
request_rec *r = f->r;
int fail = 0;
char *last;
if (pragma_header) {
while (token) {
/* handle most common quickest case... */
fail = 1;
}
/* ...then try slowest case */
fail = 1;
}
}
}
if (cc_header) {
while (token) {
switch (token[0]) {
case 'n':
case 'N': {
/* handle most common quickest cases... */
fail = 1;
}
fail = 1;
}
/* ...then try slowest cases */
}
else if (!token[8]) {
fail = 1;
}
break;
}
fail = 1;
}
break;
}
case 'p':
case 'P': {
/* handle most common quickest cases... */
fail = 1;
}
/* ...then try slowest cases */
}
else if (!token[7]) {
fail = 1;
}
break;
}
break;
}
}
}
}
if (fail) {
}
}
}
/**
* 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).
*/
{
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;
/* parse Cache-Control */
if (cc_header) {
while (token) {
switch (token[0]) {
case 'm':
case 'M': {
/* handle most common quickest cases... */
max_age = 1;
}
/* ...then try slowest cases */
max_age = 1;
}
break;
}
break;
}
case 's':
case 'S': {
s_maxage = 1;
}
break;
}
s_maxage = 1;
}
break;
}
break;
}
}
}
}
/* test s-maxage, if present */
if (s_maxage) {
f->r,
f->r->pool,
}
/* decision is made, leave */
}
/* test max-age, if present */
if (max_age) {
f->r,
f->r->pool,
}
/* decision is made, leave */
}
/* test expires, if present */
if (expires_header && date_header) {
if (expires == APR_DATE_BAD) {
f->r,
f->r->pool,
}
else if (date == APR_DATE_BAD) {
f->r,
f->r->pool,
}
f->r,
f->r->pool,
}
/* decision is made, leave */
}
/* no explicit maxage defined, so fail */
handle_policy(r, result, "Response has no explicit freshness lifetime (s-maxage, max-age or Expires/Date)",
}
}
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";
}
return "HTTP/1.1";
}
default: {
return "(unknown)";
}
}
}
/**
* Policy for HTTP Version.
*
* - The HTTP version of the response must be at least the level specified.
*/
{
request_rec *r = f->r;
if (result != policy_ignore) {
"Request HTTP version '%s' should be at least '%s'",
}
}
}
{
return (void *) new;
}
{
: add->environment_log;
: add->type_action;
: add->type_matches;
: add->length_action;
: add->length_url;
: add->vary_action;
: add->vary_matches;
: add->validation_action;
: add->validation_url;
|| base->validation_url_set;
: add->conditional_url;
|| base->conditional_url_set;
: add->nocache_action;
: add->nocache_url;
: add->maxage_action;
: add->maxage_url;
: add->version_action;
: add->version_url;
return new;
}
{
*result = policy_enforce;
}
*result = policy_log;
}
*result = policy_ignore;
}
else {
return apr_psprintf(pool,
"'%s' must be one of 'enforce, 'log' or 'ignore'.", action);
}
return NULL;
}
{
return NULL;
}
{
return NULL;
}
const char *type)
{
if (type) {
const char **match_ptr;
if (!conf->type_matches) {
sizeof(const char *));
}
}
}
{
/* url is only used inside <a href="...">, escape accordingly */
return NULL;
}
{
}
{
return NULL;
}
{
}
{
return NULL;
}
const char *vary)
{
if (vary) {
const char **match_ptr;
if (!conf->vary_matches) {
sizeof(const char *));
}
}
}
{
return NULL;
}
const char *action)
{
}
const char *url)
{
return NULL;
}
const char *action)
{
}
const char *url)
{
return NULL;
}
{
}
{
return NULL;
}
{
"'%s' must be a positive integer.", maxage);
}
}
{
return NULL;
}
static const char *set_version(cmd_parms *cmd, void *dconf, const char *action, const char *version)
{
}
}
}
else {
"'%s' must be one of 'HTTP/1.1', 'HTTP/1.0' or 'HTTP/0.9'.", version);
}
}
{
return NULL;
}
static const command_rec
policy_cmds[] =
{
| ACCESS_CONF,
"Whether policies should be applied. Defaults to 'on'."),
"PolicyConditional",
NULL,
"Action to take (enforce, ignore, log) if a conditional request was not honoured. Defaults to 'log'."),
"URL describing the conditional request policy."),
"PolicyEnvironment",
NULL,
"Environment variable to control policy enforcement, followed by the variable value for logging only, and the value for policy suspension."),
"PolicyLength",
NULL,
"Action to take (enforce, ignore, log) if Content-Length missing. Defaults to 'log'."),
"URL describing the content length policy."),
"PolicyKeepalive",
NULL,
"Action to take (enforce, ignore, log) if keepalive is not possible. Defaults to 'log'."),
"URL describing the keepalive policy."),
"PolicyType",
NULL,
"Action to take (enforce, ignore, log), followed by one or more valid content types containing optional wildcards ? and *"),
| ACCESS_CONF,
"URL describing the content type policy."),
"PolicyVary",
NULL,
"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"),
| ACCESS_CONF,
"URL describing the vary header policy."),
"PolicyValidation",
NULL,
"Action to take (enforce, ignore, log) if Last-Modified or Etag is missing or invalid. Defaults to 'log'."),
"URL describing the content validation policy."),
"PolicyNocache",
NULL,
"Action to take (enforce, ignore, log) if a response is not cacheable. Defaults to 'log'."),
"URL describing the no cache policy."),
"PolicyMaxage",
NULL,
"Action to take (enforce, ignore, log) if a response has an effective maxage less than the age provided. Defaults to 'log'."),
"URL describing the maxage policy."),
"PolicyVersion",
NULL,
"Action to take (enforce, ignore, log) if a response has an HTTP version less than the version provided. Defaults to 'log HTTP/0.9'."),
"URL describing the version policy."),
{ NULL } };
static void register_hooks(apr_pool_t *p)
{
AP_FTYPE_CONTENT_SET + 5);
AP_FTYPE_CONTENT_SET + 5);
AP_FTYPE_CONTENT_SET + 5);
AP_FTYPE_CONTENT_SET + 5);
ap_register_output_filter("POLICY_VALIDATION",
ap_register_output_filter("POLICY_CONDITIONAL",
}
{
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 */
};