1 /* Licensed to the Apache Software Foundation (ASF) under one or more
2 * contributor license agreements. See the NOTICE file distributed with
3 * this work for additional information regarding copyright ownership.
4 * The ASF licenses this file to You under the Apache License, Version 2.0
5 * (the "License"); you may not use this file except in compliance with
6 * the License. You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
18 * mod_ext_filter allows Unix-style filters to filter http content.
22 #include "http_config.h"
24 #include "http_protocol.h"
26 #include "http_core.h"
27 #include "apr_buckets.h"
28 #include "util_filter.h"
29 #include "util_script.h"
30 #include "util_time.h"
31 #include "apr_strings.h"
35 #define APR_WANT_STRFUNC
38 typedef struct ef_server_t {
43 typedef struct ef_filter_t {
45 enum {INPUT_FILTER=1, OUTPUT_FILTER} mode;
48 const char *enable_env;
49 const char *disable_env;
51 const char *intype; /* list of IMTs we process (well, just one for now) */
52 #define INTYPE_ALL (char *)1
53 const char *outtype; /* IMT of filtered output */
54 #define OUTTYPE_UNCHANGED (char *)1
55 int preserves_content_length;
58 typedef struct ef_dir_t {
63 typedef struct ef_ctx_t {
66 apr_procattr_t *procattr;
70 #if APR_FILES_AS_SOCKETS
71 apr_pollfd_t *pollset;
75 module AP_MODULE_DECLARE_DATA ext_filter_module;
76 static const server_rec *main_server;
78 static apr_status_t ef_output_filter(ap_filter_t *, apr_bucket_brigade *);
80 #define DBGLVL_SHOWOPTIONS 1
81 #define DBGLVL_ERRORCHECK 2
84 #define ERRFN_USERDATA_KEY "EXTFILTCHILDERRFN"
86 static void *create_ef_dir_conf(apr_pool_t *p, char *dummy)
88 ef_dir_t *dc = (ef_dir_t *)apr_pcalloc(p, sizeof(ef_dir_t));
96 static void *create_ef_server_conf(apr_pool_t *p, server_rec *s)
100 conf = (ef_server_t *)apr_pcalloc(p, sizeof(ef_server_t));
102 conf->h = apr_hash_make(conf->p);
106 static void *merge_ef_dir_conf(apr_pool_t *p, void *basev, void *overridesv)
108 ef_dir_t *a = (ef_dir_t *)apr_pcalloc (p, sizeof(ef_dir_t));
109 ef_dir_t *base = (ef_dir_t *)basev, *over = (ef_dir_t *)overridesv;
111 if (over->debug != -1) { /* if admin coded something... */
112 a->debug = over->debug;
115 a->debug = base->debug;
118 if (over->log_stderr != -1) { /* if admin coded something... */
119 a->log_stderr = over->log_stderr;
122 a->log_stderr = base->log_stderr;
128 static const char *add_options(cmd_parms *cmd, void *in_dc,
131 ef_dir_t *dc = in_dc;
133 if (!strncasecmp(arg, "DebugLevel=", 11)) {
134 dc->debug = atoi(arg + 11);
136 else if (!strcasecmp(arg, "LogStderr")) {
139 else if (!strcasecmp(arg, "NoLogStderr")) {
143 return apr_pstrcat(cmd->temp_pool,
144 "Invalid ExtFilterOptions option: ",
152 static const char *parse_cmd(apr_pool_t *p, const char **args, ef_filter_t *filter)
155 const char *start = *args + 1;
160 ++*args; /* move past leading " */
161 /* find true end of args string (accounting for escaped quotes) */
162 while (**args && (**args != '"' || (**args == '"' && escaping))) {
166 else if (**args == '\\') {
172 return "Expected cmd= delimiter";
174 /* copy *just* the arg string for parsing, */
175 parms = apr_pstrndup(p, start, *args - start);
176 ++*args; /* move past trailing " */
178 /* parse and tokenize the args. */
179 rv = apr_tokenize_to_argv(parms, &(filter->args), p);
180 if (rv != APR_SUCCESS) {
181 return "cmd= parse error";
187 /* Allocate space for two argv pointers and parse the args. */
188 filter->args = (char **)apr_palloc(p, 2 * sizeof(char *));
189 filter->args[0] = ap_getword_white(p, args);
190 filter->args[1] = NULL; /* end of args */
192 if (!filter->args[0]) {
193 return "Invalid cmd= parameter";
195 filter->command = filter->args[0];
200 static const char *define_filter(cmd_parms *cmd, void *dummy, const char *args)
202 ef_server_t *conf = ap_get_module_config(cmd->server->module_config,
208 name = ap_getword_white(cmd->pool, &args);
210 return "Filter name not found";
213 if (apr_hash_get(conf->h, name, APR_HASH_KEY_STRING)) {
214 return apr_psprintf(cmd->pool, "ExtFilter %s is already defined",
218 filter = (ef_filter_t *)apr_pcalloc(conf->p, sizeof(ef_filter_t));
220 filter->mode = OUTPUT_FILTER;
221 filter->ftype = AP_FTYPE_RESOURCE;
222 apr_hash_set(conf->h, name, APR_HASH_KEY_STRING, filter);
225 while (apr_isspace(*args)) {
229 /* Nasty parsing... I wish I could simply use ap_getword_white()
230 * here and then look at the token, but ap_getword_white() doesn't
231 * do the right thing when we have cmd="word word word"
233 if (!strncasecmp(args, "preservescontentlength", 22)) {
234 token = ap_getword_white(cmd->pool, &args);
235 if (!strcasecmp(token, "preservescontentlength")) {
236 filter->preserves_content_length = 1;
239 return apr_psprintf(cmd->pool,
240 "mangled argument `%s'",
246 if (!strncasecmp(args, "mode=", 5)) {
248 token = ap_getword_white(cmd->pool, &args);
249 if (!strcasecmp(token, "output")) {
250 filter->mode = OUTPUT_FILTER;
252 else if (!strcasecmp(token, "input")) {
253 filter->mode = INPUT_FILTER;
256 return apr_psprintf(cmd->pool, "Invalid mode: `%s'",
262 if (!strncasecmp(args, "ftype=", 6)) {
264 token = ap_getword_white(cmd->pool, &args);
265 filter->ftype = atoi(token);
269 if (!strncasecmp(args, "enableenv=", 10)) {
271 token = ap_getword_white(cmd->pool, &args);
272 filter->enable_env = token;
276 if (!strncasecmp(args, "disableenv=", 11)) {
278 token = ap_getword_white(cmd->pool, &args);
279 filter->disable_env = token;
283 if (!strncasecmp(args, "intype=", 7)) {
285 filter->intype = ap_getword_white(cmd->pool, &args);
289 if (!strncasecmp(args, "outtype=", 8)) {
291 filter->outtype = ap_getword_white(cmd->pool, &args);
295 if (!strncasecmp(args, "cmd=", 4)) {
297 if ((token = parse_cmd(cmd->pool, &args, filter))) {
303 return apr_psprintf(cmd->pool, "Unexpected parameter: `%s'",
307 /* parsing is done... register the filter
309 if (filter->mode == OUTPUT_FILTER) {
310 /* XXX need a way to ensure uniqueness among all filters */
311 ap_register_output_filter(filter->name, ef_output_filter, NULL, filter->ftype);
313 #if 0 /* no input filters yet */
314 else if (filter->mode == INPUT_FILTER) {
315 /* XXX need a way to ensure uniqueness among all filters */
316 ap_register_input_filter(filter->name, ef_input_filter, NULL, AP_FTYPE_RESOURCE);
320 ap_assert(1 != 1); /* we set the field wrong somehow */
326 static const command_rec cmds[] =
328 AP_INIT_ITERATE("ExtFilterOptions",
331 ACCESS_CONF, /* same as SetInputFilter/SetOutputFilter */
332 "valid options: DebugLevel=n, LogStderr, NoLogStderr"),
333 AP_INIT_RAW_ARGS("ExtFilterDefine",
337 "Define an external filter"),
341 static int ef_init(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *main_s)
343 main_server = main_s;
347 static void register_hooks(apr_pool_t *p)
349 ap_hook_post_config(ef_init, NULL, NULL, APR_HOOK_MIDDLE);
352 static apr_status_t set_resource_limits(request_rec *r,
353 apr_procattr_t *procattr)
355 #if defined(RLIMIT_CPU) || defined(RLIMIT_NPROC) || \
356 defined(RLIMIT_DATA) || defined(RLIMIT_VMEM) || defined (RLIMIT_AS)
357 core_dir_config *conf =
358 (core_dir_config *)ap_get_module_config(r->per_dir_config,
363 rv = apr_procattr_limit_set(procattr, APR_LIMIT_CPU, conf->limit_cpu);
364 ap_assert(rv == APR_SUCCESS); /* otherwise, we're out of sync with APR */
366 #if defined(RLIMIT_DATA) || defined(RLIMIT_VMEM) || defined(RLIMIT_AS)
367 rv = apr_procattr_limit_set(procattr, APR_LIMIT_MEM, conf->limit_mem);
368 ap_assert(rv == APR_SUCCESS); /* otherwise, we're out of sync with APR */
371 rv = apr_procattr_limit_set(procattr, APR_LIMIT_NPROC, conf->limit_nproc);
372 ap_assert(rv == APR_SUCCESS); /* otherwise, we're out of sync with APR */
375 #endif /* if at least one limit defined */
380 static apr_status_t ef_close_file(void *vfile)
382 return apr_file_close(vfile);
385 static void child_errfn(apr_pool_t *pool, apr_status_t err, const char *description)
389 apr_file_t *stderr_log;
391 char time_str[APR_CTIME_LEN];
393 apr_pool_userdata_get(&vr, ERRFN_USERDATA_KEY, pool);
395 apr_file_open_stderr(&stderr_log, pool);
396 ap_recent_ctime(time_str, apr_time_now());
397 apr_file_printf(stderr_log,
398 "[%s] [client %s] mod_ext_filter (%d)%s: %s\n",
400 r->connection->remote_ip,
402 apr_strerror(err, errbuf, sizeof(errbuf)),
406 /* init_ext_filter_process: get the external filter process going
407 * This is per-filter-instance (i.e., per-request) initialization.
409 static apr_status_t init_ext_filter_process(ap_filter_t *f)
411 ef_ctx_t *ctx = f->ctx;
413 ef_dir_t *dc = ctx->dc;
414 const char * const *env;
416 ctx->proc = apr_pcalloc(ctx->p, sizeof(*ctx->proc));
418 rc = apr_procattr_create(&ctx->procattr, ctx->p);
419 ap_assert(rc == APR_SUCCESS);
421 rc = apr_procattr_io_set(ctx->procattr,
425 ap_assert(rc == APR_SUCCESS);
427 rc = set_resource_limits(f->r, ctx->procattr);
428 ap_assert(rc == APR_SUCCESS);
430 if (dc->log_stderr > 0) {
431 rc = apr_procattr_child_err_set(ctx->procattr,
432 f->r->server->error_log, /* stderr in child */
434 ap_assert(rc == APR_SUCCESS);
437 rc = apr_procattr_child_errfn_set(ctx->procattr, child_errfn);
438 ap_assert(rc == APR_SUCCESS);
439 apr_pool_userdata_set(f->r, ERRFN_USERDATA_KEY, apr_pool_cleanup_null, ctx->p);
441 if (dc->debug >= DBGLVL_ERRORCHECK) {
442 rc = apr_procattr_error_check_set(ctx->procattr, 1);
443 ap_assert(rc == APR_SUCCESS);
446 /* add standard CGI variables as well as DOCUMENT_URI, DOCUMENT_PATH_INFO,
447 * and QUERY_STRING_UNESCAPED
449 ap_add_cgi_vars(f->r);
450 ap_add_common_vars(f->r);
451 apr_table_setn(f->r->subprocess_env, "DOCUMENT_URI", f->r->uri);
452 apr_table_setn(f->r->subprocess_env, "DOCUMENT_PATH_INFO", f->r->path_info);
454 /* QUERY_STRING is added by ap_add_cgi_vars */
455 char *arg_copy = apr_pstrdup(f->r->pool, f->r->args);
456 ap_unescape_url(arg_copy);
457 apr_table_setn(f->r->subprocess_env, "QUERY_STRING_UNESCAPED",
458 ap_escape_shell_cmd(f->r->pool, arg_copy));
461 env = (const char * const *) ap_create_environment(ctx->p,
462 f->r->subprocess_env);
464 rc = apr_proc_create(ctx->proc,
465 ctx->filter->command,
466 (const char * const *)ctx->filter->args,
467 env, /* environment */
470 if (rc != APR_SUCCESS) {
471 ap_log_rerror(APLOG_MARK, APLOG_ERR, rc, f->r,
472 "couldn't create child process to run `%s'",
473 ctx->filter->command);
477 apr_pool_note_subprocess(ctx->p, ctx->proc, APR_KILL_AFTER_TIMEOUT);
479 /* We don't want the handle to the child's stdin inherited by any
480 * other processes created by httpd. Otherwise, when we close our
481 * handle, the child won't see EOF because another handle will still
485 apr_pool_cleanup_register(ctx->p, ctx->proc->in,
486 apr_pool_cleanup_null, /* other mechanism */
489 #if APR_FILES_AS_SOCKETS
491 apr_socket_t *newsock;
493 rc = apr_poll_setup(&ctx->pollset, 2, ctx->p);
494 ap_assert(rc == APR_SUCCESS);
495 rc = apr_socket_from_file(&newsock, ctx->proc->in);
496 ap_assert(rc == APR_SUCCESS);
497 rc = apr_poll_socket_add(ctx->pollset, newsock, APR_POLLOUT);
498 ap_assert(rc == APR_SUCCESS);
499 rc = apr_socket_from_file(&newsock, ctx->proc->out);
500 ap_assert(rc == APR_SUCCESS);
501 rc = apr_poll_socket_add(ctx->pollset, newsock, APR_POLLIN);
502 ap_assert(rc == APR_SUCCESS);
509 static const char *get_cfg_string(ef_dir_t *dc, ef_filter_t *filter, apr_pool_t *p)
511 const char *debug_str = dc->debug == -1 ?
512 "DebugLevel=0" : apr_psprintf(p, "DebugLevel=%d", dc->debug);
513 const char *log_stderr_str = dc->log_stderr < 1 ?
514 "NoLogStderr" : "LogStderr";
515 const char *preserve_content_length_str = filter->preserves_content_length ?
516 "PreservesContentLength" : "!PreserveContentLength";
517 const char *intype_str = !filter->intype ?
518 "*/*" : filter->intype;
519 const char *outtype_str = !filter->outtype ?
520 "(unchanged)" : filter->outtype;
522 return apr_psprintf(p,
523 "ExtFilterOptions %s %s %s ExtFilterInType %s "
524 "ExtFilterOuttype %s",
525 debug_str, log_stderr_str, preserve_content_length_str,
526 intype_str, outtype_str);
529 static ef_filter_t *find_filter_def(const server_rec *s, const char *fname)
534 sc = ap_get_module_config(s->module_config, &ext_filter_module);
535 f = apr_hash_get(sc->h, fname, APR_HASH_KEY_STRING);
536 if (!f && s != main_server) {
538 sc = ap_get_module_config(s->module_config, &ext_filter_module);
539 f = apr_hash_get(sc->h, fname, APR_HASH_KEY_STRING);
544 static apr_status_t init_filter_instance(ap_filter_t *f)
550 f->ctx = ctx = apr_pcalloc(f->r->pool, sizeof(ef_ctx_t));
551 dc = ap_get_module_config(f->r->per_dir_config,
554 /* look for the user-defined filter */
555 ctx->filter = find_filter_def(f->r->server, f->frec->name);
557 ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, f->r,
558 "couldn't find definition of filter '%s'",
563 if (ctx->filter->intype &&
564 ctx->filter->intype != INTYPE_ALL) {
565 if (!f->r->content_type) {
569 const char *ctypes = f->r->content_type;
570 const char *ctype = ap_getword(f->r->pool, &ctypes, ';');
572 if (strcasecmp(ctx->filter->intype, ctype)) {
573 /* wrong IMT for us; don't mess with the output */
578 if (ctx->filter->enable_env &&
579 !apr_table_get(f->r->subprocess_env, ctx->filter->enable_env)) {
580 /* an environment variable that enables the filter isn't set; bail */
583 if (ctx->filter->disable_env &&
584 apr_table_get(f->r->subprocess_env, ctx->filter->disable_env)) {
585 /* an environment variable that disables the filter is set; bail */
589 rv = init_ext_filter_process(f);
590 if (rv != APR_SUCCESS) {
593 if (ctx->filter->outtype &&
594 ctx->filter->outtype != OUTTYPE_UNCHANGED) {
595 ap_set_content_type(f->r, ctx->filter->outtype);
597 if (ctx->filter->preserves_content_length != 1) {
598 /* nasty, but needed to avoid confusing the browser
600 apr_table_unset(f->r->headers_out, "Content-Length");
604 if (dc->debug >= DBGLVL_SHOWOPTIONS) {
605 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r,
606 "%sfiltering `%s' of type `%s' through `%s', cfg %s",
607 ctx->noop ? "NOT " : "",
608 f->r->uri ? f->r->uri : f->r->filename,
609 f->r->content_type ? f->r->content_type : "(unspecified)",
610 ctx->filter->command,
611 get_cfg_string(dc, ctx->filter, f->r->pool));
617 /* drain_available_output():
619 * if any data is available from the filter, read it and pass it
622 static apr_status_t drain_available_output(ap_filter_t *f)
624 request_rec *r = f->r;
625 conn_rec *c = r->connection;
626 ef_ctx_t *ctx = f->ctx;
627 ef_dir_t *dc = ctx->dc;
631 apr_bucket_brigade *bb;
636 rv = apr_file_read(ctx->proc->out,
639 if ((rv && !APR_STATUS_IS_EAGAIN(rv)) ||
640 dc->debug >= DBGLVL_GORY) {
641 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, r,
642 "apr_file_read(child output), len %" APR_SIZE_T_FMT,
645 if (rv != APR_SUCCESS) {
648 bb = apr_brigade_create(r->pool, c->bucket_alloc);
649 b = apr_bucket_transient_create(buf, len, c->bucket_alloc);
650 APR_BRIGADE_INSERT_TAIL(bb, b);
651 if ((rv = ap_pass_brigade(f->next, bb)) != APR_SUCCESS) {
652 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
653 "ap_pass_brigade()");
657 /* we should never get here; if we do, a bogus error message would be
658 * the least of our problems
660 return APR_ANONYMOUS;
663 static apr_status_t pass_data_to_filter(ap_filter_t *f, const char *data,
666 ef_ctx_t *ctx = f->ctx;
667 ef_dir_t *dc = ctx->dc;
669 apr_size_t bytes_written = 0;
673 tmplen = len - bytes_written;
674 rv = apr_file_write(ctx->proc->in,
675 (const char *)data + bytes_written,
677 bytes_written += tmplen;
678 if (rv != APR_SUCCESS && !APR_STATUS_IS_EAGAIN(rv)) {
679 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, f->r,
680 "apr_file_write(child input), len %" APR_SIZE_T_FMT,
684 if (APR_STATUS_IS_EAGAIN(rv)) {
685 /* XXX handle blocking conditions here... if we block, we need
686 * to read data from the child process and pass it down to the
689 rv = drain_available_output(f);
690 if (APR_STATUS_IS_EAGAIN(rv)) {
691 #if APR_FILES_AS_SOCKETS
694 rv = apr_poll(ctx->pollset, 2,
695 &num_events, f->r->server->timeout);
696 if (rv || dc->debug >= DBGLVL_GORY) {
697 ap_log_rerror(APLOG_MARK, APLOG_DEBUG,
698 rv, f->r, "apr_poll()");
700 if (rv != APR_SUCCESS && !APR_STATUS_IS_EINTR(rv)) {
701 /* some error such as APR_TIMEUP */
704 #else /* APR_FILES_AS_SOCKETS */
705 /* Yuck... I'd really like to wait until I can read
706 * or write, but instead I have to sleep and try again
708 apr_sleep(100000); /* 100 milliseconds */
709 if (dc->debug >= DBGLVL_GORY) {
710 ap_log_rerror(APLOG_MARK, APLOG_DEBUG,
711 0, f->r, "apr_sleep()");
713 #endif /* APR_FILES_AS_SOCKETS */
715 else if (rv != APR_SUCCESS) {
719 } while (bytes_written < len);
723 static apr_status_t ef_output_filter(ap_filter_t *f, apr_bucket_brigade *bb)
725 request_rec *r = f->r;
726 conn_rec *c = r->connection;
727 ef_ctx_t *ctx = f->ctx;
734 apr_bucket *eos = NULL;
737 if ((rv = init_filter_instance(f)) != APR_SUCCESS) {
743 ap_remove_output_filter(f);
744 return ap_pass_brigade(f->next, bb);
748 APR_BRIGADE_FOREACH(b, bb) {
750 if (APR_BUCKET_IS_EOS(b)) {
755 rv = apr_bucket_read(b, &data, &len, APR_BLOCK_READ);
756 if (rv != APR_SUCCESS) {
757 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, "apr_bucket_read()");
761 /* Good cast, we just tested len isn't negative */
763 (rv = pass_data_to_filter(f, data, (apr_size_t)len))
769 apr_brigade_destroy(bb);
771 /* XXX What we *really* need to do once we've hit eos is create a pipe bucket
772 * from the child output pipe and pass down the pipe bucket + eos.
775 /* close the child's stdin to signal that no more data is coming;
776 * that will cause the child to finish generating output
778 if ((rv = apr_file_close(ctx->proc->in)) != APR_SUCCESS) {
779 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
780 "apr_file_close(child input)");
783 /* since we've seen eos and closed the child's stdin, set the proper pipe
784 * timeout; we don't care if we don't return from apr_file_read() for a while...
786 rv = apr_file_pipe_timeout_set(ctx->proc->out,
789 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
790 "apr_file_pipe_timeout_set(child output)");
797 rv = apr_file_read(ctx->proc->out,
800 if ((rv && !APR_STATUS_IS_EOF(rv) && !APR_STATUS_IS_EAGAIN(rv)) ||
801 dc->debug >= DBGLVL_GORY) {
802 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, r,
803 "apr_file_read(child output), len %" APR_SIZE_T_FMT,
806 if (APR_STATUS_IS_EAGAIN(rv)) {
808 /* should not occur, because we have an APR timeout in place */
809 AP_DEBUG_ASSERT(1 != 1);
814 if (rv == APR_SUCCESS) {
815 bb = apr_brigade_create(r->pool, c->bucket_alloc);
816 b = apr_bucket_transient_create(buf, len, c->bucket_alloc);
817 APR_BRIGADE_INSERT_TAIL(bb, b);
818 if ((rv = ap_pass_brigade(f->next, bb)) != APR_SUCCESS) {
819 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
820 "ap_pass_brigade(filtered buffer) failed");
824 } while (rv == APR_SUCCESS);
826 if (!APR_STATUS_IS_EOF(rv)) {
832 bb = apr_brigade_create(r->pool, c->bucket_alloc);
833 b = apr_bucket_eos_create(c->bucket_alloc);
834 APR_BRIGADE_INSERT_TAIL(bb, b);
835 if ((rv = ap_pass_brigade(f->next, bb)) != APR_SUCCESS) {
836 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
837 "ap_pass_brigade(eos) failed");
846 static int ef_input_filter(ap_filter_t *f, apr_bucket_brigade *bb,
847 ap_input_mode_t mode, apr_read_type_e block,
856 rv = ap_get_brigade(f->next, bb, mode, block, readbytes);
857 if (rv != APR_SUCCESS) {
861 APR_BRIGADE_FOREACH(b, bb) {
862 if (!APR_BUCKET_IS_EOS(b)) {
863 if ((rv = apr_bucket_read(b, (const char **)&buf, &len, APR_BLOCK_READ)) != APR_SUCCESS) {
864 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, f->r, "apr_bucket_read() failed");
867 ap_log_error(APLOG_MARK, APLOG_ERR, 0, NULL, "apr_bucket_read -> %d bytes",
869 while ((zero = memchr(buf, '0', len))) {
874 ap_log_error(APLOG_MARK, APLOG_ERR, 0, NULL, "got eos bucket");
881 module AP_MODULE_DECLARE_DATA ext_filter_module =
883 STANDARD20_MODULE_STUFF,
886 create_ef_server_conf,