Fix compiler warning due to missing function prototype.
[svn.git] / contrib / server-side / mod_dontdothat / mod_dontdothat.c
blobb478e15782709cda6814ab3a0e5b8f0b1181fbe1
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 int no_replay;
41 } dontdothat_config_rec;
43 static void *create_dontdothat_dir_config(apr_pool_t *pool, char *dir)
45 dontdothat_config_rec *cfg = apr_pcalloc(pool, sizeof(*cfg));
47 cfg->base_path = dir;
48 cfg->no_replay = 1;
50 return cfg;
53 static const command_rec dontdothat_cmds[] =
55 AP_INIT_TAKE1("DontDoThatConfigFile", ap_set_file_slot,
56 (void *) APR_OFFSETOF(dontdothat_config_rec, config_file),
57 OR_ALL,
58 "Text file containing actions to take for specific requests"),
59 AP_INIT_FLAG("DontDoThatDisallowReplay", ap_set_flag_slot,
60 (void *) APR_OFFSETOF(dontdothat_config_rec, no_replay),
61 OR_ALL, "Disallow replay requests as if they are other recursive requests."),
62 { NULL }
65 typedef enum {
66 STATE_BEGINNING,
67 STATE_IN_UPDATE,
68 STATE_IN_SRC_PATH,
69 STATE_IN_DST_PATH,
70 STATE_IN_RECURSIVE
71 } parse_state_t;
73 typedef struct {
74 /* Set to TRUE when we determine that the request is safe and should be
75 * allowed to continue. */
76 svn_boolean_t let_it_go;
78 /* Set to TRUE when we determine that the request is unsafe and should be
79 * stopped in its tracks. */
80 svn_boolean_t no_soup_for_you;
82 XML_Parser xmlp;
84 /* The current location in the REPORT body. */
85 parse_state_t state;
87 /* A buffer to hold CDATA we encounter. */
88 svn_stringbuf_t *buffer;
90 dontdothat_config_rec *cfg;
92 /* An array of wildcards that are special cased to be allowed. */
93 apr_array_header_t *allow_recursive_ops;
95 /* An array of wildcards where recursive operations are not allowed. */
96 apr_array_header_t *no_recursive_ops;
98 /* TRUE if a path has failed a test already. */
99 svn_boolean_t path_failed;
101 /* An error for when we're using this as a baton while parsing config
102 * files. */
103 svn_error_t *err;
105 /* The current request. */
106 request_rec *r;
107 } dontdothat_filter_ctx;
109 /* Return TRUE if wildcard WC matches path P, FALSE otherwise. */
110 static svn_boolean_t
111 matches(const char *wc, const char *p)
113 for (;;)
115 switch (*wc)
117 case '*':
118 if (wc[1] != '/' && wc[1] != '\0')
119 abort(); /* This was checked for during parsing of the config. */
121 /* It's a wild card, so eat up until the next / in p. */
122 while (*p && p[1] != '/')
123 ++p;
125 /* If we ran out of p and we're out of wc then it matched. */
126 if (! *p)
128 if (wc[1] == '\0')
129 return TRUE;
130 else
131 return FALSE;
133 break;
135 case '\0':
136 if (*p != '\0')
137 /* This means we hit the end of wc without running out of p. */
138 return FALSE;
139 else
140 /* Or they were exactly the same length, so it's not lower. */
141 return TRUE;
143 default:
144 if (*wc != *p)
145 return FALSE; /* If we don't match, then move on to the next
146 * case. */
147 else
148 break;
151 ++wc;
152 ++p;
154 if (! *p && *wc)
155 return FALSE;
159 static svn_boolean_t
160 is_this_legal(dontdothat_filter_ctx *ctx, const char *uri)
162 const char *relative_path;
163 const char *cleaned_uri;
164 const char *repos_name;
165 int trailing_slash;
166 dav_error *derr;
168 /* Ok, so we need to skip past the scheme, host, etc. */
169 uri = ap_strstr_c(uri, "://");
170 if (uri)
171 uri = ap_strchr_c(uri + 3, '/');
173 if (uri)
175 const char *repos_path;
177 derr = dav_svn_split_uri(ctx->r,
178 uri,
179 ctx->cfg->base_path,
180 &cleaned_uri,
181 &trailing_slash,
182 &repos_name,
183 &relative_path,
184 &repos_path);
185 if (! derr)
187 int idx;
189 if (! repos_path)
190 repos_path = "";
192 repos_path = apr_psprintf(ctx->r->pool, "/%s", repos_path);
194 /* First check the special cases that are always legal... */
195 for (idx = 0; idx < ctx->allow_recursive_ops->nelts; ++idx)
197 const char *wc = APR_ARRAY_IDX(ctx->allow_recursive_ops,
198 idx,
199 const char *);
201 if (matches(wc, repos_path))
203 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->r,
204 "mod_dontdothat: rule %s allows %s",
205 wc, repos_path);
206 return TRUE;
210 /* Then look for stuff we explicitly don't allow. */
211 for (idx = 0; idx < ctx->no_recursive_ops->nelts; ++idx)
213 const char *wc = APR_ARRAY_IDX(ctx->no_recursive_ops,
214 idx,
215 const char *);
217 if (matches(wc, repos_path))
219 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->r,
220 "mod_dontdothat: rule %s forbids %s",
221 wc, repos_path);
222 return FALSE;
228 return TRUE;
231 static apr_status_t
232 dontdothat_filter(ap_filter_t *f,
233 apr_bucket_brigade *bb,
234 ap_input_mode_t mode,
235 apr_read_type_e block,
236 apr_off_t readbytes)
238 dontdothat_filter_ctx *ctx = f->ctx;
239 apr_status_t rv;
240 apr_bucket *e;
242 if (mode != AP_MODE_READBYTES)
243 return ap_get_brigade(f->next, bb, mode, block, readbytes);
245 rv = ap_get_brigade(f->next, bb, mode, block, readbytes);
246 if (rv)
247 return rv;
249 for (e = APR_BRIGADE_FIRST(bb);
250 e != APR_BRIGADE_SENTINEL(bb);
251 e = APR_BUCKET_NEXT(e))
253 svn_boolean_t last = APR_BUCKET_IS_EOS(e);
254 const char *str;
255 apr_size_t len;
257 if (last)
259 str = "";
260 len = 0;
262 else
264 rv = apr_bucket_read(e, &str, &len, APR_BLOCK_READ);
265 if (rv)
266 return rv;
269 if (! XML_Parse(ctx->xmlp, str, len, last))
271 /* let_it_go so we clean up our parser, no_soup_for_you so that we
272 * bail out before bothering to parse this stuff a second time. */
273 ctx->let_it_go = TRUE;
274 ctx->no_soup_for_you = TRUE;
277 /* If we found something that isn't allowed, set the correct status
278 * and return an error so it'll bail out before it gets anywhere it
279 * can do real damage. */
280 if (ctx->no_soup_for_you)
282 /* XXX maybe set up the SVN-ACTION env var so that it'll show up
283 * in the Subversion operational logs? */
285 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r,
286 "mod_dontdothat: client broke the rules, "
287 "returning error");
289 /* Ok, pass an error bucket and an eos bucket back to the client.
291 * NOTE: The custom error string passed here doesn't seem to be
292 * used anywhere by httpd. This is quite possibly a bug.
294 * TODO: Try and pass back a custom document body containing a
295 * serialized svn_error_t so the client displays a better
296 * error message. */
297 bb = apr_brigade_create(f->r->pool, f->c->bucket_alloc);
298 e = ap_bucket_error_create(403, "No Soup For You!",
299 f->r->pool, f->c->bucket_alloc);
300 APR_BRIGADE_INSERT_TAIL(bb, e);
301 e = apr_bucket_eos_create(f->c->bucket_alloc);
302 APR_BRIGADE_INSERT_TAIL(bb, e);
304 /* Don't forget to remove us, otherwise recursion blows the stack. */
305 ap_remove_input_filter(f);
307 return ap_pass_brigade(f->r->output_filters, bb);
309 else if (ctx->let_it_go || last)
311 ap_remove_input_filter(f);
313 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r,
314 "mod_dontdothat: letting request go through");
316 return rv;
320 return rv;
323 static void
324 cdata(void *baton, const char *data, int len)
326 dontdothat_filter_ctx *ctx = baton;
328 if (ctx->no_soup_for_you || ctx->let_it_go)
329 return;
331 switch (ctx->state)
333 case STATE_IN_SRC_PATH:
334 /* FALLTHROUGH */
336 case STATE_IN_DST_PATH:
337 /* FALLTHROUGH */
339 case STATE_IN_RECURSIVE:
340 if (! ctx->buffer)
341 ctx->buffer = svn_stringbuf_ncreate(data, len, ctx->r->pool);
342 else
343 svn_stringbuf_appendbytes(ctx->buffer, data, len);
344 break;
346 default:
347 break;
351 static void
352 start_element(void *baton, const char *name, const char **attrs)
354 dontdothat_filter_ctx *ctx = baton;
355 const char *sep;
357 if (ctx->no_soup_for_you || ctx->let_it_go)
358 return;
360 /* XXX Hack. We should be doing real namespace support, but for now we
361 * just skip ahead of any namespace prefix. If someone's sending us
362 * an update-report element outside of the SVN namespace they'll get
363 * what they deserve... */
364 sep = ap_strchr_c(name, ':');
365 if (sep)
366 name = sep + 1;
368 switch (ctx->state)
370 case STATE_BEGINNING:
371 if (strcmp(name, "update-report") == 0)
372 ctx->state = STATE_IN_UPDATE;
373 else if (strcmp(name, "replay-report") == 0 && ctx->cfg->no_replay)
375 /* XXX it would be useful if there was a way to override this
376 * on a per-user basis... */
377 if (! is_this_legal(ctx, ctx->r->unparsed_uri))
378 ctx->no_soup_for_you = TRUE;
379 else
380 ctx->let_it_go = TRUE;
382 else
383 ctx->let_it_go = TRUE;
384 break;
386 case STATE_IN_UPDATE:
387 if (strcmp(name, "src-path") == 0)
389 ctx->state = STATE_IN_SRC_PATH;
390 if (ctx->buffer)
391 ctx->buffer->len = 0;
393 else if (strcmp(name, "dst-path") == 0)
395 ctx->state = STATE_IN_DST_PATH;
396 if (ctx->buffer)
397 ctx->buffer->len = 0;
399 else if (strcmp(name, "recursive") == 0)
401 ctx->state = STATE_IN_RECURSIVE;
402 if (ctx->buffer)
403 ctx->buffer->len = 0;
405 else
406 ; /* XXX Figure out what else we need to deal with... Switch
407 * has that link-path thing we probably need to look out
408 * for... */
409 break;
411 default:
412 break;
416 static void
417 end_element(void *baton, const char *name)
419 dontdothat_filter_ctx *ctx = baton;
420 const char *sep;
422 if (ctx->no_soup_for_you || ctx->let_it_go)
423 return;
425 /* XXX Hack. We should be doing real namespace support, but for now we
426 * just skip ahead of any namespace prefix. If someone's sending us
427 * an update-report element outside of the SVN namespace they'll get
428 * what they deserve... */
429 sep = ap_strchr_c(name, ':');
430 if (sep)
431 name = sep + 1;
433 switch (ctx->state)
435 case STATE_IN_SRC_PATH:
436 ctx->state = STATE_IN_UPDATE;
438 svn_stringbuf_strip_whitespace(ctx->buffer);
440 if (! ctx->path_failed && ! is_this_legal(ctx, ctx->buffer->data))
441 ctx->path_failed = TRUE;
442 break;
444 case STATE_IN_DST_PATH:
445 ctx->state = STATE_IN_UPDATE;
447 svn_stringbuf_strip_whitespace(ctx->buffer);
449 if (! ctx->path_failed && ! is_this_legal(ctx, ctx->buffer->data))
450 ctx->path_failed = TRUE;
451 break;
453 case STATE_IN_RECURSIVE:
454 ctx->state = STATE_IN_UPDATE;
456 svn_stringbuf_strip_whitespace(ctx->buffer);
458 /* If this isn't recursive we let it go. */
459 if (strcmp(ctx->buffer->data, "no") == 0)
461 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, ctx->r,
462 "mod_dontdothat: letting nonrecursive request go");
463 ctx->let_it_go = TRUE;
465 break;
467 case STATE_IN_UPDATE:
468 if (strcmp(name, "update-report") == 0)
470 /* If we made it here without figuring out that this is
471 * nonrecursive, then the path check is our final word
472 * on the subject. */
474 if (ctx->path_failed)
475 ctx->no_soup_for_you = TRUE;
476 else
477 ctx->let_it_go = TRUE;
479 else
480 ; /* XXX Is there other stuff we care about? */
481 break;
483 default:
484 abort();
488 static svn_boolean_t
489 is_valid_wildcard(const char *wc)
491 while (*wc)
493 if (*wc == '*')
495 if (wc[1] && wc[1] != '/')
496 return FALSE;
499 ++wc;
502 return TRUE;
505 static svn_boolean_t
506 config_enumerator(const char *wildcard,
507 const char *action,
508 void *baton,
509 apr_pool_t *pool)
511 dontdothat_filter_ctx *ctx = baton;
513 if (strcmp(action, "deny") == 0)
515 if (is_valid_wildcard(wildcard))
516 APR_ARRAY_PUSH(ctx->no_recursive_ops, const char *) = wildcard;
517 else
518 ctx->err = svn_error_createf(APR_EINVAL,
519 NULL,
520 "'%s' is an invalid wildcard",
521 wildcard);
523 else if (strcmp(action, "allow") == 0)
525 if (is_valid_wildcard(wildcard))
526 APR_ARRAY_PUSH(ctx->allow_recursive_ops, const char *) = wildcard;
527 else
528 ctx->err = svn_error_createf(APR_EINVAL,
529 NULL,
530 "'%s' is an invalid wildcard",
531 wildcard);
533 else
535 ctx->err = svn_error_createf(APR_EINVAL,
536 NULL,
537 "'%s' is not a valid action",
538 action);
541 if (ctx->err)
542 return FALSE;
543 else
544 return TRUE;
547 static apr_status_t
548 clean_up_parser(void *baton)
550 XML_Parser xmlp = baton;
552 XML_ParserFree(xmlp);
554 return APR_SUCCESS;
557 static void
558 dontdothat_insert_filters(request_rec *r)
560 dontdothat_config_rec *cfg = ap_get_module_config(r->per_dir_config,
561 &dontdothat_module);
563 if (! cfg->config_file)
564 return;
566 if (strcmp("REPORT", r->method) == 0)
568 dontdothat_filter_ctx *ctx = apr_pcalloc(r->pool, sizeof(*ctx));
569 svn_config_t *config;
570 svn_error_t *err;
572 ctx->r = r;
574 ctx->cfg = cfg;
576 ctx->allow_recursive_ops = apr_array_make(r->pool, 5, sizeof(char *));
578 ctx->no_recursive_ops = apr_array_make(r->pool, 5, sizeof(char *));
580 /* XXX is there a way to error out from this point? Would be nice... */
582 err = svn_config_read(&config, cfg->config_file, TRUE, r->pool);
583 if (err)
585 char buff[256];
587 ap_log_rerror(APLOG_MARK, APLOG_ERR,
588 ((err->apr_err >= APR_OS_START_USERERR &&
589 err->apr_err < APR_OS_START_CANONERR) ?
590 0 : err->apr_err),
591 r, "Failed to load DontDoThatConfigFile: %s",
592 svn_err_best_message(err, buff, sizeof(buff)));
594 svn_error_clear(err);
596 return;
599 svn_config_enumerate2(config,
600 "recursive-actions",
601 config_enumerator,
602 ctx,
603 r->pool);
604 if (ctx->err)
606 char buff[256];
608 ap_log_rerror(APLOG_MARK, APLOG_ERR,
609 ((ctx->err->apr_err >= APR_OS_START_USERERR &&
610 ctx->err->apr_err < APR_OS_START_CANONERR) ?
611 0 : ctx->err->apr_err),
612 r, "Failed to parse DontDoThatConfigFile: %s",
613 svn_err_best_message(ctx->err, buff, sizeof(buff)));
615 svn_error_clear(ctx->err);
617 return;
620 ctx->state = STATE_BEGINNING;
622 ctx->xmlp = XML_ParserCreate(NULL);
624 apr_pool_cleanup_register(r->pool, ctx->xmlp,
625 clean_up_parser,
626 apr_pool_cleanup_null);
628 XML_SetUserData(ctx->xmlp, ctx);
629 XML_SetElementHandler(ctx->xmlp, start_element, end_element);
630 XML_SetCharacterDataHandler(ctx->xmlp, cdata);
632 ap_add_input_filter("DONTDOTHAT_FILTER", ctx, r, r->connection);
636 static void
637 dontdothat_register_hooks(apr_pool_t *pool)
639 ap_hook_insert_filter(dontdothat_insert_filters, NULL, NULL, APR_HOOK_FIRST);
641 ap_register_input_filter("DONTDOTHAT_FILTER",
642 dontdothat_filter,
643 NULL,
644 AP_FTYPE_RESOURCE);
647 module AP_MODULE_DECLARE_DATA dontdothat_module =
649 STANDARD20_MODULE_STUFF,
650 create_dontdothat_dir_config,
651 NULL,
652 NULL,
653 NULL,
654 dontdothat_cmds,
655 dontdothat_register_hooks