# ./pullrev.sh 1828172 1862968 1863191 1867878 1867882 1867968 1867970 1867971 http://svn.apache.org/viewvc?view=revision&revision=1828172 http://svn.apache.org/viewvc?view=revision&revision=1862968 http://svn.apache.org/viewvc?view=revision&revision=1863191 http://svn.apache.org/viewvc?view=revision&revision=1867878 http://svn.apache.org/viewvc?view=revision&revision=1867882 http://svn.apache.org/viewvc?view=revision&revision=1867968 http://svn.apache.org/viewvc?view=revision&revision=1867970 http://svn.apache.org/viewvc?view=revision&revision=1867971 --- httpd-2.4.41/modules/generators/mod_cgi.c +++ httpd-2.4.41/modules/generators/mod_cgi.c @@ -92,6 +92,10 @@ apr_size_t bufbytes; } cgi_server_conf; +typedef struct { + apr_interval_time_t timeout; +} cgi_dirconf; + static void *create_cgi_config(apr_pool_t *p, server_rec *s) { cgi_server_conf *c = @@ -112,6 +116,12 @@ return overrides->logname ? overrides : base; } +static void *create_cgi_dirconf(apr_pool_t *p, char *dummy) +{ + cgi_dirconf *c = (cgi_dirconf *) apr_pcalloc(p, sizeof(cgi_dirconf)); + return c; +} + static const char *set_scriptlog(cmd_parms *cmd, void *dummy, const char *arg) { server_rec *s = cmd->server; @@ -150,6 +160,17 @@ return NULL; } +static const char *set_script_timeout(cmd_parms *cmd, void *dummy, const char *arg) +{ + cgi_dirconf *dc = dummy; + + if (ap_timeout_parameter_parse(arg, &dc->timeout, "s") != APR_SUCCESS) { + return "CGIScriptTimeout has wrong format"; + } + + return NULL; +} + static const command_rec cgi_cmds[] = { AP_INIT_TAKE1("ScriptLog", set_scriptlog, NULL, RSRC_CONF, @@ -158,6 +179,9 @@ "the maximum length (in bytes) of the script debug log"), AP_INIT_TAKE1("ScriptLogBuffer", set_scriptlog_buffer, NULL, RSRC_CONF, "the maximum size (in bytes) to record of a POST request"), +AP_INIT_TAKE1("CGIScriptTimeout", set_script_timeout, NULL, RSRC_CONF | ACCESS_CONF, + "The amount of time to wait between successful reads from " + "the CGI script, in seconds."), {NULL} }; @@ -471,23 +495,26 @@ apr_filepath_name_get(r->filename)); } else { + cgi_dirconf *dc = ap_get_module_config(r->per_dir_config, &cgi_module); + apr_interval_time_t timeout = dc->timeout > 0 ? dc->timeout : r->server->timeout; + apr_pool_note_subprocess(p, procnew, APR_KILL_AFTER_TIMEOUT); *script_in = procnew->out; if (!*script_in) return APR_EBADF; - apr_file_pipe_timeout_set(*script_in, r->server->timeout); + apr_file_pipe_timeout_set(*script_in, timeout); if (e_info->prog_type == RUN_AS_CGI) { *script_out = procnew->in; if (!*script_out) return APR_EBADF; - apr_file_pipe_timeout_set(*script_out, r->server->timeout); + apr_file_pipe_timeout_set(*script_out, timeout); *script_err = procnew->err; if (!*script_err) return APR_EBADF; - apr_file_pipe_timeout_set(*script_err, r->server->timeout); + apr_file_pipe_timeout_set(*script_err, timeout); } } } @@ -541,212 +568,10 @@ return APR_SUCCESS; } -static void discard_script_output(apr_bucket_brigade *bb) -{ - apr_bucket *e; - const char *buf; - apr_size_t len; - apr_status_t rv; - - for (e = APR_BRIGADE_FIRST(bb); - e != APR_BRIGADE_SENTINEL(bb); - e = APR_BUCKET_NEXT(e)) - { - if (APR_BUCKET_IS_EOS(e)) { - break; - } - rv = apr_bucket_read(e, &buf, &len, APR_BLOCK_READ); - if (rv != APR_SUCCESS) { - break; - } - } -} - #if APR_FILES_AS_SOCKETS - -/* A CGI bucket type is needed to catch any output to stderr from the - * script; see PR 22030. */ -static const apr_bucket_type_t bucket_type_cgi; - -struct cgi_bucket_data { - apr_pollset_t *pollset; - request_rec *r; -}; - -/* Create a CGI bucket using pipes from script stdout 'out' - * and stderr 'err', for request 'r'. */ -static apr_bucket *cgi_bucket_create(request_rec *r, - apr_file_t *out, apr_file_t *err, - apr_bucket_alloc_t *list) -{ - apr_bucket *b = apr_bucket_alloc(sizeof(*b), list); - apr_status_t rv; - apr_pollfd_t fd; - struct cgi_bucket_data *data = apr_palloc(r->pool, sizeof *data); - - APR_BUCKET_INIT(b); - b->free = apr_bucket_free; - b->list = list; - b->type = &bucket_type_cgi; - b->length = (apr_size_t)(-1); - b->start = -1; - - /* Create the pollset */ - rv = apr_pollset_create(&data->pollset, 2, r->pool, 0); - if (rv != APR_SUCCESS) { - ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01217) - "apr_pollset_create(); check system or user limits"); - return NULL; - } - - fd.desc_type = APR_POLL_FILE; - fd.reqevents = APR_POLLIN; - fd.p = r->pool; - fd.desc.f = out; /* script's stdout */ - fd.client_data = (void *)1; - rv = apr_pollset_add(data->pollset, &fd); - if (rv != APR_SUCCESS) { - ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01218) - "apr_pollset_add(); check system or user limits"); - return NULL; - } - - fd.desc.f = err; /* script's stderr */ - fd.client_data = (void *)2; - rv = apr_pollset_add(data->pollset, &fd); - if (rv != APR_SUCCESS) { - ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01219) - "apr_pollset_add(); check system or user limits"); - return NULL; - } - - data->r = r; - b->data = data; - return b; -} - -/* Create a duplicate CGI bucket using given bucket data */ -static apr_bucket *cgi_bucket_dup(struct cgi_bucket_data *data, - apr_bucket_alloc_t *list) -{ - apr_bucket *b = apr_bucket_alloc(sizeof(*b), list); - APR_BUCKET_INIT(b); - b->free = apr_bucket_free; - b->list = list; - b->type = &bucket_type_cgi; - b->length = (apr_size_t)(-1); - b->start = -1; - b->data = data; - return b; -} - -/* Handle stdout from CGI child. Duplicate of logic from the _read - * method of the real APR pipe bucket implementation. */ -static apr_status_t cgi_read_stdout(apr_bucket *a, apr_file_t *out, - const char **str, apr_size_t *len) -{ - char *buf; - apr_status_t rv; - - *str = NULL; - *len = APR_BUCKET_BUFF_SIZE; - buf = apr_bucket_alloc(*len, a->list); /* XXX: check for failure? */ - - rv = apr_file_read(out, buf, len); - - if (rv != APR_SUCCESS && rv != APR_EOF) { - apr_bucket_free(buf); - return rv; - } - - if (*len > 0) { - struct cgi_bucket_data *data = a->data; - apr_bucket_heap *h; - - /* Change the current bucket to refer to what we read */ - a = apr_bucket_heap_make(a, buf, *len, apr_bucket_free); - h = a->data; - h->alloc_len = APR_BUCKET_BUFF_SIZE; /* note the real buffer size */ - *str = buf; - APR_BUCKET_INSERT_AFTER(a, cgi_bucket_dup(data, a->list)); - } - else { - apr_bucket_free(buf); - a = apr_bucket_immortal_make(a, "", 0); - *str = a->data; - } - return rv; -} - -/* Read method of CGI bucket: polls on stderr and stdout of the child, - * sending any stderr output immediately away to the error log. */ -static apr_status_t cgi_bucket_read(apr_bucket *b, const char **str, - apr_size_t *len, apr_read_type_e block) -{ - struct cgi_bucket_data *data = b->data; - apr_interval_time_t timeout; - apr_status_t rv; - int gotdata = 0; - - timeout = block == APR_NONBLOCK_READ ? 0 : data->r->server->timeout; - - do { - const apr_pollfd_t *results; - apr_int32_t num; - - rv = apr_pollset_poll(data->pollset, timeout, &num, &results); - if (APR_STATUS_IS_TIMEUP(rv)) { - if (timeout) { - ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, data->r, APLOGNO(01220) - "Timeout waiting for output from CGI script %s", - data->r->filename); - return rv; - } - else { - return APR_EAGAIN; - } - } - else if (APR_STATUS_IS_EINTR(rv)) { - continue; - } - else if (rv != APR_SUCCESS) { - ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, data->r, APLOGNO(01221) - "poll failed waiting for CGI child"); - return rv; - } - - for (; num; num--, results++) { - if (results[0].client_data == (void *)1) { - /* stdout */ - rv = cgi_read_stdout(b, results[0].desc.f, str, len); - if (APR_STATUS_IS_EOF(rv)) { - rv = APR_SUCCESS; - } - gotdata = 1; - } else { - /* stderr */ - apr_status_t rv2 = log_script_err(data->r, results[0].desc.f); - if (APR_STATUS_IS_EOF(rv2)) { - apr_pollset_remove(data->pollset, &results[0]); - } - } - } - - } while (!gotdata); - - return rv; -} - -static const apr_bucket_type_t bucket_type_cgi = { - "CGI", 5, APR_BUCKET_DATA, - apr_bucket_destroy_noop, - cgi_bucket_read, - apr_bucket_setaside_notimpl, - apr_bucket_split_notimpl, - apr_bucket_copy_notimpl -}; - +#define WANT_CGI_BUCKET #endif +#include "cgi_common.h" static int cgi_handler(request_rec *r) { @@ -766,6 +591,8 @@ apr_status_t rv; cgi_exec_info_t e_info; conn_rec *c; + cgi_dirconf *dc = ap_get_module_config(r->per_dir_config, &cgi_module); + apr_interval_time_t timeout = dc->timeout > 0 ? dc->timeout : r->server->timeout; if (strcmp(r->handler, CGI_MAGIC_TYPE) && strcmp(r->handler, "cgi-script")) { return DECLINED; @@ -925,10 +752,7 @@ AP_DEBUG_ASSERT(script_in != NULL); #if APR_FILES_AS_SOCKETS - apr_file_pipe_timeout_set(script_in, 0); - apr_file_pipe_timeout_set(script_err, 0); - - b = cgi_bucket_create(r, script_in, script_err, c->bucket_alloc); + b = cgi_bucket_create(r, dc->timeout, script_in, script_err, c->bucket_alloc); if (b == NULL) return HTTP_INTERNAL_SERVER_ERROR; #else @@ -938,111 +762,7 @@ b = apr_bucket_eos_create(c->bucket_alloc); APR_BRIGADE_INSERT_TAIL(bb, b); - /* Handle script return... */ - if (!nph) { - const char *location; - char sbuf[MAX_STRING_LEN]; - int ret; - - if ((ret = ap_scan_script_header_err_brigade_ex(r, bb, sbuf, - APLOG_MODULE_INDEX))) - { - ret = log_script(r, conf, ret, dbuf, sbuf, bb, script_err); - - /* - * ret could be HTTP_NOT_MODIFIED in the case that the CGI script - * does not set an explicit status and ap_meets_conditions, which - * is called by ap_scan_script_header_err_brigade, detects that - * the conditions of the requests are met and the response is - * not modified. - * In this case set r->status and return OK in order to prevent - * running through the error processing stack as this would - * break with mod_cache, if the conditions had been set by - * mod_cache itself to validate a stale entity. - * BTW: We circumvent the error processing stack anyway if the - * CGI script set an explicit status code (whatever it is) and - * the only possible values for ret here are: - * - * HTTP_NOT_MODIFIED (set by ap_meets_conditions) - * HTTP_PRECONDITION_FAILED (set by ap_meets_conditions) - * HTTP_INTERNAL_SERVER_ERROR (if something went wrong during the - * processing of the response of the CGI script, e.g broken headers - * or a crashed CGI process). - */ - if (ret == HTTP_NOT_MODIFIED) { - r->status = ret; - return OK; - } - - return ret; - } - - location = apr_table_get(r->headers_out, "Location"); - - if (location && r->status == 200) { - /* For a redirect whether internal or not, discard any - * remaining stdout from the script, and log any remaining - * stderr output, as normal. */ - discard_script_output(bb); - apr_brigade_destroy(bb); - apr_file_pipe_timeout_set(script_err, r->server->timeout); - log_script_err(r, script_err); - } - - if (location && location[0] == '/' && r->status == 200) { - /* This redirect needs to be a GET no matter what the original - * method was. - */ - r->method = "GET"; - r->method_number = M_GET; - - /* We already read the message body (if any), so don't allow - * the redirected request to think it has one. We can ignore - * Transfer-Encoding, since we used REQUEST_CHUNKED_ERROR. - */ - apr_table_unset(r->headers_in, "Content-Length"); - - ap_internal_redirect_handler(location, r); - return OK; - } - else if (location && r->status == 200) { - /* XXX: Note that if a script wants to produce its own Redirect - * body, it now has to explicitly *say* "Status: 302" - */ - return HTTP_MOVED_TEMPORARILY; - } - - rv = ap_pass_brigade(r->output_filters, bb); - } - else /* nph */ { - struct ap_filter_t *cur; - - /* get rid of all filters up through protocol... since we - * haven't parsed off the headers, there is no way they can - * work - */ - - cur = r->proto_output_filters; - while (cur && cur->frec->ftype < AP_FTYPE_CONNECTION) { - cur = cur->next; - } - r->output_filters = r->proto_output_filters = cur; - - rv = ap_pass_brigade(r->output_filters, bb); - } - - /* don't soak up script output if errors occurred writing it - * out... otherwise, we prolong the life of the script when the - * connection drops or we stopped sending output for some other - * reason */ - if (rv == APR_SUCCESS && !r->connection->aborted) { - apr_file_pipe_timeout_set(script_err, r->server->timeout); - log_script_err(r, script_err); - } - - apr_file_close(script_err); - - return OK; /* NOT r->status, even if it has changed. */ + return cgi_handle_response(r, nph, bb, timeout, conf, dbuf, script_err); } /*============================================================================ @@ -1277,7 +997,7 @@ AP_DECLARE_MODULE(cgi) = { STANDARD20_MODULE_STUFF, - NULL, /* dir config creater */ + create_cgi_dirconf, /* dir config creater */ NULL, /* dir merger --- default is to override */ create_cgi_config, /* server config */ merge_cgi_config, /* merge server config */ --- httpd-2.4.41/modules/generators/config5.m4 +++ httpd-2.4.41/modules/generators/config5.m4 @@ -78,4 +78,15 @@ APR_ADDTO(INCLUDES, [-I\$(top_srcdir)/$modpath_current]) +AC_ARG_ENABLE(cgid-fdpassing, + [APACHE_HELP_STRING(--enable-cgid-fdpassing,Enable experimental mod_cgid support for fd passing)], + [if test "$enableval" = "yes"; then + AC_CHECK_DECL(CMSG_DATA, + [AC_DEFINE([HAVE_CGID_FDPASSING], 1, [Enable FD passing support in mod_cgid])], + [AC_MSG_ERROR([cannot support mod_cgid fd-passing on this system])], [ +#include +#include ]) + fi +]) + APACHE_MODPATH_FINISH --- httpd-2.4.41/modules/generators/mod_cgid.c +++ httpd-2.4.41/modules/generators/mod_cgid.c @@ -342,15 +342,19 @@ return close(fd); } -/* deal with incomplete reads and signals - * assume you really have to read buf_size bytes - */ -static apr_status_t sock_read(int fd, void *vbuf, size_t buf_size) +/* Read from the socket dealing with incomplete messages and signals. + * Returns 0 on success or errno on failure. Stderr fd passed as + * auxiliary data from other end is written to *errfd, or else stderr + * fileno if not present. */ +static apr_status_t sock_readhdr(int fd, int *errfd, void *vbuf, size_t buf_size) { - char *buf = vbuf; int rc; +#ifndef HAVE_CGID_FDPASSING + char *buf = vbuf; size_t bytes_read = 0; + if (errfd) *errfd = 0; + do { do { rc = read(fd, buf + bytes_read, buf_size - bytes_read); @@ -365,9 +369,60 @@ } } while (bytes_read < buf_size); + +#else /* with FD passing */ + struct msghdr msg = {0}; + struct iovec vec = {vbuf, buf_size}; + struct cmsghdr *cmsg; + union { /* union to ensure alignment */ + struct cmsghdr cm; + char buf[CMSG_SPACE(sizeof(int))]; + } u; + + msg.msg_iov = &vec; + msg.msg_iovlen = 1; + + if (errfd) { + msg.msg_control = u.buf; + msg.msg_controllen = sizeof(u.buf); + *errfd = 0; + } + + /* use MSG_WAITALL to skip loop on truncated reads */ + do { + rc = recvmsg(fd, &msg, MSG_WAITALL); + } while (rc < 0 && errno == EINTR); + + if (rc == 0) { + return ECONNRESET; + } + else if (rc < 0) { + return errno; + } + else if (rc != buf_size) { + /* MSG_WAITALL should ensure the recvmsg blocks until the + * entire length is read, but let's be paranoid. */ + return APR_INCOMPLETE; + } + + if (errfd + && (cmsg = CMSG_FIRSTHDR(&msg)) != NULL + && cmsg->cmsg_len == CMSG_LEN(sizeof(*errfd)) + && cmsg->cmsg_level == SOL_SOCKET + && cmsg->cmsg_type == SCM_RIGHTS) { + *errfd = *((int *) CMSG_DATA(cmsg)); + } +#endif + return APR_SUCCESS; } +/* As sock_readhdr but without auxiliary fd passing. */ +static apr_status_t sock_read(int fd, void *vbuf, size_t buf_size) +{ + return sock_readhdr(fd, NULL, vbuf, buf_size); +} + /* deal with signals */ static apr_status_t sock_write(int fd, const void *buf, size_t buf_size) @@ -384,7 +439,7 @@ return APR_SUCCESS; } -static apr_status_t sock_writev(int fd, request_rec *r, int count, ...) +static apr_status_t sock_writev(int fd, int auxfd, request_rec *r, int count, ...) { va_list ap; int rc; @@ -399,9 +454,39 @@ } va_end(ap); +#ifndef HAVE_CGID_FDPASSING do { rc = writev(fd, vec, count); } while (rc < 0 && errno == EINTR); +#else + { + struct msghdr msg = { 0 }; + struct cmsghdr *cmsg; + union { /* union for alignment */ + char buf[CMSG_SPACE(sizeof(int))]; + struct cmsghdr align; + } u; + + msg.msg_iov = vec; + msg.msg_iovlen = count; + + if (auxfd) { + msg.msg_control = u.buf; + msg.msg_controllen = sizeof(u.buf); + + cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + *((int *) CMSG_DATA(cmsg)) = auxfd; + } + + do { + rc = sendmsg(fd, &msg, 0); + } while (rc < 0 && errno == EINTR); + } +#endif + if (rc < 0) { return errno; } @@ -410,7 +495,7 @@ } static apr_status_t get_req(int fd, request_rec *r, char **argv0, char ***env, - cgid_req_t *req) + int *errfd, cgid_req_t *req) { int i; char **environ; @@ -421,7 +506,7 @@ r->server = apr_pcalloc(r->pool, sizeof(server_rec)); /* read the request header */ - stat = sock_read(fd, req, sizeof(*req)); + stat = sock_readhdr(fd, errfd, req, sizeof(*req)); if (stat != APR_SUCCESS) { return stat; } @@ -479,14 +564,15 @@ return APR_SUCCESS; } -static apr_status_t send_req(int fd, request_rec *r, char *argv0, char **env, - int req_type) +static apr_status_t send_req(int fd, apr_file_t *errpipe, request_rec *r, + char *argv0, char **env, int req_type) { int i; cgid_req_t req = {0}; apr_status_t stat; ap_unix_identity_t * ugid = ap_run_get_suexec_identity(r); core_dir_config *core_conf = ap_get_core_module_config(r->per_dir_config); + int errfd; if (ugid == NULL) { @@ -507,16 +593,21 @@ req.args_len = r->args ? strlen(r->args) : 0; req.loglevel = r->server->log.level; + if (errpipe) + apr_os_file_get(&errfd, errpipe); + else + errfd = 0; + /* Write the request header */ if (req.args_len) { - stat = sock_writev(fd, r, 5, + stat = sock_writev(fd, errfd, r, 5, &req, sizeof(req), r->filename, req.filename_len, argv0, req.argv0_len, r->uri, req.uri_len, r->args, req.args_len); } else { - stat = sock_writev(fd, r, 4, + stat = sock_writev(fd, errfd, r, 4, &req, sizeof(req), r->filename, req.filename_len, argv0, req.argv0_len, @@ -531,7 +622,7 @@ for (i = 0; i < req.env_count; i++) { apr_size_t curlen = strlen(env[i]); - if ((stat = sock_writev(fd, r, 2, &curlen, sizeof(curlen), + if ((stat = sock_writev(fd, 0, r, 2, &curlen, sizeof(curlen), env[i], curlen)) != APR_SUCCESS) { return stat; } @@ -582,20 +673,34 @@ } } +/* Callback executed in the forked child process if exec of the CGI + * script fails. For the fd-passing case, output to stderr goes to + * the client (request handling thread) and is logged via + * ap_log_rerror there. For the non-fd-passing case, the "fake" + * request_rec passed via userdata is used to log. */ static void cgid_child_errfn(apr_pool_t *pool, apr_status_t err, const char *description) { - request_rec *r; void *vr; apr_pool_userdata_get(&vr, ERRFN_USERDATA_KEY, pool); - r = vr; - - /* sure we got r, but don't call ap_log_rerror() because we don't - * have r->headers_in and possibly other storage referenced by - * ap_log_rerror() - */ - ap_log_error(APLOG_MARK, APLOG_ERR, err, r->server, APLOGNO(01241) "%s", description); + if (vr) { + request_rec *r = vr; + + /* sure we got r, but don't call ap_log_rerror() because we don't + * have r->headers_in and possibly other storage referenced by + * ap_log_rerror() + */ + ap_log_error(APLOG_MARK, APLOG_ERR, err, r->server, APLOGNO(01241) "%s", description); + } + else { + const char *logstr; + + logstr = apr_psprintf(pool, APLOGNO(01241) "error spawning CGI child: %s (%pm)\n", + description, &err); + fputs(logstr, stderr); + fflush(stderr); + } } static int cgid_server(void *data) @@ -669,7 +774,7 @@ } while (!daemon_should_exit) { - int errfileno = STDERR_FILENO; + int errfileno; char *argv0 = NULL; char **env = NULL; const char * const *argv; @@ -709,7 +814,7 @@ r = apr_pcalloc(ptrans, sizeof(request_rec)); procnew = apr_pcalloc(ptrans, sizeof(*procnew)); r->pool = ptrans; - stat = get_req(sd2, r, &argv0, &env, &cgid_req); + stat = get_req(sd2, r, &argv0, &env, &errfileno, &cgid_req); if (stat != APR_SUCCESS) { ap_log_error(APLOG_MARK, APLOG_ERR, stat, main_server, APLOGNO(01248) @@ -741,6 +846,16 @@ continue; } + if (errfileno == 0) { + errfileno = STDERR_FILENO; + } + else { + ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, main_server, + "using passed fd %d as stderr", errfileno); + /* Limit the received fd lifetime to pool lifetime */ + apr_pool_cleanup_register(ptrans, (void *)((long)errfileno), + close_unix_socket, close_unix_socket); + } apr_os_file_put(&r->server->error_log, &errfileno, 0, r->pool); apr_os_file_put(&inout, &sd2, 0, r->pool); @@ -800,7 +915,10 @@ close(sd2); } else { - apr_pool_userdata_set(r, ERRFN_USERDATA_KEY, apr_pool_cleanup_null, ptrans); + if (errfileno == STDERR_FILENO) { + /* Used by cgid_child_errfn without fd-passing. */ + apr_pool_userdata_set(r, ERRFN_USERDATA_KEY, apr_pool_cleanup_null, ptrans); + } argv = (const char * const *)create_argv(r->pool, NULL, NULL, NULL, argv0, r->args); @@ -1099,6 +1217,33 @@ return ret; } +/* Soak up stderr from a script and redirect it to the error log. + * TODO: log_scripterror() and this could move to cgi_common.h. */ +static apr_status_t log_script_err(request_rec *r, apr_file_t *script_err) +{ + char argsbuffer[HUGE_STRING_LEN]; + char *newline; + apr_status_t rv; + cgid_server_conf *conf = ap_get_module_config(r->server->module_config, &cgid_module); + + while ((rv = apr_file_gets(argsbuffer, HUGE_STRING_LEN, + script_err)) == APR_SUCCESS) { + + newline = strchr(argsbuffer, '\n'); + if (newline) { + char *prev = newline - 1; + if (prev >= argsbuffer && *prev == '\r') { + newline = prev; + } + + *newline = '\0'; + } + log_scripterror(r, conf, r->status, 0, argsbuffer); + } + + return rv; +} + static int log_script(request_rec *r, cgid_server_conf * conf, int ret, char *dbuf, const char *sbuf, apr_bucket_brigade *bb, apr_file_t *script_err) @@ -1204,6 +1349,13 @@ return ret; } +/* Pull in CGI bucket implementation. */ +#define cgi_server_conf cgid_server_conf +#ifdef HAVE_CGID_FDPASSING +#define WANT_CGI_BUCKET +#endif +#include "cgi_common.h" + static int connect_to_daemon(int *sdptr, request_rec *r, cgid_server_conf *conf) { @@ -1270,27 +1422,6 @@ return OK; } -static void discard_script_output(apr_bucket_brigade *bb) -{ - apr_bucket *e; - const char *buf; - apr_size_t len; - apr_status_t rv; - - for (e = APR_BRIGADE_FIRST(bb); - e != APR_BRIGADE_SENTINEL(bb); - e = APR_BUCKET_NEXT(e)) - { - if (APR_BUCKET_IS_EOS(e)) { - break; - } - rv = apr_bucket_read(e, &buf, &len, APR_BLOCK_READ); - if (rv != APR_SUCCESS) { - break; - } - } -} - /**************************************************************** * * Actual cgid handling... @@ -1395,6 +1526,7 @@ static int cgid_handler(request_rec *r) { + conn_rec *c = r->connection; int retval, nph, dbpos; char *argv0, *dbuf; apr_bucket_brigade *bb; @@ -1404,10 +1536,11 @@ int seen_eos, child_stopped_reading; int sd; char **env; - apr_file_t *tempsock; + apr_file_t *tempsock, *script_err, *errpipe_out; struct cleanup_script_info *info; apr_status_t rv; cgid_dirconf *dc; + apr_interval_time_t timeout; if (strcmp(r->handler, CGI_MAGIC_TYPE) && strcmp(r->handler, "cgi-script")) { return DECLINED; @@ -1416,7 +1549,7 @@ conf = ap_get_module_config(r->server->module_config, &cgid_module); dc = ap_get_module_config(r->per_dir_config, &cgid_module); - + timeout = dc->timeout > 0 ? dc->timeout : r->server->timeout; is_included = !strcmp(r->protocol, "INCLUDED"); if ((argv0 = strrchr(r->filename, '/')) != NULL) { @@ -1469,6 +1602,17 @@ } */ +#ifdef HAVE_CGID_FDPASSING + rv = apr_file_pipe_create(&script_err, &errpipe_out, r->pool); + if (rv) { + return log_scripterror(r, conf, HTTP_SERVICE_UNAVAILABLE, rv, APLOGNO(10176) + "could not create pipe for stderr"); + } +#else + script_err = NULL; + errpipe_out = NULL; +#endif + /* * httpd core function used to add common environment variables like * DOCUMENT_ROOT. @@ -1481,12 +1625,16 @@ return retval; } - rv = send_req(sd, r, argv0, env, CGI_REQ); + rv = send_req(sd, errpipe_out, r, argv0, env, CGI_REQ); if (rv != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01268) "write to cgi daemon process"); } + /* The write-end of the pipe is only used by the server, so close + * it here. */ + if (errpipe_out) apr_file_close(errpipe_out); + info = apr_palloc(r->pool, sizeof(struct cleanup_script_info)); info->conf = conf; info->r = r; @@ -1508,12 +1656,7 @@ */ apr_os_pipe_put_ex(&tempsock, &sd, 1, r->pool); - if (dc->timeout > 0) { - apr_file_pipe_timeout_set(tempsock, dc->timeout); - } - else { - apr_file_pipe_timeout_set(tempsock, r->server->timeout); - } + apr_file_pipe_timeout_set(tempsock, timeout); apr_pool_cleanup_kill(r->pool, (void *)((long)sd), close_unix_socket); /* Transfer any put/post args, CERN style... @@ -1605,114 +1748,19 @@ */ shutdown(sd, 1); - /* Handle script return... */ - if (!nph) { - conn_rec *c = r->connection; - const char *location; - char sbuf[MAX_STRING_LEN]; - int ret; - - bb = apr_brigade_create(r->pool, c->bucket_alloc); - b = apr_bucket_pipe_create(tempsock, c->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(bb, b); - b = apr_bucket_eos_create(c->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(bb, b); - - if ((ret = ap_scan_script_header_err_brigade_ex(r, bb, sbuf, - APLOG_MODULE_INDEX))) - { - ret = log_script(r, conf, ret, dbuf, sbuf, bb, NULL); - - /* - * ret could be HTTP_NOT_MODIFIED in the case that the CGI script - * does not set an explicit status and ap_meets_conditions, which - * is called by ap_scan_script_header_err_brigade, detects that - * the conditions of the requests are met and the response is - * not modified. - * In this case set r->status and return OK in order to prevent - * running through the error processing stack as this would - * break with mod_cache, if the conditions had been set by - * mod_cache itself to validate a stale entity. - * BTW: We circumvent the error processing stack anyway if the - * CGI script set an explicit status code (whatever it is) and - * the only possible values for ret here are: - * - * HTTP_NOT_MODIFIED (set by ap_meets_conditions) - * HTTP_PRECONDITION_FAILED (set by ap_meets_conditions) - * HTTP_INTERNAL_SERVER_ERROR (if something went wrong during the - * processing of the response of the CGI script, e.g broken headers - * or a crashed CGI process). - */ - if (ret == HTTP_NOT_MODIFIED) { - r->status = ret; - return OK; - } - - return ret; - } - - location = apr_table_get(r->headers_out, "Location"); - - if (location && location[0] == '/' && r->status == 200) { - - /* Soak up all the script output */ - discard_script_output(bb); - apr_brigade_destroy(bb); - /* This redirect needs to be a GET no matter what the original - * method was. - */ - r->method = "GET"; - r->method_number = M_GET; - - /* We already read the message body (if any), so don't allow - * the redirected request to think it has one. We can ignore - * Transfer-Encoding, since we used REQUEST_CHUNKED_ERROR. - */ - apr_table_unset(r->headers_in, "Content-Length"); - - ap_internal_redirect_handler(location, r); - return OK; - } - else if (location && r->status == 200) { - /* XXX: Note that if a script wants to produce its own Redirect - * body, it now has to explicitly *say* "Status: 302" - */ - discard_script_output(bb); - apr_brigade_destroy(bb); - return HTTP_MOVED_TEMPORARILY; - } - - rv = ap_pass_brigade(r->output_filters, bb); - if (rv != APR_SUCCESS) { - ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, - "Failed to flush CGI output to client"); - } - } - - if (nph) { - conn_rec *c = r->connection; - struct ap_filter_t *cur; - - /* get rid of all filters up through protocol... since we - * haven't parsed off the headers, there is no way they can - * work - */ - - cur = r->proto_output_filters; - while (cur && cur->frec->ftype < AP_FTYPE_CONNECTION) { - cur = cur->next; - } - r->output_filters = r->proto_output_filters = cur; - - bb = apr_brigade_create(r->pool, c->bucket_alloc); - b = apr_bucket_pipe_create(tempsock, c->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(bb, b); - b = apr_bucket_eos_create(c->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(bb, b); - ap_pass_brigade(r->output_filters, bb); - } + bb = apr_brigade_create(r->pool, c->bucket_alloc); +#ifdef HAVE_CGID_FDPASSING + b = cgi_bucket_create(r, dc->timeout, tempsock, script_err, c->bucket_alloc); + if (b == NULL) + return HTTP_INTERNAL_SERVER_ERROR; /* should call log_scripterror() w/ _UNAVAILABLE? */ +#else + b = apr_bucket_pipe_create(tempsock, c->bucket_alloc); +#endif + APR_BRIGADE_INSERT_TAIL(bb, b); + b = apr_bucket_eos_create(c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, b); - return OK; /* NOT r->status, even if it has changed. */ + return cgi_handle_response(r, nph, bb, timeout, conf, dbuf, script_err); } @@ -1829,7 +1877,7 @@ return retval; } - send_req(sd, r, command, env, SSI_REQ); + send_req(sd, NULL, r, command, env, SSI_REQ); info = apr_palloc(r->pool, sizeof(struct cleanup_script_info)); info->conf = conf; --- httpd-2.4.41/modules/generators/cgi_common.h +++ httpd-2.4.41/modules/generators/cgi_common.h @@ -0,0 +1,359 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "apr.h" +#include "apr_strings.h" +#include "apr_buckets.h" +#include "apr_lib.h" +#include "apr_poll.h" + +#define APR_WANT_STRFUNC +#define APR_WANT_MEMFUNC +#include "apr_want.h" + +#include "httpd.h" +#include "util_filter.h" + +static void discard_script_output(apr_bucket_brigade *bb) +{ + apr_bucket *e; + const char *buf; + apr_size_t len; + + for (e = APR_BRIGADE_FIRST(bb); + e != APR_BRIGADE_SENTINEL(bb) && !APR_BUCKET_IS_EOS(e); + e = APR_BRIGADE_FIRST(bb)) + { + if (apr_bucket_read(e, &buf, &len, APR_BLOCK_READ)) { + break; + } + apr_bucket_delete(e); + } +} + +#ifdef WANT_CGI_BUCKET +/* A CGI bucket type is needed to catch any output to stderr from the + * script; see PR 22030. */ +static const apr_bucket_type_t bucket_type_cgi; + +struct cgi_bucket_data { + apr_pollset_t *pollset; + request_rec *r; + apr_interval_time_t timeout; +}; + +/* Create a CGI bucket using pipes from script stdout 'out' + * and stderr 'err', for request 'r'. */ +static apr_bucket *cgi_bucket_create(request_rec *r, + apr_interval_time_t timeout, + apr_file_t *out, apr_file_t *err, + apr_bucket_alloc_t *list) +{ + apr_bucket *b = apr_bucket_alloc(sizeof(*b), list); + apr_status_t rv; + apr_pollfd_t fd; + struct cgi_bucket_data *data = apr_palloc(r->pool, sizeof *data); + + /* Disable APR timeout handling since we'll use poll() entirely. */ + apr_file_pipe_timeout_set(out, 0); + apr_file_pipe_timeout_set(err, 0); + + APR_BUCKET_INIT(b); + b->free = apr_bucket_free; + b->list = list; + b->type = &bucket_type_cgi; + b->length = (apr_size_t)(-1); + b->start = -1; + + /* Create the pollset */ + rv = apr_pollset_create(&data->pollset, 2, r->pool, 0); + if (rv != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01217) + "apr_pollset_create(); check system or user limits"); + return NULL; + } + + fd.desc_type = APR_POLL_FILE; + fd.reqevents = APR_POLLIN; + fd.p = r->pool; + fd.desc.f = out; /* script's stdout */ + fd.client_data = (void *)1; + rv = apr_pollset_add(data->pollset, &fd); + if (rv != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01218) + "apr_pollset_add(); check system or user limits"); + return NULL; + } + + fd.desc.f = err; /* script's stderr */ + fd.client_data = (void *)2; + rv = apr_pollset_add(data->pollset, &fd); + if (rv != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01219) + "apr_pollset_add(); check system or user limits"); + return NULL; + } + + data->r = r; + data->timeout = timeout; + b->data = data; + return b; +} + +/* Create a duplicate CGI bucket using given bucket data */ +static apr_bucket *cgi_bucket_dup(struct cgi_bucket_data *data, + apr_bucket_alloc_t *list) +{ + apr_bucket *b = apr_bucket_alloc(sizeof(*b), list); + APR_BUCKET_INIT(b); + b->free = apr_bucket_free; + b->list = list; + b->type = &bucket_type_cgi; + b->length = (apr_size_t)(-1); + b->start = -1; + b->data = data; + return b; +} + +/* Handle stdout from CGI child. Duplicate of logic from the _read + * method of the real APR pipe bucket implementation. */ +static apr_status_t cgi_read_stdout(apr_bucket *a, apr_file_t *out, + const char **str, apr_size_t *len) +{ + char *buf; + apr_status_t rv; + + *str = NULL; + *len = APR_BUCKET_BUFF_SIZE; + buf = apr_bucket_alloc(*len, a->list); /* XXX: check for failure? */ + + rv = apr_file_read(out, buf, len); + + if (rv != APR_SUCCESS && rv != APR_EOF) { + apr_bucket_free(buf); + return rv; + } + + if (*len > 0) { + struct cgi_bucket_data *data = a->data; + apr_bucket_heap *h; + + /* Change the current bucket to refer to what we read */ + a = apr_bucket_heap_make(a, buf, *len, apr_bucket_free); + h = a->data; + h->alloc_len = APR_BUCKET_BUFF_SIZE; /* note the real buffer size */ + *str = buf; + APR_BUCKET_INSERT_AFTER(a, cgi_bucket_dup(data, a->list)); + } + else { + apr_bucket_free(buf); + a = apr_bucket_immortal_make(a, "", 0); + *str = a->data; + } + return rv; +} + +/* Read method of CGI bucket: polls on stderr and stdout of the child, + * sending any stderr output immediately away to the error log. */ +static apr_status_t cgi_bucket_read(apr_bucket *b, const char **str, + apr_size_t *len, apr_read_type_e block) +{ + struct cgi_bucket_data *data = b->data; + apr_interval_time_t timeout = 0; + apr_status_t rv; + int gotdata = 0; + + if (block != APR_NONBLOCK_READ) { + timeout = data->timeout > 0 ? data->timeout : data->r->server->timeout; + } + + do { + const apr_pollfd_t *results; + apr_int32_t num; + + rv = apr_pollset_poll(data->pollset, timeout, &num, &results); + if (APR_STATUS_IS_TIMEUP(rv)) { + if (timeout) { + ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, data->r, APLOGNO(01220) + "Timeout waiting for output from CGI script %s", + data->r->filename); + return rv; + } + else { + return APR_EAGAIN; + } + } + else if (APR_STATUS_IS_EINTR(rv)) { + continue; + } + else if (rv != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, data->r, APLOGNO(01221) + "poll failed waiting for CGI child"); + return rv; + } + + for (; num; num--, results++) { + if (results[0].client_data == (void *)1) { + /* stdout */ + rv = cgi_read_stdout(b, results[0].desc.f, str, len); + if (APR_STATUS_IS_EOF(rv)) { + rv = APR_SUCCESS; + } + gotdata = 1; + } else { + /* stderr */ + apr_status_t rv2 = log_script_err(data->r, results[0].desc.f); + if (APR_STATUS_IS_EOF(rv2)) { + apr_pollset_remove(data->pollset, &results[0]); + } + } + } + + } while (!gotdata); + + return rv; +} + +static const apr_bucket_type_t bucket_type_cgi = { + "CGI", 5, APR_BUCKET_DATA, + apr_bucket_destroy_noop, + cgi_bucket_read, + apr_bucket_setaside_notimpl, + apr_bucket_split_notimpl, + apr_bucket_copy_notimpl +}; + +#endif /* WANT_CGI_BUCKET */ + +/* Handle the CGI response output, having set up the brigade with the + * CGI or PIPE bucket as appropriate. */ +static int cgi_handle_response(request_rec *r, int nph, apr_bucket_brigade *bb, + apr_interval_time_t timeout, cgi_server_conf *conf, + char *logdata, apr_file_t *script_err) +{ + apr_status_t rv; + + /* Handle script return... */ + if (!nph) { + const char *location; + char sbuf[MAX_STRING_LEN]; + int ret; + + if ((ret = ap_scan_script_header_err_brigade_ex(r, bb, sbuf, + APLOG_MODULE_INDEX))) + { + ret = log_script(r, conf, ret, logdata, sbuf, bb, script_err); + + /* + * ret could be HTTP_NOT_MODIFIED in the case that the CGI script + * does not set an explicit status and ap_meets_conditions, which + * is called by ap_scan_script_header_err_brigade, detects that + * the conditions of the requests are met and the response is + * not modified. + * In this case set r->status and return OK in order to prevent + * running through the error processing stack as this would + * break with mod_cache, if the conditions had been set by + * mod_cache itself to validate a stale entity. + * BTW: We circumvent the error processing stack anyway if the + * CGI script set an explicit status code (whatever it is) and + * the only possible values for ret here are: + * + * HTTP_NOT_MODIFIED (set by ap_meets_conditions) + * HTTP_PRECONDITION_FAILED (set by ap_meets_conditions) + * HTTP_INTERNAL_SERVER_ERROR (if something went wrong during the + * processing of the response of the CGI script, e.g broken headers + * or a crashed CGI process). + */ + if (ret == HTTP_NOT_MODIFIED) { + r->status = ret; + return OK; + } + + return ret; + } + + location = apr_table_get(r->headers_out, "Location"); + + if (location && r->status == 200) { + /* For a redirect whether internal or not, discard any + * remaining stdout from the script, and log any remaining + * stderr output, as normal. */ + discard_script_output(bb); + apr_brigade_destroy(bb); + + if (script_err) { + apr_file_pipe_timeout_set(script_err, timeout); + log_script_err(r, script_err); + } + } + + if (location && location[0] == '/' && r->status == 200) { + /* This redirect needs to be a GET no matter what the original + * method was. + */ + r->method = "GET"; + r->method_number = M_GET; + + /* We already read the message body (if any), so don't allow + * the redirected request to think it has one. We can ignore + * Transfer-Encoding, since we used REQUEST_CHUNKED_ERROR. + */ + apr_table_unset(r->headers_in, "Content-Length"); + + ap_internal_redirect_handler(location, r); + return OK; + } + else if (location && r->status == 200) { + /* XXX: Note that if a script wants to produce its own Redirect + * body, it now has to explicitly *say* "Status: 302" + */ + discard_script_output(bb); + apr_brigade_destroy(bb); + return HTTP_MOVED_TEMPORARILY; + } + + rv = ap_pass_brigade(r->output_filters, bb); + } + else /* nph */ { + struct ap_filter_t *cur; + + /* get rid of all filters up through protocol... since we + * haven't parsed off the headers, there is no way they can + * work + */ + + cur = r->proto_output_filters; + while (cur && cur->frec->ftype < AP_FTYPE_CONNECTION) { + cur = cur->next; + } + r->output_filters = r->proto_output_filters = cur; + + rv = ap_pass_brigade(r->output_filters, bb); + } + + /* don't soak up script output if errors occurred writing it + * out... otherwise, we prolong the life of the script when the + * connection drops or we stopped sending output for some other + * reason */ + if (script_err && rv == APR_SUCCESS && !r->connection->aborted) { + apr_file_pipe_timeout_set(script_err, timeout); + log_script_err(r, script_err); + } + + if (script_err) apr_file_close(script_err); + + return OK; /* NOT r->status, even if it has changed. */ +}