blob: dc63c0a3f174afca2393bc66d636f579721bf210 [file] [log] [blame]
/*
* mod_dontdothat.c: an Apache filter that allows you to return arbitrary
* errors for various types of Subversion requests.
*
* ====================================================================
* 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.
* ====================================================================
*/
#include <httpd.h>
#include <http_config.h>
#include <http_protocol.h>
#include <http_request.h>
#include <http_log.h>
#include <util_filter.h>
#include <ap_config.h>
#include <apr_strings.h>
#include <apr_uri.h>
#include "mod_dav_svn.h"
#include "svn_string.h"
#include "svn_config.h"
#include "svn_path.h"
#include "svn_xml.h"
#include "private/svn_fspath.h"
extern module AP_MODULE_DECLARE_DATA dontdothat_module;
typedef struct dontdothat_config_rec {
const char *config_file;
const char *base_path;
int no_replay;
} dontdothat_config_rec;
static void *create_dontdothat_dir_config(apr_pool_t *pool, char *dir)
{
dontdothat_config_rec *cfg = apr_pcalloc(pool, sizeof(*cfg));
cfg->base_path = dir;
cfg->no_replay = 1;
return cfg;
}
static const command_rec dontdothat_cmds[] =
{
AP_INIT_TAKE1("DontDoThatConfigFile", ap_set_file_slot,
(void *) APR_OFFSETOF(dontdothat_config_rec, config_file),
OR_ALL,
"Text file containing actions to take for specific requests"),
AP_INIT_FLAG("DontDoThatDisallowReplay", ap_set_flag_slot,
(void *) APR_OFFSETOF(dontdothat_config_rec, no_replay),
OR_ALL, "Disallow replay requests as if they are other recursive requests."),
{ NULL }
};
typedef enum parse_state_t {
STATE_BEGINNING,
STATE_IN_UPDATE,
STATE_IN_SRC_PATH,
STATE_IN_DST_PATH,
STATE_IN_RECURSIVE
} parse_state_t;
typedef struct dontdothat_filter_ctx {
/* Set to TRUE when we determine that the request is safe and should be
* allowed to continue. */
svn_boolean_t let_it_go;
/* Set to TRUE when we determine that the request is unsafe and should be
* stopped in its tracks. */
svn_boolean_t no_soup_for_you;
svn_xml_parser_t *xmlp;
/* The current location in the REPORT body. */
parse_state_t state;
/* A buffer to hold CDATA we encounter. */
svn_stringbuf_t *buffer;
dontdothat_config_rec *cfg;
/* An array of wildcards that are special cased to be allowed. */
apr_array_header_t *allow_recursive_ops;
/* An array of wildcards where recursive operations are not allowed. */
apr_array_header_t *no_recursive_ops;
/* TRUE if a path has failed a test already. */
svn_boolean_t path_failed;
/* An error for when we're using this as a baton while parsing config
* files. */
svn_error_t *err;
/* The current request. */
request_rec *r;
} dontdothat_filter_ctx;
/* Return TRUE if wildcard WC matches path P, FALSE otherwise. */
static svn_boolean_t
matches(const char *wc, const char *p)
{
for (;;)
{
switch (*wc)
{
case '*':
if (wc[1] != '/' && wc[1] != '\0')
abort(); /* This was checked for during parsing of the config. */
/* It's a wild card, so eat up until the next / in p. */
while (*p && p[1] != '/')
++p;
/* If we ran out of p and we're out of wc then it matched. */
if (! *p)
{
if (wc[1] == '\0')
return TRUE;
else
return FALSE;
}
break;
case '\0':
if (*p != '\0')
/* This means we hit the end of wc without running out of p. */
return FALSE;
else
/* Or they were exactly the same length, so it's not lower. */
return TRUE;
default:
if (*wc != *p)
return FALSE; /* If we don't match, then move on to the next
* case. */
else
break;
}
++wc;
++p;
if (! *p && *wc)
return FALSE;
}
}
/* duplicate of dav_svn__log_err() from mod_dav_svn/util.c */
static void
log_dav_err(request_rec *r,
dav_error *err,
int level)
{
dav_error *errscan;
/* Log the errors */
/* ### should have a directive to log the first or all */
for (errscan = err; errscan != NULL; errscan = errscan->prev) {
apr_status_t status;
if (errscan->desc == NULL)
continue;
#if AP_MODULE_MAGIC_AT_LEAST(20091119,0)
status = errscan->aprerr;
#else
status = errscan->save_errno;
#endif
ap_log_rerror(APLOG_MARK, level, status, r,
"%s [%d, #%d]",
errscan->desc, errscan->status, errscan->error_id);
}
}
static svn_boolean_t
is_this_legal(dontdothat_filter_ctx *ctx, const char *uri)
{
const char *relative_path;
const char *cleaned_uri;
const char *repos_name;
const char *uri_path;
int trailing_slash;
dav_error *derr;
/* uri can be an absolute uri or just a path, we only want the path to match
* against */
if (uri && svn_path_is_url(uri))
{
apr_uri_t parsed_uri;
apr_status_t rv = apr_uri_parse(ctx->r->pool, uri, &parsed_uri);
if (APR_SUCCESS != rv)
{
/* Error parsing the URI, log and reject request. */
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, ctx->r,
"mod_dontdothat: blocked request after failing "
"to parse uri: '%s'", uri);
return FALSE;
}
uri_path = parsed_uri.path;
}
else
{
uri_path = uri;
}
if (uri_path)
{
const char *repos_path;
derr = dav_svn_split_uri(ctx->r,
uri_path,
ctx->cfg->base_path,
&cleaned_uri,
&trailing_slash,
&repos_name,
&relative_path,
&repos_path);
if (! derr)
{
int idx;
if (! repos_path)
repos_path = "";
repos_path = svn_fspath__canonicalize(repos_path, ctx->r->pool);
/* First check the special cases that are always legal... */
for (idx = 0; idx < ctx->allow_recursive_ops->nelts; ++idx)
{
const char *wc = APR_ARRAY_IDX(ctx->allow_recursive_ops,
idx,
const char *);
if (matches(wc, repos_path))
{
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->r,
"mod_dontdothat: rule %s allows %s",
wc, repos_path);
return TRUE;
}
}
/* Then look for stuff we explicitly don't allow. */
for (idx = 0; idx < ctx->no_recursive_ops->nelts; ++idx)
{
const char *wc = APR_ARRAY_IDX(ctx->no_recursive_ops,
idx,
const char *);
if (matches(wc, repos_path))
{
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->r,
"mod_dontdothat: rule %s forbids %s",
wc, repos_path);
return FALSE;
}
}
}
else
{
log_dav_err(ctx->r, derr, APLOG_ERR);
return FALSE;
}
}
else
{
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, ctx->r,
"mod_dontdothat: empty uri passed to is_this_legal(), "
"module bug?");
return FALSE;
}
return TRUE;
}
static apr_status_t
dontdothat_filter(ap_filter_t *f,
apr_bucket_brigade *bb,
ap_input_mode_t mode,
apr_read_type_e block,
apr_off_t readbytes)
{
dontdothat_filter_ctx *ctx = f->ctx;
apr_status_t rv;
apr_bucket *e;
if (mode != AP_MODE_READBYTES)
return ap_get_brigade(f->next, bb, mode, block, readbytes);
rv = ap_get_brigade(f->next, bb, mode, block, readbytes);
if (rv)
return rv;
for (e = APR_BRIGADE_FIRST(bb);
e != APR_BRIGADE_SENTINEL(bb);
e = APR_BUCKET_NEXT(e))
{
svn_boolean_t last = APR_BUCKET_IS_EOS(e);
const char *str;
apr_size_t len;
svn_error_t *err;
if (last)
{
str = "";
len = 0;
}
else
{
rv = apr_bucket_read(e, &str, &len, APR_BLOCK_READ);
if (rv)
return rv;
}
err = svn_xml_parse(ctx->xmlp, str, len, last);
if (err)
{
/* let_it_go so we clean up our parser, no_soup_for_you so that we
* bail out before bothering to parse this stuff a second time. */
ctx->let_it_go = TRUE;
ctx->no_soup_for_you = TRUE;
svn_error_clear(err);
}
/* If we found something that isn't allowed, set the correct status
* and return an error so it'll bail out before it gets anywhere it
* can do real damage. */
if (ctx->no_soup_for_you)
{
/* XXX maybe set up the SVN-ACTION env var so that it'll show up
* in the Subversion operational logs? */
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r,
"mod_dontdothat: client broke the rules, "
"returning error");
/* Ok, pass an error bucket and an eos bucket back to the client.
*
* NOTE: The custom error string passed here doesn't seem to be
* used anywhere by httpd. This is quite possibly a bug.
*
* TODO: Try and pass back a custom document body containing a
* serialized svn_error_t so the client displays a better
* error message. */
bb = apr_brigade_create(f->r->pool, f->c->bucket_alloc);
e = ap_bucket_error_create(403, "No Soup For You!",
f->r->pool, f->c->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(bb, e);
e = apr_bucket_eos_create(f->c->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(bb, e);
/* Don't forget to remove us, otherwise recursion blows the stack. */
ap_remove_input_filter(f);
return ap_pass_brigade(f->r->output_filters, bb);
}
else if (ctx->let_it_go || last)
{
ap_remove_input_filter(f);
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r,
"mod_dontdothat: letting request go through");
return rv;
}
}
return rv;
}
/* Implements svn_xml_char_data callback */
static void
cdata(void *baton, const char *data, apr_size_t len)
{
dontdothat_filter_ctx *ctx = baton;
if (ctx->no_soup_for_you || ctx->let_it_go)
return;
switch (ctx->state)
{
case STATE_IN_SRC_PATH:
/* FALLTHROUGH */
case STATE_IN_DST_PATH:
/* FALLTHROUGH */
case STATE_IN_RECURSIVE:
if (! ctx->buffer)
ctx->buffer = svn_stringbuf_ncreate(data, len, ctx->r->pool);
else
svn_stringbuf_appendbytes(ctx->buffer, data, len);
break;
default:
break;
}
}
/* Implements svn_xml_start_elem callback */
static void
start_element(void *baton, const char *name, const char **attrs)
{
dontdothat_filter_ctx *ctx = baton;
const char *sep;
if (ctx->no_soup_for_you || ctx->let_it_go)
return;
/* XXX Hack. We should be doing real namespace support, but for now we
* just skip ahead of any namespace prefix. If someone's sending us
* an update-report element outside of the SVN namespace they'll get
* what they deserve... */
sep = ap_strchr_c(name, ':');
if (sep)
name = sep + 1;
switch (ctx->state)
{
case STATE_BEGINNING:
if (strcmp(name, "update-report") == 0)
ctx->state = STATE_IN_UPDATE;
else if (strcmp(name, "replay-report") == 0 && ctx->cfg->no_replay)
{
/* XXX it would be useful if there was a way to override this
* on a per-user basis... */
if (! is_this_legal(ctx, ctx->r->unparsed_uri))
ctx->no_soup_for_you = TRUE;
else
ctx->let_it_go = TRUE;
}
else
ctx->let_it_go = TRUE;
break;
case STATE_IN_UPDATE:
if (strcmp(name, "src-path") == 0)
{
ctx->state = STATE_IN_SRC_PATH;
if (ctx->buffer)
ctx->buffer->len = 0;
}
else if (strcmp(name, "dst-path") == 0)
{
ctx->state = STATE_IN_DST_PATH;
if (ctx->buffer)
ctx->buffer->len = 0;
}
else if (strcmp(name, "recursive") == 0)
{
ctx->state = STATE_IN_RECURSIVE;
if (ctx->buffer)
ctx->buffer->len = 0;
}
else
; /* XXX Figure out what else we need to deal with... Switch
* has that link-path thing we probably need to look out
* for... */
break;
default:
break;
}
}
/* Implements svn_xml_end_elem callback */
static void
end_element(void *baton, const char *name)
{
dontdothat_filter_ctx *ctx = baton;
const char *sep;
if (ctx->no_soup_for_you || ctx->let_it_go)
return;
/* XXX Hack. We should be doing real namespace support, but for now we
* just skip ahead of any namespace prefix. If someone's sending us
* an update-report element outside of the SVN namespace they'll get
* what they deserve... */
sep = ap_strchr_c(name, ':');
if (sep)
name = sep + 1;
switch (ctx->state)
{
case STATE_IN_SRC_PATH:
ctx->state = STATE_IN_UPDATE;
svn_stringbuf_strip_whitespace(ctx->buffer);
if (! ctx->path_failed && ! is_this_legal(ctx, ctx->buffer->data))
ctx->path_failed = TRUE;
break;
case STATE_IN_DST_PATH:
ctx->state = STATE_IN_UPDATE;
svn_stringbuf_strip_whitespace(ctx->buffer);
if (! ctx->path_failed && ! is_this_legal(ctx, ctx->buffer->data))
ctx->path_failed = TRUE;
break;
case STATE_IN_RECURSIVE:
ctx->state = STATE_IN_UPDATE;
svn_stringbuf_strip_whitespace(ctx->buffer);
/* If this isn't recursive we let it go. */
if (strcmp(ctx->buffer->data, "no") == 0)
{
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->r,
"mod_dontdothat: letting nonrecursive request go");
ctx->let_it_go = TRUE;
}
break;
case STATE_IN_UPDATE:
if (strcmp(name, "update-report") == 0)
{
/* If we made it here without figuring out that this is
* nonrecursive, then the path check is our final word
* on the subject. */
if (ctx->path_failed)
ctx->no_soup_for_you = TRUE;
else
ctx->let_it_go = TRUE;
}
else
; /* XXX Is there other stuff we care about? */
break;
default:
abort();
}
}
static svn_boolean_t
is_valid_wildcard(const char *wc)
{
while (*wc)
{
if (*wc == '*')
{
if (wc[1] && wc[1] != '/')
return FALSE;
}
++wc;
}
return TRUE;
}
static svn_boolean_t
config_enumerator(const char *wildcard,
const char *action,
void *baton,
apr_pool_t *pool)
{
dontdothat_filter_ctx *ctx = baton;
if (strcmp(action, "deny") == 0)
{
if (is_valid_wildcard(wildcard))
APR_ARRAY_PUSH(ctx->no_recursive_ops, const char *) = wildcard;
else
ctx->err = svn_error_createf(APR_EINVAL,
NULL,
"'%s' is an invalid wildcard",
wildcard);
}
else if (strcmp(action, "allow") == 0)
{
if (is_valid_wildcard(wildcard))
APR_ARRAY_PUSH(ctx->allow_recursive_ops, const char *) = wildcard;
else
ctx->err = svn_error_createf(APR_EINVAL,
NULL,
"'%s' is an invalid wildcard",
wildcard);
}
else
{
ctx->err = svn_error_createf(APR_EINVAL,
NULL,
"'%s' is not a valid action",
action);
}
if (ctx->err)
return FALSE;
else
return TRUE;
}
static void
dontdothat_insert_filters(request_rec *r)
{
dontdothat_config_rec *cfg = ap_get_module_config(r->per_dir_config,
&dontdothat_module);
if (! cfg->config_file)
return;
if (strcmp("REPORT", r->method) == 0)
{
dontdothat_filter_ctx *ctx = apr_pcalloc(r->pool, sizeof(*ctx));
svn_config_t *config;
svn_error_t *err;
ctx->r = r;
ctx->cfg = cfg;
ctx->allow_recursive_ops = apr_array_make(r->pool, 5, sizeof(char *));
ctx->no_recursive_ops = apr_array_make(r->pool, 5, sizeof(char *));
/* XXX is there a way to error out from this point? Would be nice... */
err = svn_config_read3(&config, cfg->config_file, TRUE,
FALSE, TRUE, r->pool);
if (err)
{
char buff[256];
ap_log_rerror(APLOG_MARK, APLOG_ERR,
((err->apr_err >= APR_OS_START_USERERR &&
err->apr_err < APR_OS_START_CANONERR) ?
0 : err->apr_err),
r, "Failed to load DontDoThatConfigFile: %s",
svn_err_best_message(err, buff, sizeof(buff)));
svn_error_clear(err);
return;
}
svn_config_enumerate2(config,
"recursive-actions",
config_enumerator,
ctx,
r->pool);
if (ctx->err)
{
char buff[256];
ap_log_rerror(APLOG_MARK, APLOG_ERR,
((ctx->err->apr_err >= APR_OS_START_USERERR &&
ctx->err->apr_err < APR_OS_START_CANONERR) ?
0 : ctx->err->apr_err),
r, "Failed to parse DontDoThatConfigFile: %s",
svn_err_best_message(ctx->err, buff, sizeof(buff)));
svn_error_clear(ctx->err);
return;
}
ctx->state = STATE_BEGINNING;
ctx->xmlp = svn_xml_make_parser(ctx, start_element, end_element,
cdata, r->pool);
ap_add_input_filter("DONTDOTHAT_FILTER", ctx, r, r->connection);
}
}
static void
dontdothat_register_hooks(apr_pool_t *pool)
{
ap_hook_insert_filter(dontdothat_insert_filters, NULL, NULL, APR_HOOK_FIRST);
ap_register_input_filter("DONTDOTHAT_FILTER",
dontdothat_filter,
NULL,
AP_FTYPE_RESOURCE);
}
module AP_MODULE_DECLARE_DATA dontdothat_module =
{
STANDARD20_MODULE_STUFF,
create_dontdothat_dir_config,
NULL,
NULL,
NULL,
dontdothat_cmds,
dontdothat_register_hooks
};