/* * mod_dontdothat.c: an Apache filter that allows you to return arbitrary * errors for various types of Subversion requests. * * ==================================================================== * Copyright (c) 2006 CollabNet. All rights reserved. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms * are also available at http://subversion.tigris.org/license-1.html. * If newer versions of this license are posted there, you may use a * newer version instead, at your option. * * This software consists of voluntary contributions made by many * individuals. For exact contribution history, see the revision * history and logs, available at http://subversion.tigris.org/. * ==================================================================== */ #include #include #include #include #include #include #include #include #include "mod_dav_svn.h" #include "svn_string.h" #include "svn_config.h" module AP_MODULE_DECLARE_DATA dontdothat_module; typedef struct { const char *config_file; const char *base_path; } 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; 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"), { NULL } }; typedef enum { STATE_BEGINNING, STATE_IN_UPDATE, STATE_IN_SRC_PATH, STATE_IN_DST_PATH, STATE_IN_RECURSIVE } parse_state_t; typedef struct { /* 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; XML_Parser 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; } } 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; int trailing_slash; dav_error *derr; /* Ok, so we need to skip past the scheme, host, etc. */ uri = ap_strstr_c(uri, "://"); if (uri) uri = ap_strchr_c(uri + 3, '/'); if (uri) { const char *repos_path; derr = dav_svn_split_uri(ctx->r, uri, 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 = apr_psprintf(ctx->r->pool, "/%s", repos_path); /* 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; } } } } 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; if (last) { str = ""; len = 0; } else { rv = apr_bucket_read(e, &str, &len, APR_BLOCK_READ); if (rv) return rv; } if (! XML_Parse(ctx->xmlp, str, len, last)) { /* 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; } /* 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"); f->r->status = 403; f->r->status_line = "403 Forbidden, No Soup For You!"; return APR_EGENERAL; } 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; } static void cdata(void *baton, const char *data, int 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; } } 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) { /* 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; } } 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 apr_status_t clean_up_parser(void *baton) { XML_Parser xmlp = baton; XML_ParserFree(xmlp); return APR_SUCCESS; } 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_read(&config, cfg->config_file, 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 = XML_ParserCreate(NULL); apr_pool_cleanup_register(r->pool, ctx->xmlp, clean_up_parser, NULL); XML_SetUserData(ctx->xmlp, ctx); XML_SetElementHandler(ctx->xmlp, start_element, end_element); XML_SetCharacterDataHandler(ctx->xmlp, cdata); 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 };