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 * ====================================================================
21 #include <http_config.h>
22 #include <http_protocol.h>
23 #include <http_request.h>
25 #include <util_filter.h>
26 #include <ap_config.h>
27 #include <apr_strings.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
;
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
));
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
),
56 "Text file containing actions to take for specific requests"),
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
;
79 /* The current location in the REPORT body. */
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
100 /* The current request. */
102 } dontdothat_filter_ctx
;
104 /* Return TRUE if wildcard WC matches path P, FALSE otherwise. */
106 matches(const char *wc
, const char *p
)
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] != '/')
120 /* If we ran out of p and we're out of wc then it matched. */
132 /* This means we hit the end of wc without running out of p. */
135 /* Or they were exactly the same length, so it's not lower. */
140 return FALSE
; /* If we don't match, then move on to the next
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
;
163 /* Ok, so we need to skip past the scheme, host, etc. */
164 uri
= ap_strstr_c(uri
, "://");
166 uri
= ap_strchr_c(uri
+ 3, '/');
170 const char *repos_path
;
172 derr
= dav_svn_split_uri(ctx
->r
,
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
,
196 if (matches(wc
, repos_path
))
198 ap_log_rerror(APLOG_MARK
, APLOG_DEBUG
, 0, ctx
->r
,
199 "mod_dontdothat: rule %s allows %s",
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
,
212 if (matches(wc
, repos_path
))
214 ap_log_rerror(APLOG_MARK
, APLOG_DEBUG
, 0, ctx
->r
,
215 "mod_dontdothat: rule %s forbids %s",
227 dontdothat_filter(ap_filter_t
*f
,
228 apr_bucket_brigade
*bb
,
229 ap_input_mode_t mode
,
230 apr_read_type_e block
,
233 dontdothat_filter_ctx
*ctx
= f
->ctx
;
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
);
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
);
259 rv
= apr_bucket_read(e
, &str
, &len
, APR_BLOCK_READ
);
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, "
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
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");
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
)
328 case STATE_IN_SRC_PATH
:
331 case STATE_IN_DST_PATH
:
334 case STATE_IN_RECURSIVE
:
336 ctx
->buffer
= svn_stringbuf_ncreate(data
, len
, ctx
->r
->pool
);
338 svn_stringbuf_appendbytes(ctx
->buffer
, data
, len
);
347 start_element(void *baton
, const char *name
, const char **attrs
)
349 dontdothat_filter_ctx
*ctx
= baton
;
352 if (ctx
->no_soup_for_you
|| ctx
->let_it_go
)
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
, ':');
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
;
375 ctx
->let_it_go
= TRUE
;
378 ctx
->let_it_go
= TRUE
;
381 case STATE_IN_UPDATE
:
382 if (strcmp(name
, "src-path") == 0)
384 ctx
->state
= STATE_IN_SRC_PATH
;
386 ctx
->buffer
->len
= 0;
388 else if (strcmp(name
, "dst-path") == 0)
390 ctx
->state
= STATE_IN_DST_PATH
;
392 ctx
->buffer
->len
= 0;
394 else if (strcmp(name
, "recursive") == 0)
396 ctx
->state
= STATE_IN_RECURSIVE
;
398 ctx
->buffer
->len
= 0;
401 ; /* XXX Figure out what else we need to deal with... Switch
402 * has that link-path thing we probably need to look out
412 end_element(void *baton
, const char *name
)
414 dontdothat_filter_ctx
*ctx
= baton
;
417 if (ctx
->no_soup_for_you
|| ctx
->let_it_go
)
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
, ':');
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
;
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
;
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
;
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
469 if (ctx
->path_failed
)
470 ctx
->no_soup_for_you
= TRUE
;
472 ctx
->let_it_go
= TRUE
;
475 ; /* XXX Is there other stuff we care about? */
484 is_valid_wildcard(const char *wc
)
490 if (wc
[1] && wc
[1] != '/')
501 config_enumerator(const char *wildcard
,
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
;
513 ctx
->err
= svn_error_createf(APR_EINVAL
,
515 "'%s' is an invalid 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
;
523 ctx
->err
= svn_error_createf(APR_EINVAL
,
525 "'%s' is an invalid wildcard",
530 ctx
->err
= svn_error_createf(APR_EINVAL
,
532 "'%s' is not a valid action",
543 clean_up_parser(void *baton
)
545 XML_Parser xmlp
= baton
;
547 XML_ParserFree(xmlp
);
553 dontdothat_insert_filters(request_rec
*r
)
555 dontdothat_config_rec
*cfg
= ap_get_module_config(r
->per_dir_config
,
558 if (! cfg
->config_file
)
561 if (strcmp("REPORT", r
->method
) == 0)
563 dontdothat_filter_ctx
*ctx
= apr_pcalloc(r
->pool
, sizeof(*ctx
));
564 svn_config_t
*config
;
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
);
582 ap_log_rerror(APLOG_MARK
, APLOG_ERR
,
583 ((err
->apr_err
>= APR_OS_START_USERERR
&&
584 err
->apr_err
< APR_OS_START_CANONERR
) ?
586 r
, "Failed to load DontDoThatConfigFile: %s",
587 svn_err_best_message(err
, buff
, sizeof(buff
)));
589 svn_error_clear(err
);
594 svn_config_enumerate2(config
,
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
);
615 ctx
->state
= STATE_BEGINNING
;
617 ctx
->xmlp
= XML_ParserCreate(NULL
);
619 apr_pool_cleanup_register(r
->pool
, ctx
->xmlp
,
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
);
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",
642 module AP_MODULE_DECLARE_DATA dontdothat_module
=
644 STANDARD20_MODULE_STUFF
,
645 create_dontdothat_dir_config
,
650 dontdothat_register_hooks