/* * mod_limit_svn.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 limit_svn_module; typedef struct { const char *config_file; const char *base_path; svn_config_t *config; } limit_svn_config_rec; static void *create_limit_dir_config(apr_pool_t *pool, char *dir) { limit_svn_config_rec *cfg = apr_pcalloc(pool, sizeof(*cfg)); cfg->base_path = dir; return cfg; } static const command_rec limit_cmds[] = { AP_INIT_TAKE1("LimitSVNConfigFile", ap_set_file_slot, (void *) APR_OFFSETOF(limit_svn_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 } 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; /* The S:src-uri for an S:update-report request. */ svn_stringbuf_t *src_uri; /* Set to TRUE if the report has a S:dst-path element. */ svn_boolean_t has_dst_path; /* Set to TRUE if we hit the end of the REPORT body. */ svn_boolean_t done; limit_svn_config_rec *cfg; /* The current request. */ request_rec *r; } limit_svn_filter_ctx; typedef struct { svn_boolean_t *no_soup_for_you; const char *repos_path; } legal_checkout_baton_t; static svn_boolean_t is_legal_checkout(const char *path, const char *permitted, void *b, apr_pool_t *pool) { legal_checkout_baton_t *lcb = b; ap_log_perror(APLOG_MARK, APLOG_DEBUG, 0, pool, "checking path '%s' against repos_path '%s'", path, lcb->repos_path); if (strcmp(path, lcb->repos_path) == 0) { /* XXX Check permitted, if the user is a member of that group, let * them do it, otherwise don't. * * Of course, that implies that we actually have a group section, * which we don't. Yet. * * Of course, it's not clear if we can even do that, due to the * way authentication works. Sigh. Maybe the groups are lists * of IP addresses? */ *lcb->no_soup_for_you = 1; return FALSE; } else { return TRUE; } } static apr_status_t limit_filter(ap_filter_t *f, apr_bucket_brigade *bb, ap_input_mode_t mode, apr_read_type_e block, apr_off_t readbytes) { limit_svn_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_NONBLOCK_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; } else if (ctx->src_uri && ctx->has_dst_path) { ctx->let_it_go = TRUE; /* It's a diff, it gets a free pass. */ } /* If we get to the end and we've got a src uri that means it's an * update/checkout/export, not a diff or switch, so we need to see * if this is something we want to block. */ if ((last || ctx->done) && ctx->src_uri && ! ctx->let_it_go) { int trailing_slash; const char *cleaned_uri; const char *repos_name; const char *relative_path; dav_error *derr; char *uri; /* There can be trailing newlines after the URI... */ svn_stringbuf_strip_whitespace(ctx->src_uri); /* Ok, so we need to skip past the scheme, host, etc. */ uri = strstr(ctx->src_uri->data, "://"); if (uri) uri = strchr(uri + 3, '/'); if (uri) { legal_checkout_baton_t lcb; derr = dav_svn_split_uri(ctx->r, uri, ctx->cfg->base_path, &cleaned_uri, &trailing_slash, &repos_name, &relative_path, &lcb.repos_path); if (! derr) { int nchecks; if (! lcb.repos_path) lcb.repos_path = ""; lcb.repos_path = apr_psprintf(ctx->r->pool, "/%s", lcb.repos_path); lcb.no_soup_for_you = &ctx->no_soup_for_you; /* Loop over the urls that are not allowed for checkouts, * and see if any of them match our repos_path. */ nchecks = svn_config_enumerate2(ctx->cfg->config, "checkouts", is_legal_checkout, &lcb, ctx->r->pool); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "mod_limit_svn: checked %d paths", nchecks); } } /* At this point, if we're not going to block it, then we're done. */ if (! ctx->no_soup_for_you) ctx->let_it_go = TRUE; } /* Clean up our parser if we're done. */ if (last || ctx->let_it_go || ctx->no_soup_for_you) { XML_ParserFree(ctx->xmlp); ctx->xmlp = NULL; } /* 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) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, f->r, "mod_limit_svn: client broke the rules, " "returning error"); f->r->status = 403; f->r->status_line = "406 Forbidden, No Soup For You!"; return APR_EGENERAL; } else if (ctx->let_it_go) { ap_remove_input_filter(f); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r, "mod_limit_svn: letting request go through"); return rv; } } return rv; } static void cdata(void *baton, const char *data, int len) { limit_svn_filter_ctx *ctx = baton; switch (ctx->state) { case STATE_IN_SRC_PATH: if (! ctx->src_uri) ctx->src_uri = svn_stringbuf_ncreate(data, len, ctx->r->pool); else svn_stringbuf_appendbytes(ctx->src_uri, data, len); break; case STATE_IN_DST_PATH: ctx->has_dst_path = TRUE; break; default: break; } } static void start_element(void *baton, const char *name, const char **attrs) { limit_svn_filter_ctx *ctx = baton; /* XXX namespace handling needs to be fixed, right now this breaks down * on anything other than the default prefixes we use in Subversion. */ switch (ctx->state) { case STATE_BEGINNING: if (strcmp(name, "S:update-report") == 0) ctx->state = STATE_IN_UPDATE; /* XXX look for send-all attr? */ else ctx->let_it_go = TRUE; break; case STATE_IN_UPDATE: if (strcmp(name, "S:src-path") == 0) ctx->state = STATE_IN_SRC_PATH; else if (strcmp(name, "S:dst-path") == 0) ctx->state = STATE_IN_DST_PATH; break; default: break; } } static void end_element(void *baton, const char *name) { limit_svn_filter_ctx *ctx = baton; if (ctx->state == STATE_IN_SRC_PATH) ctx->state = STATE_IN_UPDATE; else if (ctx->state == STATE_IN_DST_PATH) ctx->state = STATE_IN_UPDATE; else if (ctx->state == STATE_IN_UPDATE && strcmp(name, "S:update-report") == 0) ctx->done = TRUE; } static void limit_insert_filters(request_rec *r) { limit_svn_config_rec *cfg = ap_get_module_config(r->per_dir_config, &limit_svn_module); if (! cfg->config_file) return; if (strcmp("REPORT", r->method) == 0) { limit_svn_filter_ctx *ctx = apr_pcalloc(r->pool, sizeof(*ctx)); svn_error_t *err; ctx->r = r; ctx->cfg = cfg; err = svn_config_read(&cfg->config, cfg->config_file, TRUE, r->pool); if (err) { ap_log_rerror(APLOG_MARK, APLOG_ERR, err->apr_err, r, "mod_limit_svn: Couldn't parse config file '%s'", cfg->config_file); svn_error_clear(err); return; } ctx->state = STATE_BEGINNING; ctx->xmlp = XML_ParserCreate(NULL); XML_SetUserData(ctx->xmlp, ctx); XML_SetElementHandler(ctx->xmlp, start_element, end_element); XML_SetCharacterDataHandler(ctx->xmlp, cdata); ap_add_input_filter("SVN_LIMIT_FILTER", ctx, r, r->connection); } } static void limit_register_hooks(apr_pool_t *pool) { ap_hook_insert_filter(limit_insert_filters, NULL, NULL, APR_HOOK_FIRST); ap_register_input_filter("SVN_LIMIT_FILTER", limit_filter, NULL, AP_FTYPE_RESOURCE); } module AP_MODULE_DECLARE_DATA limit_svn_module = { STANDARD20_MODULE_STUFF, create_limit_dir_config, NULL, NULL, NULL, limit_cmds, limit_register_hooks };