Follow-up to r29036: Now that the "mergeinfo" transaction file is no
[svn.git] / contrib / server-side / mod_dontdothat / mod_dontdothat.c
blob1fd4bb3d4daf69377d7d24b5ef45c6aa34043982
1 /*
2 * mod_dontdothat.c: an Apache filter that allows you to return arbitrary
3 * errors for various types of Subversion requests.
5 * ====================================================================
6 * Copyright (c) 2006 CollabNet. All rights reserved.
8 * This software is licensed as described in the file COPYING, which
9 * you should have received as part of this distribution. The terms
10 * are also available at http://subversion.tigris.org/license-1.html.
11 * If newer versions of this license are posted there, you may use a
12 * newer version instead, at your option.
14 * This software consists of voluntary contributions made by many
15 * individuals. For exact contribution history, see the revision
16 * history and logs, available at http://subversion.tigris.org/.
17 * ====================================================================
20 #include <httpd.h>
21 #include <http_config.h>
22 #include <http_protocol.h>
23 #include <http_request.h>
24 #include <http_log.h>
25 #include <util_filter.h>
26 #include <ap_config.h>
27 #include <apr_strings.h>
29 #include <expat.h>
31 #include "mod_dav_svn.h"
32 #include "svn_string.h"
33 #include "svn_config.h"
35 module AP_MODULE_DECLARE_DATA dontdothat_module;
37 typedef struct {
38 const char *config_file;
39 const char *base_path;
40 } dontdothat_config_rec;
42 static void *create_dontdothat_dir_config(apr_pool_t *pool, char *dir)
44 dontdothat_config_rec *cfg = apr_pcalloc(pool, sizeof(*cfg));
46 cfg->base_path = dir;
48 return cfg;
51 static const command_rec dontdothat_cmds[] =
53 AP_INIT_TAKE1("DontDoThatConfigFile", ap_set_file_slot,
54 (void *) APR_OFFSETOF(dontdothat_config_rec, config_file),
55 OR_ALL,
56 "Text file containing actions to take for specific requests"),
57 { NULL }
60 typedef enum {
61 STATE_BEGINNING,
62 STATE_IN_UPDATE,
63 STATE_IN_SRC_PATH,
64 STATE_IN_DST_PATH,
65 STATE_IN_RECURSIVE
66 } parse_state_t;
68 typedef struct {
69 /* Set to TRUE when we determine that the request is safe and should be
70 * allowed to continue. */
71 svn_boolean_t let_it_go;
73 /* Set to TRUE when we determine that the request is unsafe and should be
74 * stopped in its tracks. */
75 svn_boolean_t no_soup_for_you;
77 XML_Parser xmlp;
79 /* The current location in the REPORT body. */
80 parse_state_t state;
82 /* A buffer to hold CDATA we encounter. */
83 svn_stringbuf_t *buffer;
85 dontdothat_config_rec *cfg;
87 /* An array of wildcards that are special cased to be allowed. */
88 apr_array_header_t *allow_recursive_ops;
90 /* An array of wildcards where recursive operations are not allowed. */
91 apr_array_header_t *no_recursive_ops;
93 /* TRUE if a path has failed a test already. */
94 svn_boolean_t path_failed;
96 /* An error for when we're using this as a baton while parsing config
97 * files. */
98 svn_error_t *err;
100 /* The current request. */
101 request_rec *r;
102 } dontdothat_filter_ctx;
104 /* Return TRUE if wildcard WC matches path P, FALSE otherwise. */
105 static svn_boolean_t
106 matches(const char *wc, const char *p)
108 for (;;)
110 switch (*wc)
112 case '*':
113 if (wc[1] != '/' && wc[1] != '\0')
114 abort(); /* This was checked for during parsing of the config. */
116 /* It's a wild card, so eat up until the next / in p. */
117 while (*p && p[1] != '/')
118 ++p;
120 /* If we ran out of p and we're out of wc then it matched. */
121 if (! *p)
123 if (wc[1] == '\0')
124 return TRUE;
125 else
126 return FALSE;
128 break;
130 case '\0':
131 if (*p != '\0')
132 /* This means we hit the end of wc without running out of p. */
133 return FALSE;
134 else
135 /* Or they were exactly the same length, so it's not lower. */
136 return TRUE;
138 default:
139 if (*wc != *p)
140 return FALSE; /* If we don't match, then move on to the next
141 * case. */
142 else
143 break;
146 ++wc;
147 ++p;
149 if (! *p && *wc)
150 return FALSE;
154 static svn_boolean_t
155 is_this_legal(dontdothat_filter_ctx *ctx, const char *uri)
157 const char *relative_path;
158 const char *cleaned_uri;
159 const char *repos_name;
160 int trailing_slash;
161 dav_error *derr;
163 /* Ok, so we need to skip past the scheme, host, etc. */
164 uri = ap_strstr_c(uri, "://");
165 if (uri)
166 uri = ap_strchr_c(uri + 3, '/');
168 if (uri)
170 const char *repos_path;
172 derr = dav_svn_split_uri(ctx->r,
173 uri,
174 ctx->cfg->base_path,
175 &cleaned_uri,
176 &trailing_slash,
177 &repos_name,
178 &relative_path,
179 &repos_path);
180 if (! derr)
182 int idx;
184 if (! repos_path)
185 repos_path = "";
187 repos_path = apr_psprintf(ctx->r->pool, "/%s", repos_path);
189 /* First check the special cases that are always legal... */
190 for (idx = 0; idx < ctx->allow_recursive_ops->nelts; ++idx)
192 const char *wc = APR_ARRAY_IDX(ctx->allow_recursive_ops,
193 idx,
194 const char *);
196 if (matches(wc, repos_path))
198 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->r,
199 "mod_dontdothat: rule %s allows %s",
200 wc, repos_path);
201 return TRUE;
205 /* Then look for stuff we explicitly don't allow. */
206 for (idx = 0; idx < ctx->no_recursive_ops->nelts; ++idx)
208 const char *wc = APR_ARRAY_IDX(ctx->no_recursive_ops,
209 idx,
210 const char *);
212 if (matches(wc, repos_path))
214 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->r,
215 "mod_dontdothat: rule %s forbids %s",
216 wc, repos_path);
217 return FALSE;
223 return TRUE;
226 static apr_status_t
227 dontdothat_filter(ap_filter_t *f,
228 apr_bucket_brigade *bb,
229 ap_input_mode_t mode,
230 apr_read_type_e block,
231 apr_off_t readbytes)
233 dontdothat_filter_ctx *ctx = f->ctx;
234 apr_status_t rv;
235 apr_bucket *e;
237 if (mode != AP_MODE_READBYTES)
238 return ap_get_brigade(f->next, bb, mode, block, readbytes);
240 rv = ap_get_brigade(f->next, bb, mode, block, readbytes);
241 if (rv)
242 return rv;
244 for (e = APR_BRIGADE_FIRST(bb);
245 e != APR_BRIGADE_SENTINEL(bb);
246 e = APR_BUCKET_NEXT(e))
248 svn_boolean_t last = APR_BUCKET_IS_EOS(e);
249 const char *str;
250 apr_size_t len;
252 if (last)
254 str = "";
255 len = 0;
257 else
259 rv = apr_bucket_read(e, &str, &len, APR_BLOCK_READ);
260 if (rv)
261 return rv;
264 if (! XML_Parse(ctx->xmlp, str, len, last))
266 /* let_it_go so we clean up our parser, no_soup_for_you so that we
267 * bail out before bothering to parse this stuff a second time. */
268 ctx->let_it_go = TRUE;
269 ctx->no_soup_for_you = TRUE;
272 /* If we found something that isn't allowed, set the correct status
273 * and return an error so it'll bail out before it gets anywhere it
274 * can do real damage. */
275 if (ctx->no_soup_for_you)
277 /* XXX maybe set up the SVN-ACTION env var so that it'll show up
278 * in the Subversion operational logs? */
280 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r,
281 "mod_dontdothat: client broke the rules, "
282 "returning error");
284 /* Ok, pass an error bucket and an eos bucket back to the client.
286 * NOTE: The custom error string passed here doesn't seem to be
287 * used anywhere by httpd. This is quite possibly a bug.
289 * TODO: Try and pass back a custom document body containing a
290 * serialized svn_error_t so the client displays a better
291 * error message. */
292 bb = apr_brigade_create(f->r->pool, f->c->bucket_alloc);
293 e = ap_bucket_error_create(403, "No Soup For You!",
294 f->r->pool, f->c->bucket_alloc);
295 APR_BRIGADE_INSERT_TAIL(bb, e);
296 e = apr_bucket_eos_create(f->c->bucket_alloc);
297 APR_BRIGADE_INSERT_TAIL(bb, e);
299 /* Don't forget to remove us, otherwise recursion blows the stack. */
300 ap_remove_input_filter(f);
302 return ap_pass_brigade(f->r->output_filters, bb);
304 else if (ctx->let_it_go || last)
306 ap_remove_input_filter(f);
308 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r,
309 "mod_dontdothat: letting request go through");
311 return rv;
315 return rv;
318 static void
319 cdata(void *baton, const char *data, int len)
321 dontdothat_filter_ctx *ctx = baton;
323 if (ctx->no_soup_for_you || ctx->let_it_go)
324 return;
326 switch (ctx->state)
328 case STATE_IN_SRC_PATH:
329 /* FALLTHROUGH */
331 case STATE_IN_DST_PATH:
332 /* FALLTHROUGH */
334 case STATE_IN_RECURSIVE:
335 if (! ctx->buffer)
336 ctx->buffer = svn_stringbuf_ncreate(data, len, ctx->r->pool);
337 else
338 svn_stringbuf_appendbytes(ctx->buffer, data, len);
339 break;
341 default:
342 break;
346 static void
347 start_element(void *baton, const char *name, const char **attrs)
349 dontdothat_filter_ctx *ctx = baton;
350 const char *sep;
352 if (ctx->no_soup_for_you || ctx->let_it_go)
353 return;
355 /* XXX Hack. We should be doing real namespace support, but for now we
356 * just skip ahead of any namespace prefix. If someone's sending us
357 * an update-report element outside of the SVN namespace they'll get
358 * what they deserve... */
359 sep = ap_strchr_c(name, ':');
360 if (sep)
361 name = sep + 1;
363 switch (ctx->state)
365 case STATE_BEGINNING:
366 if (strcmp(name, "update-report") == 0)
367 ctx->state = STATE_IN_UPDATE;
368 else if (strcmp(name, "replay-report") == 0)
370 /* XXX it would be useful if there was a way to override this
371 * on a per-user basis... */
372 if (! is_this_legal(ctx, ctx->r->unparsed_uri))
373 ctx->no_soup_for_you = TRUE;
374 else
375 ctx->let_it_go = TRUE;
377 else
378 ctx->let_it_go = TRUE;
379 break;
381 case STATE_IN_UPDATE:
382 if (strcmp(name, "src-path") == 0)
384 ctx->state = STATE_IN_SRC_PATH;
385 if (ctx->buffer)
386 ctx->buffer->len = 0;
388 else if (strcmp(name, "dst-path") == 0)
390 ctx->state = STATE_IN_DST_PATH;
391 if (ctx->buffer)
392 ctx->buffer->len = 0;
394 else if (strcmp(name, "recursive") == 0)
396 ctx->state = STATE_IN_RECURSIVE;
397 if (ctx->buffer)
398 ctx->buffer->len = 0;
400 else
401 ; /* XXX Figure out what else we need to deal with... Switch
402 * has that link-path thing we probably need to look out
403 * for... */
404 break;
406 default:
407 break;
411 static void
412 end_element(void *baton, const char *name)
414 dontdothat_filter_ctx *ctx = baton;
415 const char *sep;
417 if (ctx->no_soup_for_you || ctx->let_it_go)
418 return;
420 /* XXX Hack. We should be doing real namespace support, but for now we
421 * just skip ahead of any namespace prefix. If someone's sending us
422 * an update-report element outside of the SVN namespace they'll get
423 * what they deserve... */
424 sep = ap_strchr_c(name, ':');
425 if (sep)
426 name = sep + 1;
428 switch (ctx->state)
430 case STATE_IN_SRC_PATH:
431 ctx->state = STATE_IN_UPDATE;
433 svn_stringbuf_strip_whitespace(ctx->buffer);
435 if (! ctx->path_failed && ! is_this_legal(ctx, ctx->buffer->data))
436 ctx->path_failed = TRUE;
437 break;
439 case STATE_IN_DST_PATH:
440 ctx->state = STATE_IN_UPDATE;
442 svn_stringbuf_strip_whitespace(ctx->buffer);
444 if (! ctx->path_failed && ! is_this_legal(ctx, ctx->buffer->data))
445 ctx->path_failed = TRUE;
446 break;
448 case STATE_IN_RECURSIVE:
449 ctx->state = STATE_IN_UPDATE;
451 svn_stringbuf_strip_whitespace(ctx->buffer);
453 /* If this isn't recursive we let it go. */
454 if (strcmp(ctx->buffer->data, "no") == 0)
456 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->r,
457 "mod_dontdothat: letting nonrecursive request go");
458 ctx->let_it_go = TRUE;
460 break;
462 case STATE_IN_UPDATE:
463 if (strcmp(name, "update-report") == 0)
465 /* If we made it here without figuring out that this is
466 * nonrecursive, then the path check is our final word
467 * on the subject. */
469 if (ctx->path_failed)
470 ctx->no_soup_for_you = TRUE;
471 else
472 ctx->let_it_go = TRUE;
474 else
475 ; /* XXX Is there other stuff we care about? */
476 break;
478 default:
479 abort();
483 static svn_boolean_t
484 is_valid_wildcard(const char *wc)
486 while (*wc)
488 if (*wc == '*')
490 if (wc[1] && wc[1] != '/')
491 return FALSE;
494 ++wc;
497 return TRUE;
500 static svn_boolean_t
501 config_enumerator(const char *wildcard,
502 const char *action,
503 void *baton,
504 apr_pool_t *pool)
506 dontdothat_filter_ctx *ctx = baton;
508 if (strcmp(action, "deny") == 0)
510 if (is_valid_wildcard(wildcard))
511 APR_ARRAY_PUSH(ctx->no_recursive_ops, const char *) = wildcard;
512 else
513 ctx->err = svn_error_createf(APR_EINVAL,
514 NULL,
515 "'%s' is an invalid wildcard",
516 wildcard);
518 else if (strcmp(action, "allow") == 0)
520 if (is_valid_wildcard(wildcard))
521 APR_ARRAY_PUSH(ctx->allow_recursive_ops, const char *) = wildcard;
522 else
523 ctx->err = svn_error_createf(APR_EINVAL,
524 NULL,
525 "'%s' is an invalid wildcard",
526 wildcard);
528 else
530 ctx->err = svn_error_createf(APR_EINVAL,
531 NULL,
532 "'%s' is not a valid action",
533 action);
536 if (ctx->err)
537 return FALSE;
538 else
539 return TRUE;
542 static apr_status_t
543 clean_up_parser(void *baton)
545 XML_Parser xmlp = baton;
547 XML_ParserFree(xmlp);
549 return APR_SUCCESS;
552 static void
553 dontdothat_insert_filters(request_rec *r)
555 dontdothat_config_rec *cfg = ap_get_module_config(r->per_dir_config,
556 &dontdothat_module);
558 if (! cfg->config_file)
559 return;
561 if (strcmp("REPORT", r->method) == 0)
563 dontdothat_filter_ctx *ctx = apr_pcalloc(r->pool, sizeof(*ctx));
564 svn_config_t *config;
565 svn_error_t *err;
567 ctx->r = r;
569 ctx->cfg = cfg;
571 ctx->allow_recursive_ops = apr_array_make(r->pool, 5, sizeof(char *));
573 ctx->no_recursive_ops = apr_array_make(r->pool, 5, sizeof(char *));
575 /* XXX is there a way to error out from this point? Would be nice... */
577 err = svn_config_read(&config, cfg->config_file, TRUE, r->pool);
578 if (err)
580 char buff[256];
582 ap_log_rerror(APLOG_MARK, APLOG_ERR,
583 ((err->apr_err >= APR_OS_START_USERERR &&
584 err->apr_err < APR_OS_START_CANONERR) ?
585 0 : err->apr_err),
586 r, "Failed to load DontDoThatConfigFile: %s",
587 svn_err_best_message(err, buff, sizeof(buff)));
589 svn_error_clear(err);
591 return;
594 svn_config_enumerate2(config,
595 "recursive-actions",
596 config_enumerator,
597 ctx,
598 r->pool);
599 if (ctx->err)
601 char buff[256];
603 ap_log_rerror(APLOG_MARK, APLOG_ERR,
604 ((ctx->err->apr_err >= APR_OS_START_USERERR &&
605 ctx->err->apr_err < APR_OS_START_CANONERR) ?
606 0 : ctx->err->apr_err),
607 r, "Failed to parse DontDoThatConfigFile: %s",
608 svn_err_best_message(ctx->err, buff, sizeof(buff)));
610 svn_error_clear(ctx->err);
612 return;
615 ctx->state = STATE_BEGINNING;
617 ctx->xmlp = XML_ParserCreate(NULL);
619 apr_pool_cleanup_register(r->pool, ctx->xmlp,
620 clean_up_parser,
621 apr_pool_cleanup_null);
623 XML_SetUserData(ctx->xmlp, ctx);
624 XML_SetElementHandler(ctx->xmlp, start_element, end_element);
625 XML_SetCharacterDataHandler(ctx->xmlp, cdata);
627 ap_add_input_filter("DONTDOTHAT_FILTER", ctx, r, r->connection);
631 static void
632 dontdothat_register_hooks(apr_pool_t *pool)
634 ap_hook_insert_filter(dontdothat_insert_filters, NULL, NULL, APR_HOOK_FIRST);
636 ap_register_input_filter("DONTDOTHAT_FILTER",
637 dontdothat_filter,
638 NULL,
639 AP_FTYPE_RESOURCE);
642 module AP_MODULE_DECLARE_DATA dontdothat_module =
644 STANDARD20_MODULE_STUFF,
645 create_dontdothat_dir_config,
646 NULL,
647 NULL,
648 NULL,
649 dontdothat_cmds,
650 dontdothat_register_hooks